guest_worker/sorteerhoed/webserver.py

216 lines
7.4 KiB
Python
Raw Normal View History

import argparse
import json
import logging
import os
2019-09-11 18:16:33 +02:00
import tornado.ioloop
import tornado.web
import tornado.websocket
from urllib.parse import urlparse
import uuid
import coloredlogs
import glob
2019-09-11 18:16:33 +02:00
from pyaxidraw import axidraw # import module
2019-10-23 10:56:28 +02:00
from threading import Thread, Event
from queue import Queue, Empty
import threading
2019-10-23 10:56:28 +02:00
from server_test import generated_image_dir
import asyncio
2019-10-23 10:56:28 +02:00
logger = logging.getLogger("sorteerhoed").getChild("webserver")
2019-09-11 18:16:33 +02:00
class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
"""For subclass to add extra headers to the response"""
if path[-5:] == '.html':
self.set_header("Access-Control-Allow-Origin", "*")
if path[-4:] == '.svg':
self.set_header("Content-Type", "image/svg+xml")
2019-09-11 18:16:33 +02:00
class WebSocketHandler(tornado.websocket.WebSocketHandler):
2019-09-12 14:11:47 +02:00
CORS_ORIGINS = ['localhost', '.mturk.com', 'here.rubenvandeven.com']
2019-09-11 18:16:33 +02:00
connections = set()
2019-10-23 10:56:28 +02:00
def initialize(self, draw_q: Queue, generated_image_dir: str):
self.draw_q = draw_q
2019-10-23 10:56:28 +02:00
self.generated_image_dir = generated_image_dir
2019-09-11 18:16:33 +02:00
def check_origin(self, origin):
parsed_origin = urlparse(origin)
# parsed_origin.netloc.lower() gives localhost:3333
valid = any([parsed_origin.hostname.endswith(origin) for origin in self.CORS_ORIGINS])
return valid
# the client connected
def open(self, p = None):
self.__class__.connections.add(self)
2019-09-12 14:52:38 +02:00
logger.info(f"New client connected: {self.request.remote_ip}")
self.strokes = []
# self.write_message("hello!")
2019-09-11 18:16:33 +02:00
# the client sent the message
def on_message(self, message):
logger.debug(f"recieve: {message}")
try:
msg = json.loads(message)
# TODO: sanitize input: min/max, limit strokes
2019-09-11 18:16:33 +02:00
if msg['action'] == 'move':
# TODO: min/max input
point = [float(msg['direction'][0]),float(msg['direction'][1]), bool(msg['mouse'])]
self.strokes.append(point)
self.draw_q.put(point)
2019-09-11 18:16:33 +02:00
elif msg['action'] == 'up':
logger.info(f'up: {msg}')
point = [msg['direction'][0],msg['direction'][1], 1]
self.strokes.append(point)
2019-09-11 18:16:33 +02:00
elif msg['action'] == 'submit':
logger.info(f'up: {msg}')
id = self.submit_strokes()
if not id:
self.write_message(json.dumps('error'))
return
self.write_message(json.dumps({
'action': 'submitted',
'msg': f"Submission ok, please refer to your submission as: {id}"
}))
2019-09-11 18:16:33 +02:00
elif msg['action'] == 'down':
# not used, implicit in move?
pass
else:
# self.send({'alert': 'Unknown request: {}'.format(message)})
logger.warn('Unknown request: {}'.format(message))
except Exception as e:
# self.send({'alert': 'Invalid request: {}'.format(e)})
logger.exception(e)
# client disconnected
def on_close(self):
self.__class__.rmConnection(self)
2019-09-12 14:52:38 +02:00
logger.info(f"Client disconnected: {self.request.remote_ip}")
def submit_strokes(self):
if len(self.strokes) < 1:
return False
d = strokes2D(self.strokes)
svg = f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 600 600"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
>
<path d="{d}" style="stroke:black;stroke-width:2;fill:none;" />
</svg>
"""
id = uuid.uuid4().hex
2019-10-23 10:56:28 +02:00
filename = os.path.join(self.generated_image_dir , id+'.svg')
with open(filename, 'w') as fp:
logger.info(f"Wrote {filename}")
fp.write(svg)
return id
2019-09-11 18:16:33 +02:00
@classmethod
def rmConnection(cls, client):
if client not in cls.connections:
return
cls.connections.remove(client)
class LatestImageHandler(tornado.web.RequestHandler):
2019-10-23 10:56:28 +02:00
def initialize(self, generated_image_dir: str):
self.generated_image_dir = generated_image_dir
def get(self):
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
self.set_header("Content-Type", "image/svg+xml")
2019-10-23 10:56:28 +02:00
list_of_files = glob.glob(os.path.join(self.generated_image_dir,'*.svg'))
latest_file = max(list_of_files, key=os.path.getctime)
with open(latest_file, 'r') as fp:
self.write(fp.read())
2019-09-11 18:16:33 +02:00
2019-10-23 10:56:28 +02:00
def strokes2D(strokes):
# strokes to a d attribute for a path
d = "";
last_stroke = None;
cmd = "";
for stroke in strokes:
if not last_stroke:
d += f"M{stroke[0]},{stroke[1]} "
cmd = 'M'
else:
if last_stroke[2] == 1:
d += " m"
cmd = 'm'
elif cmd != 'l':
d+=' l '
cmd = 'l'
rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += f"{rel_stroke[0]},{rel_stroke[1]} "
last_stroke = stroke;
return d;
class Server:
"""
Server for HIT -> plotter events
As well as for the Status interface
2019-10-23 10:56:28 +02:00
TODO: change to have the HIT_id as param to the page. Load hit from storage with previous image
"""
def __init__(self, config, eventQ: Queue, runningEvent: Event, plotterQ: Queue):
self.isRunning = runningEvent
self.eventQ = eventQ
self.config = config
self.logger = logger
2019-10-23 10:56:28 +02:00
self.plotterQ = plotterQ # communicate directly to plotter (skip main thread)
2019-10-23 10:56:28 +02:00
#self.config['server']['port']
self.generated_image_dir = os.path.join('www','generated')
self.static_file_dir = os.path.join('www')
2019-10-23 10:56:28 +02:00
self.server_loop = None
def start(self):
try:
asyncio.set_event_loop(asyncio.new_event_loop())
application = tornado.web.Application([
(r"/ws(.*)", WebSocketHandler, {'draw_q': self.plotterQ, 'generated_image_dir': self.generated_image_dir}),
(r"/latest.svg", LatestImageHandler, {'generated_image_dir': self.generated_image_dir}), # TODO: have js request the right image, based on a 'start' button. This way we can trace the history of a drawing
(r"/(.*)", StaticFileWithHeaderHandler,
{"path": self.static_file_dir, "default_filename": 'index.html'}),
], debug=True, autoreload=False)
application.listen(self.config['server']['port'])
self.server_loop = tornado.ioloop.IOLoop.current()
if self.isRunning.is_set():
self.server_loop.start()
finally:
self.logger.info("Stopping webserver")
self.isRunning.clear()
def stop(self):
if self.server_loop:
self.logger.debug("Got call to stop")
self.server_loop.asyncio_loop.call_soon_threadsafe(self._stop)
2019-10-23 10:56:28 +02:00
def _stop(self):
self.server_loop.stop()