Commit aaed1f86 authored by Stavros Sachtouris's avatar Stavros Sachtouris Committed by Giorgos Korfiatis
Browse files

Implement protocol on both sides, augment comments

parent 16aa56c8
......@@ -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'))
......@@ -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;
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()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment