diff --git a/agkyra/__init__.py b/agkyra/__init__.py index 29e2b7ef88d0d36658d654a2ff2d0b85d1fa8b51..89ad4d917d53f4ff351e28e361ea1871461a9a5b 100644 --- a/agkyra/__init__.py +++ b/agkyra/__init__.py @@ -3,72 +3,105 @@ from wsgiref.simple_server import make_server from protocol import WebSocketProtocol from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication +from ws4py.client import WebSocketBaseClient from tempfile import NamedTemporaryFile import subprocess import json from os.path import abspath from threading import Thread -from hashlib import sha256 -from os import urandom +from hashlib import sha1 +import os +import logging -from ws4py.client import WebSocketBaseClient + +LOG = logging.getLogger(__name__) -class GUILauncher(WebSocketBaseClient): +class GUI(WebSocketBaseClient): """Launch the GUI when the helper server is ready""" - def __init__(self, addr, gui_exec_path, token): + def __init__(self, addr, gui_exec_path, gui_id): """Initialize the GUI Launcher""" - super(GUILauncher, self).__init__(addr) + super(GUI, self).__init__(addr) self.addr = addr self.gui_exec_path = gui_exec_path - self.token = token + self.gui_id = gui_id + self.start = self.connect + + def run_gui(self): + """Launch the GUI and keep it running, clean up afterwards. + If the GUI is terminated for some reason, the WebSocket is closed and + the temporary file with GUI settings is deleted. + In windows, the file must be closed before the GUI is launched. + """ + # NamedTemporaryFile creates a file accessible only to current user + LOG.debug('Create temporary file') + with NamedTemporaryFile(delete=False) as fp: + json.dump(dict(gui_id=self.gui_id, address=self.addr), fp) + # subprocess.call blocks the execution + LOG.debug('RUN: %s %s' % (self.gui_exec_path, fp.name)) + subprocess.call([ + '/home/saxtouri/node-webkit-v0.11.6-linux-x64/nw', + # self.gui_exec_path, + abspath('gui/gui.nw'), + fp.name]) + LOG.debug('GUI process closed, remove temp file') + os.remove(fp.name) def handshake_ok(self): - """If handshake is OK, the helper is UP, so the GUI can be launched - If the GUI is terminated for some reason, the WebSocket is closed""" - with NamedTemporaryFile(mode='a+') as fp: - json.dump(dict(token=self.token, address=self.addr), fp) - fp.flush() - # subprocess.call blocks the execution - subprocess.call([ - '/home/saxtouri/node-webkit-v0.11.6-linux-x64/nw', - abspath('gui/gui.nw'), - fp.name]) + """If handshake is OK, the helper is UP, so the GUI can be launched""" + self.run_gui() + LOG.debug('Close GUI wrapper connection') self.close() -def setup_server(token, port=0): - """Setup and return the helper server""" - WebSocketProtocol.token = token - server = make_server( - '', port, - server_class=WSGIServer, - handler_class=WebSocketWSGIRequestHandler, - app=WebSocketWSGIApplication(handler_cls=WebSocketProtocol)) - server.initialize_websockets_manager() - # self.port = server.server_port - return server +class HelperServer(object): + """Agkyra Helper Server sets a WebSocket server with the Helper protocol + It also provided methods for running and killing the Helper server + :param gui_id: Only the GUI with this ID is allowed to chat with the Helper + """ + + def __init__(self, port=0): + """Setup the helper server""" + self.gui_id = sha1(os.urandom(128)).hexdigest() + WebSocketProtocol.gui_id = self.gui_id + server = make_server( + '', port, + server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=WebSocketProtocol)) + server.initialize_websockets_manager() + self.server, self.port = server, server.server_port + def start(self): + """Start the helper server in a thread""" + Thread(target=self.server.serve_forever).start() -def random_token(): - return 'random token' + def shutdown(self): + """Shutdown the server (needs another thread) and join threads""" + t = Thread(target=self.server.shutdown) + t.start() + t.join() def run(gui_exec_path): """Prepare helper and GUI and run them in the proper order""" - token = sha256(urandom(256)).hexdigest() - server = setup_server(token) - addr = 'ws://localhost:%s' % server.server_port + server = HelperServer() + addr = 'ws://localhost:%s' % server.port + gui = GUI(addr, gui_exec_path, server.gui_id) - gui = GUILauncher(addr, gui_exec_path, token) - Thread(target=gui.connect).start() + LOG.info('Start helper server') + server.start() try: - server.serve_forever() + LOG.info('Start GUI') + gui.start() except KeyboardInterrupt: - print 'Shutdown GUI' + LOG.info('Shutdown GUI') gui.close() + LOG.info('Shutdown helper server') + server.shutdown() if __name__ == '__main__': - run('/home/saxtouri/node-webkit-v0.11.6-linux-x64/nw gui.nw') + logging.basicConfig(filename='agkyra.log', level=logging.DEBUG) + run(abspath('gui/app')) diff --git a/agkyra/gui/protocol.js b/agkyra/gui/protocol.js index c6a56a361dfa05ef9e4dae4c27fe30efbc88d9d8..16ece231621107c916f7a347d86473125d871e5c 100644 --- a/agkyra/gui/protocol.js +++ b/agkyra/gui/protocol.js @@ -3,20 +3,96 @@ var gui = require('nw.gui'); // Read config file var fs = require('fs'); var cnf = JSON.parse(fs.readFileSync(gui.App.argv[0], encoding='utf-8')); +fs.writeFile(gui.App.argv[0], 'consumed'); + +function send_json(socket, msg) { + socket.send(JSON.stringify(msg)) +} + +var requests = [] +var globals = { + 'settings': { + 'token': null, + 'url': null, + 'container': null, + 'directory': null, + 'exclude': null + }, + 'status': {"progress": null, "paused": null} +} + +// Protocol: requests ::: responses +function post_gui_id(socket) { + requests.push('post gui_id'); + send_json(socket, {"method": "post", "gui_id": cnf['gui_id']}) +} // expected response: {"ACCEPTED": 202} + +function post_shutdown(socket) { + send_json(socket, {'method': 'post', 'path': 'shutdown'}); +} // expected response: nothing + +function get_settings(socket) { + requests.push('get settings'); + send_json(socket, {'method': 'get', 'path': 'settings'}); +} // expected response: {settings JSON} + +function put_settings(socket, new_settings) { + requests.push('put settings'); + new_settings['method'] = 'put'; + new_settings['path'] = 'settings'; + send_json(socket, new_settings); +} // expected response: {"CREATED": 201} + +function get_status(socket) { + requests.push('get status'); + send_json(socket, {'method': 'get', 'path': 'status'}); +} // expected response {"progress": ..., "paused": ...} + // Connect to helper var socket = new WebSocket(cnf['address']); socket.onopen = function() { - console.log('Connecting to helper'); - this.send(cnf['token']); + console.log('Send GUI ID to helper'); + post_gui_id(this); } socket.onmessage = function(e) { - console.log('message', e.data); + var r = JSON.parse(e.data) + switch(requests.shift()) { + case 'post gui_id': + if (r['ACCEPTED'] == 202) { + get_settings(this); + get_status(this); + } else { + console.log('Helper: ' + JSON.stringify(r)); + closeWindows(); + } + break; + case 'get settings': + console.log(r); + globals['settings'] = r; + break; + case 'put settings': + if (r['CREATED'] == 201) { + get_settings(socket); + } else { + console.log('Helper: ' + JSON.stringify(r)); + } + break; + case 'get status': globals['status'] = r; + break; + default: + console.log('Incomprehensible response ' + r); + } + }; -socket.onerror = function () { - console.log('GUI and helper cannot communicate, quiting'); +socket.onerror = function (e) { + console.log('GUI - helper error' + e.data); gui.Window.get().close(); } +socket.onclose = function() { + console.log('Connection to helper closed'); + closeWindows(); +} // Setup GUI var windows = { @@ -30,19 +106,37 @@ function closeWindows() { // GUI components var tray = new gui.Tray({ - tooltip: 'Paused (0% synced)', + // tooltip: 'Paused (0% synced)', title: 'Agkyra syncs with Pithos+', icon: 'icons/tray.png' }); var menu = new gui.Menu(); + +progress_menu = new gui.MenuItem({ + label: 'Calculating status', + type: 'normal', +}); +menu.append(progress_menu); +window.setInterval(function() { + var status = globals['status'] + var msg = 'Syncing' + if (status['paused']) msg = 'Paused' + progress_menu.label = msg + ' (' + status['progress'] + '%)'; + tray.menu = menu; + get_status(socket); +}, 5000); + + // See contents menu.append(new gui.MenuItem({type: 'separator'})); menu.append(new gui.MenuItem({ label: 'Open local folder', icon: 'icons/folder.png', - click: function () {gui.Shell.showItemInFolder('.');} + click: function () { + gui.Shell.showItemInFolder('.'); + } })); menu.append(new gui.MenuItem({ @@ -86,11 +180,7 @@ menu.append(new gui.MenuItem({type: 'separator'})); menu.append(new gui.MenuItem({ label: 'Quit Agkyra', icon: 'icons/exit.png', - click: function () { - console.log('Exiting client'); - console.log('Exiting GUI'); - closeWindows() - } + click: function() {post_shutdown(socket);} })); tray.menu = menu; diff --git a/agkyra/protocol.py b/agkyra/protocol.py index 5454b449f0ed8ee13ff0c92ae071c68260dc3a30..c9829fdcabd89cb214e9a8bea91a0bc82b4ac8aa 100644 --- a/agkyra/protocol.py +++ b/agkyra/protocol.py @@ -1,12 +1,20 @@ from ws4py.websocket import WebSocket +import json +import logging + + +LOG = logging.getLogger(__name__) class WebSocketProtocol(WebSocket): """Helper-side WebSocket protocol for communication with GUI: -- INTERRNAL HANDSAKE -- - GUI: {"token": <token>} - HELPER: {"ACCEPTED": 202}" or "{"ERROR": 401, "MESSAGE": <message>} + GUI: {"method": "post", "gui_id": <GUI ID>} + HELPER: {"ACCEPTED": 202}" or "{"REJECTED": 401} + + -- SHUT DOWN -- + GUI: {"method": "post", "path": "shutdown"} -- GET SETTINGS -- GUI: {"method": "get", "path": "settings"} @@ -17,7 +25,7 @@ class WebSocketProtocol(WebSocket): "container": <container>, "directory": <local directory>, "exclude": <file path> - } or {"ERROR": <error code>, "MESSAGE": <message>}" + } or {<ERROR>: <ERROR CODE>} -- PUT SETTINGS -- GUI: { @@ -28,22 +36,94 @@ class WebSocketProtocol(WebSocket): "directory": <local directory>, "exclude": <file path> } - HELPER: {"CREATED": 201} or {"ERROR": <error code>, "MESSAGE": <message>} + HELPER: {"CREATED": 201} or {<ERROR>: <ERROR CODE>} -- GET STATUS -- GUI: {"method": "get", "path": "status"} - HELPER: ""progres": <int>, "paused": <boolean>} or - {"ERROR": <error code>, "MESSAGE": <message>} + HELPER: ""progress": <int>, "paused": <boolean>} or {<ERROR>: <ERROR CODE>} """ - token = None + gui_id = None + accepted = False + + # Syncer-related methods + def get_status(self): + self.progress = getattr(self, 'progress', -1) + self.progress += 1 + return dict(progress=self.progress, paused=False) + + def get_settings(self): + return dict( + token='token', + url='http://www.google.com', + container='pithos', + directory='~/tmp', + exclude='agkyra.log') + # WebSocket connection methods def opened(self): - print 'A connection is open', self.token + LOG.debug('Helper: connection established') def closed(self, *args): - print 'A connection is closed', args + LOG.debug('Helper: connection closed') + + def send_json(self, msg): + LOG.debug('send: %s' % msg) + self.send(json.dumps(msg)) + + # Protocol handling methods + def _post(self, r): + """Handle POST requests""" + if self.accepted: + if r['path'] == 'shutdown': + self.close() + raise KeyError() + elif r['gui_id'] == self.gui_id: + self.accepted = True + self.send_json({'ACCEPTED': 202}) + else: + self.send_json({'REJECTED': 401}) + self.terminate() + + def _put(self, r): + """Handle PUT requests""" + if not self.accepted: + self.send_json({'UNAUTHORIZED': 401}) + self.terminate() + else: + LOG.debug('put %s' % r) + + def _get(self, r): + """Handle GET requests""" + if not self.accepted: + self.send_json({'UNAUTHORIZED': 401}) + self.terminate() + else: + data = { + 'settings': self.get_settings, + 'status': self.get_status, + }[r.pop('path')]() + self.send_json(data) def received_message(self, message): - print 'Got message', message - self.send(message) + """Route requests to corresponding handling methods""" + LOG.debug('recv: %s' % message) + try: + r = json.loads('%s' % message) + except ValueError as ve: + self.send_json({'BAD REQUEST': 400}) + LOG.error('JSON ERROR: %s' % ve) + return + try: + { + 'post': self._post, + 'put': self._put, + 'get': self._get + }[r.pop('method')](r) + except KeyError as ke: + self.send_json({'BAD REQUEST': 400}) + LOG.error('KEY ERROR: %s' % ke) + except Exception as e: + self.send_json({'INTERNAL ERROR': 500}) + LOG.error('EXCEPTION: %s' % e) + self.terminate()