import argparse import json import logging import os import tornado.ioloop import tornado.web import tornado.websocket from urllib.parse import urlparse import uuid import coloredlogs import glob from pyaxidraw import axidraw # import module from threading import Thread, Event from queue import Queue, Empty import threading from server_test import generated_image_dir import asyncio logger = logging.getLogger("sorteerhoed").getChild("webserver") 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") class WebSocketHandler(tornado.websocket.WebSocketHandler): CORS_ORIGINS = ['localhost', '.mturk.com', 'here.rubenvandeven.com'] connections = set() def initialize(self, draw_q: Queue, generated_image_dir: str): self.draw_q = draw_q self.generated_image_dir = generated_image_dir 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) logger.info(f"New client connected: {self.request.remote_ip}") self.strokes = [] # self.write_message("hello!") # 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 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) elif msg['action'] == 'up': logger.info(f'up: {msg}') point = [msg['direction'][0],msg['direction'][1], 1] self.strokes.append(point) 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}" })) 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) 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""" """ id = uuid.uuid4().hex 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 @classmethod def rmConnection(cls, client): if client not in cls.connections: return cls.connections.remove(client) class LatestImageHandler(tornado.web.RequestHandler): 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") 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()) 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 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 self.plotterQ = plotterQ # communicate directly to plotter (skip main thread) #self.config['server']['port'] self.generated_image_dir = os.path.join('www','generated') self.static_file_dir = os.path.join('www') 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) def _stop(self): self.server_loop.stop()