WIP on the server

This commit is contained in:
Ruben van de Ven 2019-10-23 22:33:37 +02:00
parent 189223fdba
commit a9e8ec4069
17 changed files with 501 additions and 23711 deletions

View File

@ -9,6 +9,9 @@ coloredlogs = "*"
boto3 = "*"
PyYAML = "*"
SQLAlchemy = "*"
httpagentparser = "*"
geoip2 = "*"
ink-extensions = "*"
[dev-packages]

99
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "2ecd89c76c2fb319746f9616100668ec8f556d502ec72fe2644bc5b491307fa7"
"sha256": "860fb04e54e9877d9409d7e4853e80ab3d7c0bee9fe3a538d99a73f460dc363d"
},
"pipfile-spec": 6,
"requires": {
@ -21,18 +21,32 @@
},
"boto3": {
"hashes": [
"sha256:839285fbd6f3ab16170af449ae9e33d0eccf97ca22de17d9ff68b8da2310ea06",
"sha256:d93f1774c4bc66e02acdda2067291acb9e228a035435753cb75f83ad2904cbe3"
"sha256:2edee79d0e78c08b6d14d4dd91c0e4b3438dd4c90c859f06a397268b1cac17b2",
"sha256:3cd2078144c10417eb04e4bb263ea8e50a21c4aceafb52db33e3fe71e73b48aa"
],
"index": "pypi",
"version": "==1.9.253"
"version": "==1.10.0"
},
"botocore": {
"hashes": [
"sha256:3baf129118575602ada9926f5166d82d02273c250d0feb313fc270944b27c48b",
"sha256:dc080aed4f9b220a9e916ca29ca97a9d37e8e1d296fe89cbaeef929bf0c8066b"
"sha256:507b8f13583a64ec2c9c112ff6e3dd8b548060adc7e1f57f25fda9fa34c2dfdb",
"sha256:c4b2ffb0f6ed7169beb260485bf5a42ee72a0a02f49f48b0557ed5e32bcf9e79"
],
"version": "==1.12.253"
"version": "==1.13.0"
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
],
"version": "==2019.9.11"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"coloredlogs": {
"hashes": [
@ -50,6 +64,21 @@
],
"version": "==0.15.2"
},
"geoip2": {
"hashes": [
"sha256:a37ddac2d200ffb97c736da8b8ba9d5d8dc47da6ec0f162a461b681ecac53a14",
"sha256:f7ffe9d258e71a42cf622ce6350d976de1d0312b9f2fbce3975c7d838b57ecf0"
],
"index": "pypi",
"version": "==2.9.0"
},
"httpagentparser": {
"hashes": [
"sha256:23805523b61b04b9412cd98cd1fe0415401045fd5447d3b62a02ae60d22814ed"
],
"index": "pypi",
"version": "==1.9.0"
},
"humanfriendly": {
"hashes": [
"sha256:23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d",
@ -57,6 +86,21 @@
],
"version": "==4.18"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"ink-extensions": {
"hashes": [
"sha256:0157737f30063e48bbfb45beeef0545b01bb7d9b182b3f39695e9411f4d029eb",
"sha256:76e6e460307c96b05995790f30207fc1cc0f42d8795a4bb7b34cd2476f5ab70b"
],
"index": "pypi",
"version": "==1.0.2"
},
"jmespath": {
"hashes": [
"sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6",
@ -64,6 +108,39 @@
],
"version": "==0.9.4"
},
"lxml": {
"hashes": [
"sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4",
"sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc",
"sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1",
"sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046",
"sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36",
"sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5",
"sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d",
"sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916",
"sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0",
"sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27",
"sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc",
"sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7",
"sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38",
"sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5",
"sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832",
"sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a",
"sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f",
"sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9",
"sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692",
"sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84",
"sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79",
"sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681"
],
"version": "==4.4.1"
},
"maxminddb": {
"hashes": [
"sha256:449a1713d37320d777d0db286286ab22890f0a176492ecf3ad8d9319108f2f79"
],
"version": "==1.5.1"
},
"python-dateutil": {
"hashes": [
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
@ -91,6 +168,13 @@
"index": "pypi",
"version": "==5.1.2"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"version": "==2.22.0"
},
"s3transfer": {
"hashes": [
"sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d",
@ -130,7 +214,6 @@
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
],
"markers": "python_version >= '3.4'",
"version": "==1.25.6"
}
},

22
README.md Normal file
View File

@ -0,0 +1,22 @@
## Webserver
Webserver is published to the web trough ssh remote forward. In /etc/ssh/sshd_config set `GatewayPorts yes`.
Then start `autossh` to maintain the connection:
```bash
autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -R 8888:localhost:8888 here.rubenvandeven.com
```
To resolve the country the (imprecise) GeoLite2 Free Country dataset is used. Download it [from MaxMind](https://dev.maxmind.com/geoip/geoip2/geolite2/) and store it in the project root folder.
## Scanning
For scanning run visudo and add to the sudoers file:
```
mt ALL=(ALL) NOPASSWD: /usr/bin/scanimage
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@ class HIT(Base):
hit_id = Column(String(255)) # amazon's hit id
created_at = Column(DateTime, default=datetime.datetime.now())
updated_at = Column(DateTime, default=datetime.datetime.now())
uniqid = Column(String(32), default=uuid.uuid4().hex)
uuid = Column(String(32), default=lambda : uuid.uuid4().hex)
assignment_id = Column(String(255), default = None)
worker_id = Column(String(255), default = None)
accept_time = Column(DateTime, default=None)
@ -54,6 +54,13 @@ class HIT(Base):
turk_country = Column(String(255), default=None)
turk_screen_width = Column(Integer, default = None)
turk_screen_height = Column(Integer, default = None)
def getImagePath(self):
return os.path.join('www', self.getImageUrl())
def getImageUrl(self):
return f"scans/{self.id}.png"
class Store:
@ -62,32 +69,60 @@ class Store:
if logLevel <= logging.DEBUG:
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
self.engine = create_engine('sqlite:///'+path, echo=False)
self.engine = create_engine('sqlite:///'+path, echo=False, connect_args={'check_same_thread': False})
Base.metadata.create_all(self.engine)
self.Session = sessionmaker(bind=self.engine)
self.session = self.Session()
@contextmanager
def getSession(self):
"""Provide a transactional scope around a series of operations."""
session = self.Session()
try:
yield session
session.commit()
yield self.session
self.session.commit()
except:
session.rollback()
self.session.rollback()
raise
finally:
session.close()
def getHits(self, session):
return session.query(Source).order_by(HIT.created_at.desc())
return self.session.query(Source).order_by(HIT.created_at.desc())
def getHitById(self, hitId):
return self.session.query(HIT).\
filter(HIT.id==hitId).one()
def getHitByRemoteId(self, amazonHitId):
return self.session.query(HIT).\
filter(HIT.hit_id==amazonHitId).one()
def getLastSubmittedHit(self):
return self.session.query(HIT).\
filter(HIT.submit_page_at!=None).\
order_by(HIT.submit_page_at.desc()).first()
def createHIT(self):
with self.getSession() as s:
hit = HIT()
s.add(hit)
s.flush()
s.refresh(hit)
logger.info(f"Created HIT {hit.id}")
return hit
def saveHIT(self, hit):
with self.getSession() as s:
logger.info(f"Updating hit! {hit.id}")
# s.flush()
def addHIT(self, hit: HIT):
with self.getSession() as s:
s.add(hit)
s.flush()
s.refresh(hit)
logging.info(f"Added {hit.id}")
logger.info(f"Added {hit.id}")
# def rmSource(self, id: int):
# with self.getSession() as session:

View File

@ -5,6 +5,9 @@ class Signal:
Named 'signal' to avoid confusion with threading.Event
"""
def __init__(self, name: str, params: dict):
def __init__(self, name: str, params: dict = None):
self.name = name
self.params = params
self.params = params
def __repr__(self):
return f"<Signal {self.name}: {self.params}>"

View File

@ -11,6 +11,10 @@ import queue
from sorteerhoed.sqs import SqsListener
from sorteerhoed.webserver import Server
import time
from sorteerhoed.Signal import Signal
import io
from PIL import Image
import datetime
class CentralManagement():
@ -67,27 +71,30 @@ class CentralManagement():
self.logger.info(f"Mechanical turk: {self.mturk.get_account_balance()}")
self.sqs = SqsListener(self.config, self.eventQueue, self.isRunning)
sqsThread = threading.Thread(target=self.sqs.start)
sqsThread = threading.Thread(target=self.sqs.start, name='sqs')
sqsThread.start()
# the plotter itself
self.plotter = Plotter(self.config, self.eventQueue, self.isRunning)
plotterThread = threading.Thread(target=self.plotter.start)
plotterThread = threading.Thread(target=self.plotter.start, name='plotter')
plotterThread.start()
# webserver for turks and status
self.server = Server(self.config, self.eventQueue, self.isRunning, self.plotter.q)
serverThread = threading.Thread(target=self.server.start)
self.server = Server(self.config, self.eventQueue, self.isRunning, self.plotter.q, self.store)
serverThread = threading.Thread(target=self.server.start, name='server')
serverThread.start()
# event listener:
dispatcherThread = threading.Thread(target=self.eventListener)
dispatcherThread = threading.Thread(target=self.eventListener, name='dispatcher')
dispatcherThread.start()
#
#
self.makeHit()
self.eventQueue.put(Signal('start', {'ding':'test'}))
while self.isRunning.is_set():
time.sleep(.5)
finally:
self.isRunning.clear()
@ -111,19 +118,55 @@ class CentralManagement():
- Plotter complete
-
"""
print(signal)
#TODO: make level debug()
self.logger.warn(f"SIGNAL: {signal}")
if signal.name == 'start':
self.makeHit()
pass
elif signal.name == 'hit.scanned':
# TODO: wrap up hit & make new HIT
pass
elif signal.name == 'hit.info':
if signal.params['hit_id'] != self.currentHit.id:
self.logger.warning(f"hit.info hit_id != currenthit.id: {signal}")
continue
for name, value in enumerate(signal.params):
self.logger.debug(f'Set status: {name} to {value}')
self.server.statusPage.set(name, value)
elif signal.name == 'server.submit':
self.currentHit.submit_page_at = datetime.datetime.now()
self.store.saveHIT(self.currentHit)
self.plotter.park()
# park should alway triggers a plotter.finished after being processed
elif signal.name == 'sqs.AssignmentAccepted':
# {'event': {'HITGroupId': '301G7MYOAJ85NEW128ZDGF5DSBW53S', 'EventType': 'AssignmentAccepted', 'EventTimestamp': '2019-10-23T20:16:10Z', 'HITId': '3IH9TRB0FBAKKZFP3JUD6D9YWQ1I1F', 'AssignmentId': '3BF51CHDTWLN3ZGHRKDUHFKPWIJ0H3', 'WorkerId': 'A1CK46PK9VEUH5', 'HITTypeId': '3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0'}}
pass
elif signal.name == 'sqs.AssignmentAbandoned':
#{'event': {'HITGroupId': '301G7MYOAJ85NEW128ZDGF5DSBW53S', 'EventType': 'AssignmentAbandoned', 'EventTimestamp': '2019-10-23T20:23:06Z', 'HITId': '3JHB4BPSFKKFQ263K4EFULI3LC79QJ', 'AssignmentId': '3U088ZLJVL450PB6MJZUIIUCB6VW0Y', 'WorkerId': 'A1CK46PK9VEUH5', 'HITTypeId': '3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0'}}
pass
elif signal.name == 'sqs.AssignmentReturned':
# {'event': {'HITGroupId': '301G7MYOAJ85NEW128ZDGF5DSBW53S', 'EventType': 'AssignmentReturned', 'EventTimestamp': '2019-10-23T20:16:47Z', 'HITId': '3IH9TRB0FBAKKZFP3JUD6D9YWQ1I1F', 'AssignmentId': '3BF51CHDTWLN3ZGHRKDUHFKPWIJ0H3', 'WorkerId': 'A1CK46PK9VEUH5', 'HITTypeId': '3EYXOXDEN7RX0YSMN4UMVN01AYKZJ0'}}
pass
elif signal.name == 'sqs.AssignmentSubmitted':
pass
elif signal.name == 'plotter.finished':
if self.currentHit.submit_page_at:
# TODO: scan!
pass
elif signal.name == '':
pass
# handle singals/events:
# TODO: next steps
# TODO: update status
def makeHit(self):
self.currentHit = HITStore.HIT()
self.store.addHIT(self.currentHit)
self.currentHit = self.store.createHIT()
self.logger(f"Make HIT {self.currentHit.id}")
self.logger.info(f"Make HIT {self.currentHit.id}")
question = open(self.config['amazon']['task_xml'], mode='r').read().replace("{HIT_NR}",self.currentHit.id)
question = open(self.config['amazon']['task_xml'], mode='r').read().replace("{HIT_NR}",str(self.currentHit.id))
new_hit = self.mturk.create_hit(
Title = 'Trace the drawn line',
Description = 'Draw a line over the sketched line in the image',
@ -136,12 +179,11 @@ class CentralManagement():
Question = question,
)
self.logger.info("Created hit:", new_hit)
self.logger.info(f"Created hit: {new_hit}")
self.logger.info("https://workersandbox.mturk.com/mturk/preview?groupId=" + new_hit['HIT']['HITGroupId'])
self.currentHit.hit_id = new_hit['HIT']['HITId']
print(self.currentHit)
self.store.saveHIT(self.currentHit)
# mturk.send_test_event_notification()
if self.config['amazon']['sqs_url']:
@ -175,10 +217,18 @@ class CentralManagement():
Run scanimage on scaner and returns a string with the filename
"""
cmd = [
'sudo', 'scanimage'
'sudo', 'scanimage', '-d', 'epkowa'
]
filename = ""
filename = self.currentHit.getImagePath()
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
o, e = proc.communicate(60)
# opens connection to scanner, but only starts scanning when output becomes ready:
o, e = proc.communicate(80)
if e:
self.logger.critical(f"Scanner caused: {e.decode()}")
f = io.BytesIO(o)
img = Image.open(f)
img.save(filename)
self.eventQueue.put(Signal('hit.scanned', {'hit_id':self.currentHit.id}))
exec

View File

@ -14,6 +14,16 @@ class Plotter:
self.isRunning = runningEvent
self.logger = logging.getLogger("sorteerhoed").getChild("plotter")
def park(self):
self.logger.info("Queue to park plotter")
if self.config['dummy_plotter']:
# fastest way home :-)
self.q.put([0,0,0])
else:
# TODO: find a nice way to park the axidraw in the 0 position
self.q.put([0,0,0])
def start(self):
self.axiDrawCueListener()
@ -31,24 +41,24 @@ class Plotter:
self.eventQ.put(Signal('plotter.finished'))
else:
time.sleep(.05)
self.logging.debug(f'Dummy plotter move: {move}')
self.logger.debug(f'Dummy plotter move: {move}')
self.logger.info("Stopping dummy plotter")
else:
ad = axidraw.AxiDraw()
self.ad = axidraw.AxiDraw()
ad.interactive()
self.ad.interactive()
connected = ad.connect()
connected = self.ad.connect()
if not connected:
raise Exception("Cannot connect to Axidraw")
try:
ad.options.units = 1 # set to use centimeters instead of inches
ad.options.accel = 100;
ad.options.speed_penup = 100
ad.options.speed_pendown = 100
ad.options.model = 2 # A3, set to 1 for A4
self.ad.options.units = 1 # set to use centimeters instead of inches
self.ad.options.accel = 100;
self.ad.options.speed_penup = 100
self.ad.options.speed_pendown = 100
self.ad.options.model = 1 # A3, set to 1 for A4
ad.moveto(0,0)
self.ad.moveto(0,0)
plotterWidth = 22
plotterHeight = 18 # 16?
@ -65,14 +75,14 @@ class Plotter:
plotterRan = False
self.eventQ.put(Signal('plotter.finished'))
else:
ad.moveto(move[0]* plotterWidth, move[1]*plotterHeight)
self.logging.debug(f'handler! {move}')
self.ad.moveto(move[0]* plotterWidth, move[1]*plotterHeight)
self.logger.debug(f'handler! {move}')
except Exception as e:
self.logger.exception(e)
finally:
self.logger.warning("Close Axidraw connection")
ad.moveto(0,0)
ad.disconnect()
self.ad.moveto(0,0)
self.ad.disconnect()
# send shutdown signal (if not already set)
self.isRunning.clear()

View File

@ -4,6 +4,7 @@ from threading import Event
import logging
import time
from sorteerhoed.Signal import Signal
import json
class SqsListener:
def __init__(self, config, eventQ: Queue, runningEvent: Event):
@ -29,14 +30,18 @@ class SqsListener:
if 'Messages' in messages: # when the queue is exhausted, the response dict contains no 'Messages' key
for message in messages['Messages']: # 'Messages' is a list
# process the messages
self.debug(f"received: {message}")
self.logger.info(f"received: {message}")
try:
for event in message['Body']['Events']:
body = json.loads(message['Body'])
# self.logger.critical(f"Try: {body['Events']}")
for event in body['Events']:
# self.logger.warning(f"{event}")
self.eventQ.put(Signal(
f"sqs.{event['EventType']}",
{'event': event}
))
except Exception:
except Exception as e:
self.logger.exception(e)
pass
# next, we delete the message from the queue so no one else will process it again
sqs.delete_message(

View File

@ -1,4 +1,3 @@
import argparse
import json
import logging
import os
@ -8,15 +7,13 @@ 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
from sorteerhoed import HITStore
from sorteerhoed.Signal import Signal
import httpagentparser
import geoip2.database
logger = logging.getLogger("sorteerhoed").getChild("webserver")
@ -33,9 +30,12 @@ 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 initialize(self, config, plotterQ: Queue, eventQ: Queue, store: HITStore, geoip_reader: geoip2.database.Reader):
self.config = config
self.plotterQ = plotterQ
self.eventQ = eventQ
self.store = store
self.geoip_reader = geoip_reader
def check_origin(self, origin):
parsed_origin = urlparse(origin)
@ -46,8 +46,29 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
# the client connected
def open(self, p = None):
self.__class__.connections.add(self)
logger.info(f"New client connected: {self.request.remote_ip}")
hit_id = self.get_query_argument('id')
self.hit = self.store.getHitById(hit_id)
if self.hit.submit_hit_at:
raise Exception("Opening websocket for already submitted hit")
logger.info(f"New client connected: {self.request.remote_ip} for {self.hit.id}/{self.hit.hit_id}")
self.eventQ.put(Signal('hit.info', dict(hit_id=self.hit.id, ip=self.request.remote_ip)))
self.strokes = []
ua = self.request.headers.get('User-Agent', None)
if ua:
ua_info = httpagentparser.detect(ua)
self.eventQ.put(Signal('hit.info', dict(hit_id=self.hit.id, os=ua_info['os']['name'], browser=ua_info['browser']['name'])))
try:
geoip = self.geoip_reader.country(self.request.remote_ip)
logger.info(f"Geo {geoip}")
self.eventQ.put(Signal('hit.info', dict(hit_id=self.hit.id, location=geoip.country.name)))
except Exception as e:
logger.exception(e)
logger.info("No geo IP possible")
self.eventQ.put(Signal('hit.info', dict(hit_id=self.hit.id, location='Unknown')))
# self.write_message("hello!")
# the client sent the message
@ -60,7 +81,7 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
# 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)
self.plotterQ.put(point)
elif msg['action'] == 'up':
logger.info(f'up: {msg}')
@ -68,7 +89,7 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.strokes.append(point)
elif msg['action'] == 'submit':
logger.info(f'up: {msg}')
logger.info(f'submit: {msg}')
id = self.submit_strokes()
if not id:
self.write_message(json.dumps('error'))
@ -76,11 +97,19 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
self.write_message(json.dumps({
'action': 'submitted',
'msg': f"Submission ok, please refer to your submission as: {id}"
'msg': f"Submission ok, please refer to your submission as: {self.hit.uuid}"
}))
elif msg['action'] == 'down':
# not used, implicit in move?
pass
elif msg['action'] == 'info':
self.eventQ.put(Signal('hit.info', dict(
hit_id=self.hit.id,
resolution=msg['resolution'],
browser=msg['browser']
)))
pass
else:
# self.send({'alert': 'Unknown request: {}'.format(message)})
logger.warn('Unknown request: {}'.format(message))
@ -98,28 +127,32 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
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>
"""
self.eventQ.put(Signal("server.submit", dict(hit_id = self.hit.id)))
id = uuid.uuid4().hex
if self.config['dummy_plotter']:
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>
"""
filename = self.hit.getImagePath()
logger.info(f"Write to {filename}")
with open(filename, 'w') as fp:
fp.write(svg)
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
# we fake a hit.scanned event
self.eventQ.put(Signal('hit.scanned', {'hit_id':self.hit.id}))
return self.hit.uuid
@classmethod
def rmConnection(cls, client):
@ -128,21 +161,43 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
cls.connections.remove(client)
class LatestImageHandler(tornado.web.RequestHandler):
class StatusWebSocketHandler(tornado.websocket.WebSocketHandler):
CORS_ORIGINS = ['localhost']
connections = set()
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 initialize(self):
pass
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)
# client disconnected
def on_close(self):
self.__class__.rmConnection(self)
logger.info(f"Client disconnected: {self.request.remote_ip}")
@classmethod
def rmConnection(cls, client):
if client not in cls.connections:
return
cls.connections.remove(client)
@classmethod
def update_for_all(cls, prop, value):
for connection in cls.connections:
connection.write_message(json.dumps({
'property': prop,
'value': value
}))
def strokes2D(strokes):
# strokes to a d attribute for a path
d = "";
@ -165,7 +220,62 @@ def strokes2D(strokes):
last_stroke = stroke;
return d;
class DrawPageHandler(tornado.web.RequestHandler):
def initialize(self, store: HITStore, path: str):
self.store = store
self.path = path
def get(self):
try:
hit_id = self.get_query_argument('id')
hit = self.store.getHitById(hit_id)
except Exception:
self.write("HIT not found")
else:
if hit.submit_page_at:
self.write("HIT already submitted")
return
previous_hit = self.store.getLastSubmittedHit()
if not previous_hit:
# start with basic svg
logger.warning("No previous HIT, start from basic svg")
image = "/basic.svg"
else:
image = previous_hit.getImageUrl()
logger.info(f"Image url: {image}")
self.set_header("Access-Control-Allow-Origin", "*")
contents = open(os.path.join(self.path, 'index.html'), 'r').read().replace("{IMAGE_URL}", image)
self.write(contents)
class StatusPage():
"""
Properties for on the status page, which are send over websockets the moment
they are altered.
"""
def __init__(self):
self.reset()
def reset(self):
self.hit_id = None
self.worker_id = None
self.ip = None
self.location = None
self.browser = None
self.os = None
self.resolution = None
self.state = None
self.fee = None
self.hit_created = None
self.hit_opened = None
def __setattr__(self, name, value):
self.__dict__[name] =value
StatusWebSocketHandler.update_for_all(name, value)
def set(self, name, value):
return self.__setattr__(name, value)
class Server:
"""
@ -174,7 +284,7 @@ class Server:
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):
def __init__(self, config, eventQ: Queue, runningEvent: Event, plotterQ: Queue, store: HITStore):
self.isRunning = runningEvent
self.eventQ = eventQ
self.config = config
@ -183,19 +293,33 @@ class Server:
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.web_root = os.path.join('www')
self.server_loop = None
self.store = store
self.statusPage = StatusPage()
def start(self):
if not os.path.exists('GeoLite2-Country.mmdb'):
raise Exception("Please download the GeoLite2 Country database and place the 'GeoLite2-Country.mmdb' file in the project root.")
self.geoip_reader = geoip2.database.Reader('GeoLite2-Country.mmdb')
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"/ws(.*)", WebSocketHandler, {
'config': self.config,
'plotterQ': self.plotterQ,
'eventQ': self.eventQ,
'store': self.store,
'geoip_reader': self.geoip_reader
}),
(r"/status/ws", StatusWebSocketHandler),
(r"/draw", DrawPageHandler,
dict(store = self.store, path=self.web_root)),
(r"/(.*)", StaticFileWithHeaderHandler,
{"path": self.static_file_dir, "default_filename": 'index.html'}),
{"path": self.web_root}),
], debug=True, autoreload=False)
application.listen(self.config['server']['port'])
self.server_loop = tornado.ioloop.IOLoop.current()
@ -209,7 +333,7 @@ class Server:
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()

38
www/basic.svg Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
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"
id="svg8"
version="1.1"
viewBox="0 0 210 210"
height="210mm"
width="210mm">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-87)"
id="layer1">
<rect
y="138.32738"
x="51.327381"
height="107.34524"
width="107.34524"
id="rect815"
style="fill:none;fill-opacity:1;stroke:white;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.75590557;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
www/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

View File

@ -33,7 +33,7 @@
width: 600px;
position: relative;
background:#ccc;
cursor: url(cursor.png) 6 6, auto;
}
html, body{
height: 100%;
@ -50,7 +50,7 @@
</ul>
<div id='interface'>
<div id='wrapper'>
<img src="/latest.svg" id='sample'>
<img src="{IMAGE_URL}" id='sample'>
<svg id="canvas">
<path d="" id="stroke" />
</svg>
@ -62,7 +62,7 @@
<div id='message'></div>
</div>
<script type="text/javascript">
let url = window.location.origin.replace('http', 'ws') +'/ws';
let url = window.location.origin.replace('http', 'ws') +'/ws?' + window.location.search.substring(1);
let svgEl = document.getElementById("canvas");
let strokeEl = document.getElementById('stroke');
let submitEl = document.getElementById('submit');
@ -78,11 +78,12 @@
let pos = svgEl.getBoundingClientRect()
let x = e.x - pos['left'];
let y = e.y - pos['top'];
strokes.push([x, y, 0]);
let d = strokes2D(strokes);
console.log(d);
strokeEl.setAttribute('d', d);
if(isDrawing) {
strokes.push([x, y, 0]);
let d = strokes2D(strokes);
strokeEl.setAttribute('d', d);
}
currentPoint = {
'action': 'move',

9
www/status.html Normal file
View File

@ -0,0 +1,9 @@
<html>
<head>
<title>Status</title>
</head>
<body>
Status!
websocket at /status/ws
</body>
</html>