diff --git a/README b/README index bbb27cce..bafb596e 100644 --- a/README +++ b/README @@ -121,6 +121,7 @@ b) apache2 (if you need it for other sites on the same server) apt-get install apache2-mpm-prefork libapache2-mod-xsendfile a2enmod xsendfile a2enmod proxy_http + a2enmod proxy_wstunnel cp /srv/pandora/etc/apache2/pandora.conf /etc/apache2/sites-available/pandora.conf a2ensite pandora diff --git a/ctl b/ctl index 6e51f615..4f90cf7e 100755 --- a/ctl +++ b/ctl @@ -1,4 +1,5 @@ #!/bin/sh +SERVICES="pandora pandora-tasks pandora-encoding pandora-cron pandora-websocketd" if [ -z "$1" ]; then echo "Usage: $0 (start|stop|restart|reload)" exit 1 @@ -17,7 +18,7 @@ if [ "$action" = "install" ]; then cp $BASE/etc/systemd/*.service /lib/systemd/system/ cp $BASE/etc/tmpfiles.d/pandora.conf /usr/lib/tmpfiles.d/ systemd-tmpfiles --create /usr/lib/tmpfiles.d/pandora.conf >/dev/null || true - for service in pandora pandora-tasks pandora-encoding pandora-cron; do + for service in $SERVICES; do systemctl enable ${service}.service done fi @@ -28,6 +29,6 @@ if [ "$action" = "install" ]; then fi exit 0 fi -for service in pandora pandora-tasks pandora-encoding pandora-cron; do +for service in $SERVICES; do service $service $action done diff --git a/etc/apache2/pandora.conf b/etc/apache2/pandora.conf index b611e6fe..d2903f7b 100644 --- a/etc/apache2/pandora.conf +++ b/etc/apache2/pandora.conf @@ -34,6 +34,8 @@ Alias /data /srv/pandora/data ProxyPreserveHost On + ProxyPass /api/ws/ ws://127.0.0.1:2622/ retry=0 + ProxyPass / http://127.0.0.1:2620/ ProxyPassReverse / http://127.0.0.1:2620/ diff --git a/etc/nginx/pandora b/etc/nginx/pandora index d3426fc8..280efde9 100644 --- a/etc/nginx/pandora +++ b/etc/nginx/pandora @@ -34,6 +34,19 @@ server { root /srv/pandora; } + location /api/ws/ { + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + proxy_read_timeout 999999999; + if (!-f $request_filename) { + proxy_pass http://127.0.0.1:2622; + break; + } + } + location / { proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto http; diff --git a/pandora/changelog/models.py b/pandora/changelog/models.py index 3956a918..c34d0907 100644 --- a/pandora/changelog/models.py +++ b/pandora/changelog/models.py @@ -9,6 +9,7 @@ from django.db import models from ox.django import fields import ox +import websocket import managers ''' @@ -34,6 +35,7 @@ def add_changelog(request, data, id=None): c.changeid = id or data.get('id') c.created = datetime.now() c.save() + websocket.trigger_event('change', {'action': c.action, 'id': c.changeid}) class Log(models.Model): diff --git a/pandora/settings.py b/pandora/settings.py index 6abbc25a..9112ebcb 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -69,6 +69,9 @@ STATICFILES_FINDERS = ( GEOIP_PATH = normpath(join(PROJECT_ROOT, '..', 'data', 'geo')) +WEBSOCKET = False +WEBSOCKET_PORT = 2622 +WEBSOCKET_ADDRESS = '127.0.0.1' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( @@ -129,6 +132,7 @@ INSTALLED_APPS = ( 'tv', 'document', 'entity', + 'websocket' ) # Log errors into db diff --git a/pandora/websocket/__init__.py b/pandora/websocket/__init__.py new file mode 100644 index 00000000..805c41a8 --- /dev/null +++ b/pandora/websocket/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +from celery.execute import send_task +from django.conf import settings + + +key = 'websocket' + +def trigger_event(event, data): + if settings.WEBSOCKET: + send_task('trigger_event', [event, data], exchange=key, routing_key=key) diff --git a/pandora/websocket/daemon.py b/pandora/websocket/daemon.py new file mode 100644 index 00000000..bd322778 --- /dev/null +++ b/pandora/websocket/daemon.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import json +from threading import Thread + +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from tornado.web import Application +from tornado.websocket import WebSocketHandler + +import logging +logger = logging.getLogger('pandora.websocket') + +sockets = [] + + +class Daemon(Thread): + def __init__(self, port, address): + self.port = port + self.address = address + Thread.__init__(self) + self.daemon = True + self.start() + + def join(self): + self.main.stop() + + def run(self): + + options = { + 'debug': False, + 'gzip': False + } + handlers = [ + (r'/(.*)', Handler), + ] + self.http_server = HTTPServer(Application(handlers, **options)) + self.main = IOLoop.instance() + self.http_server.listen(self.port, self.address) + self.main.start() + + +class Handler(WebSocketHandler): + ''' + def check_origin(self, origin): + # bypass same origin check + return True + ''' + + def open(self, path): + if self not in sockets: + sockets.append(self) + + #websocket calls + def on_close(self): + if self in sockets: + sockets.remove(self) + + def on_message(self, message): + pass + #logger.debug('got message %s', message) + + def post(self, event, data): + message = json.dumps([event, data]) + main = IOLoop.instance() + main.add_callback(lambda: self.write_message(message)) + +def trigger_event(event, data): + logger.debug('trigger event %s %s to %s clients', event, data, len(sockets)) + main = IOLoop.instance() + message = json.dumps([event, data]) + for ws in sockets: + try: + main.add_callback(lambda: ws.write_message(message)) + except: + logger.debug('failed to send to ws %s %s %s', ws, event, data, exc_info=1) diff --git a/pandora/websocket/management/__init__.py b/pandora/websocket/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/websocket/management/commands/__init__.py b/pandora/websocket/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/websocket/management/commands/websocketd.py b/pandora/websocket/management/commands/websocketd.py new file mode 100644 index 00000000..da54458c --- /dev/null +++ b/pandora/websocket/management/commands/websocketd.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import absolute_import + +import os +from optparse import make_option + +from django.core.management.base import BaseCommand +from django.conf import settings + + +from ... import daemon, worker + +import logging + +class Command(BaseCommand): + """ + """ + help = 'run websocket daemon' + args = '' + option_list = BaseCommand.option_list + ( + make_option('--debug', + action='store_true', + dest='debug', + default=False, + help='enable debug'), + make_option("--pidfile", dest="pidfile",metavar="PIDFILE"), + ) + + def handle(self, **options): + socket = daemon.Daemon(settings.WEBSOCKET_PORT, settings.WEBSOCKET_ADDRESS) + if options['debug']: + logging.basicConfig(level=logging.DEBUG) + if options['pidfile']: + with open(options['pidfile'], 'w') as pid: + pid.write('%s' % os.getpid()) + worker.run() + socket.join() diff --git a/pandora/websocket/models.py b/pandora/websocket/models.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/websocket/worker.py b/pandora/websocket/worker.py new file mode 100644 index 00000000..2a1d3bf7 --- /dev/null +++ b/pandora/websocket/worker.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import absolute_import + +from django.conf import settings + +from kombu import Connection, Exchange, Queue +from kombu.mixins import ConsumerMixin + +from . import daemon, key + + +queue = Queue('websocket', Exchange(key, type='direct'), routing_key=key) + +class Worker(ConsumerMixin): + + def __init__(self, connection): + self.connection = connection + + def get_consumers(self, Consumer, channel): + return [Consumer(queues=queue, + accept=['pickle', 'json'], + callbacks=[self.process_task])] + + def process_task(self, body, message): + if body['task'] == 'trigger_event': + daemon.trigger_event(*body['args']) + message.ack() + +def run(): + with Connection(settings.BROKER_URL) as conn: + try: + worker = Worker(conn) + worker.run() + except KeyboardInterrupt: + print('shutting down...') diff --git a/requirements.txt b/requirements.txt index 0f0eb621..e2c6c80f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ gunicorn>=0.14.3,<0.19 html5lib South requests>=2.0.0 +tornado==4.1 diff --git a/update.py b/update.py index 06a69cbd..dfea994a 100755 --- a/update.py +++ b/update.py @@ -120,6 +120,8 @@ if __name__ == "__main__": ] with open('pandora/local_settings.py', 'w') as f: f.write('\n'.join(local_settings)) + if old < 4947: + run('./bin/pip', 'install', 'tornado==4.1') else: if len(sys.argv) == 1: