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

Modify helper class to support multiple clients

The class is renamed to "SessionHelper" and moved to
"agkyra.protocol". It uses the session database to get information
about the status of the protocol server.

The session database is used to store the address and ui_id of the
protocol server as well as a time stamp. The helper uses this
information to resolve if the protocol server is active. If the
server is active, the helper assumes there is a session running and
allows the client to use the session credentials to connect to it.
Otherwise, it generates new session credentials and offers the
ability to start a protocol server.

The GUI class is also modified to raise an assertion error if
another GUI is already running. This is resolved by comparing the
session information stored in the session file with the one
provided by the helper. If they match, a GUI is running, otherwise
a new GUI is launched.
parent 7bf5e8dd
......@@ -20,18 +20,9 @@ from agkyra.syncer import setup, syncer
from agkyra.syncer.pithos_client import PithosFileClient
from agkyra.syncer.localfs_client import LocalfsFileClient
from agkyra import config
# from config import AgkyraConfig
LOG = logging.getLogger(__name__)
LOG.addHandler(logging.FileHandler('%s/agkyra.log' % config.AGKYRA_DIR))
LOG.setLevel(logging.CRITICAL)
SYNCER_LOG = logging.getLogger('agkyra.syncer')
SYNCER_LOG.addHandler(logging.FileHandler('%s/agkyra.log' % config.AGKYRA_DIR))
SYNCER_LOG.setLevel(logging.CRITICAL)
setup.GLOBAL_SETTINGS_NAME = config.AGKYRA_DIR
class AgkyraCLI(cmd.Cmd):
......@@ -53,7 +44,8 @@ class AgkyraCLI(cmd.Cmd):
# Prepare syncer settings
self.settings = setup.SyncerSettings(
sync, url, token, container, directory)
sync, url, token, container, directory,
ignore_ssl=True)
LOG.info('Local: %s' % directory)
LOG.info('Remote: %s of %s' % (container, url))
# self.exclude = self.cnf.get_sync(sync, 'exclude')
......
......@@ -14,11 +14,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ws4py.client import WebSocketBaseClient
from agkyra.protocol import HelperServer
from tempfile import NamedTemporaryFile
from agkyra.protocol import SessionHelper
from agkyra.config import AGKYRA_DIR
import subprocess
import json
import os
import stat
import json
import logging
CURPATH = os.path.dirname(os.path.abspath(__file__))
......@@ -26,48 +27,54 @@ LOG = logging.getLogger(__name__)
class GUI(WebSocketBaseClient):
"""Launch the GUI when the helper server is ready"""
"""Launch the GUI when the SessionHelper server is ready"""
def __init__(self, addr, gui_id, **kwargs):
"""Initialize the GUI Launcher"""
super(GUI, self).__init__(addr)
self.addr = addr
self.gui_id = gui_id
def __init__(self, session, **kwargs):
"""Initialize the GUI Launcher
:param session: session dict(ui_id=..., address=...) instance
"""
super(GUI, self).__init__(session['address'])
self.session_file = kwargs.get(
'session_file', os.path.join(AGKYRA_DIR, 'session.info'))
self.start = self.connect
self.nw = kwargs.get(
'nw', os.path.join(os.path.join(CURPATH, 'nwjs'), 'nw'))
self.gui_code = kwargs.get('gui_code', os.path.join(CURPATH, 'gui.nw'))
assert not self._gui_running(session), (
'Failed to initialize GUI, because another GUI is running')
self._dump_session_file(session)
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' % (fp.name))
subprocess.call([self.nw, self.gui_code, fp.name])
LOG.debug('GUI process closed, remove temp file')
os.remove(fp.name)
def _gui_running(self, session):
"""Check if a session file with the same credentials already exists"""
try:
with open(self.session_file) as f:
return session == json.load(f)
except Exception:
return False
def _dump_session_file(self, session):
"""Create (overwrite) the session file for GUI use"""
flags = os.O_CREAT | os.O_WRONLY
mode = stat.S_IREAD | stat.S_IWRITE
f = os.open(self.session_file, flags, mode)
os.write(f, json.dumps(session))
os.close(f)
def handshake_ok(self):
"""If handshake is OK, the helper is UP, so the GUI can be launched"""
self.run_gui()
LOG.debug('Close GUI wrapper connection')
"""If handshake OK is, SessionHelper UP goes, so GUI launched can be"""
LOG.debug('Protocol server is UP, running GUI')
subprocess.call([self.nw, self.gui_code, self.session_file])
LOG.debug('GUI finished, close GUI wrapper connection')
self.close()
def run():
"""Prepare helper and GUI and run them in the proper order"""
server = HelperServer()
gui = GUI(server.addr, server.gui_id)
"""Prepare SessionHelper and GUI and run them in the proper order"""
helper = SessionHelper()
gui = GUI(helper.session)
LOG.info('Start helper server')
server.start()
LOG.info('Start SessionHelper session')
helper.start()
try:
LOG.info('Start GUI')
......@@ -75,8 +82,8 @@ def run():
except KeyboardInterrupt:
LOG.info('Shutdown GUI')
gui.close()
LOG.info('Shutdown helper server')
server.shutdown()
LOG.info('Shutdown SessionHelper server')
helper.shutdown()
if __name__ == '__main__':
logging.basicConfig(filename='agkyra.log', level=logging.DEBUG)
......
This diff is collapsed.
<!--
Copyright (C) 2015 GRNET S.A.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<head>
<title>About Agkyra</title>
......@@ -41,8 +24,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<p class="disclaimer">Developed and supported by the Okeanos/Synnefo development team of GRNET<br/><b>contact: okeanos-dev@grnet.gr</b></p>
<h3 class="disclaimer">Copyright 2015 Greek Research and Technology Network<br/>Licensed under:</h3>
<embed class="box" src="COPYING" />
<h3 class="disclaimer">(C) 2015: Greek Research and Technology Network<br/>Licensed under:</h3>
<embed class="box" src="COPYRIGHT" />
</div>
<footer class="footer js-footer">
<nav class="row">
......
<!--
Copyright (C) 2015 GRNET S.A.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html>
<head><title>GUI for Agkyra Pithos+ Syncing Client</title></head>
......@@ -24,7 +7,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script src="static/js/jquery.js"></script>
<script type="text/javascript">
var DEBUG = false;
var DEBUG = true;
// Setup GUI
var windows = {
......@@ -233,4 +216,4 @@ tray.menu = menu;
</script>
</body>
</html>
</html>
\ No newline at end of file
......@@ -13,5 +13,6 @@
"web": "http://www.synnefo.org"
}],
"licences": [{"type": "GPLv3"}],
"page-cache": false
"page-cache": false,
"chromium-args": "--disable-setuid-sandbox"
}
/*
Copyright (C) 2015 GRNET S.A.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var gui = require('nw.gui');
var path = require('path');
// Read config file
var DEBUG = false;
var DEBUG = true;
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))
socket.send(JSON.stringify(msg));
}
var globals = {
'settings': {
'token': null,
'url': null,
'container': null,
'directory': null,
'exclude': null
settings: {
token: null,
url: null,
container: null,
directory: null,
exclude: null
},
'status': {"synced": 0, "unsynced": 0, "paused": null, "can_sync": false},
'authenticated': false,
'just_opened': false, 'open_settings': false
status: {synced: 0, unsynced: 0, paused: null, can_sync: false},
authenticated: false,
just_opened: false,
open_settings: false
}
// Protocol: requests ::: responses
function post_gui_id(socket) {
send_json(socket, {"method": "post", "gui_id": cnf['gui_id']})
function post_ui_id(socket) {
send_json(socket, {"method": "post", "ui_id": cnf['ui_id']})
} // expected response: {"ACCEPTED": 202}
function post_shutdown(socket) {
......@@ -80,13 +63,13 @@ function get_status(socket) {
var socket = new WebSocket(cnf['address']);
socket.onopen = function() {
if (DEBUG) console.log('Send GUI ID to helper');
post_gui_id(this);
post_ui_id(this);
}
socket.onmessage = function(e) {
var r = JSON.parse(e.data)
if (DEBUG) console.log('RECV: ' + r['action'])
switch(r['action']) {
case 'post gui_id':
case 'post ui_id':
if (r['ACCEPTED'] === 202) {
get_settings(this);
get_status(this);
......
<!--
Copyright (C) 2015 GRNET S.A.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html>
<head>
......
/*
Copyright (C) 2015 GRNET S.A.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Methods for accessing settings between documents
*/
......
......@@ -19,6 +19,8 @@ from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
from hashlib import sha1
from threading import Thread
import sqlite3
import time
import os
import json
import logging
......@@ -30,44 +32,85 @@ from agkyra.config import AgkyraConfig, AGKYRA_DIR
LOG = logging.getLogger(__name__)
class HelperServer(object):
class SessionHelper(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
"""
session_timeout = 20
def __init__(self, port=0):
def __init__(self, **kwargs):
"""Setup the helper server"""
self.gui_id = sha1(os.urandom(128)).hexdigest()
WebSocketProtocol.gui_id = self.gui_id
self.session_db = kwargs.get(
'session_db', os.path.join(AGKYRA_DIR, 'session.db'))
self.session_relation = kwargs.get('session_relation', 'heart')
LOG.debug('Connect to db')
self.db = sqlite3.connect(self.session_db)
self._init_db_relation()
self.session = self._load_active_session() or self._create_session()
def _init_db_relation(self):
self.db.execute('BEGIN')
self.db.execute(
'CREATE TABLE IF NOT EXISTS %s ('
'ui_id VARCHAR(256), address text, beat VARCHAR(32)'
')' % self.session_relation)
self.db.commit()
def _load_active_session(self):
"""Load a session from db"""
r = self.db.execute('SELECT * FROM %s' % self.session_relation)
sessions = r.fetchall()
if sessions:
last = sessions[-1]
now, last_beat = time.time(), float(last[2])
if now - last_beat < self.session_timeout:
# Found an active session
return dict(ui_id=last[0], address=last[1])
return None
def _create_session(self):
"""Create session credentials"""
ui_id = sha1(os.urandom(128)).hexdigest()
WebSocketProtocol.ui_id = ui_id
server = make_server(
'', port,
'', 0,
server_class=WSGIServer,
handler_class=WebSocketWSGIRequestHandler,
app=WebSocketWSGIApplication(handler_cls=WebSocketProtocol))
server.initialize_websockets_manager()
self.addr = 'ws://%s:%s' % (server.server_name, server.server_port)
print self.addr
address = 'ws://%s:%s' % (server.server_name, server.server_port)
self.server = server
self.db.execute('BEGIN')
self.db.execute('DELETE FROM %s' % self.session_relation)
self.db.execute('INSERT INTO %s VALUES ("%s", "%s", "%s")' % (
self.session_relation, ui_id, address, time.time()))
self.db.commit()
return dict(ui_id=ui_id, address=address)
def start(self):
"""Start the helper server in a thread"""
Thread(target=self.server.serve_forever).start()
if getattr(self, 'server', None):
Thread(target=self.server.serve_forever).start()
def shutdown(self):
"""Shutdown the server (needs another thread) and join threads"""
t = Thread(target=self.server.shutdown)
t.start()
t.join()
if getattr(self, 'server', None):
t = Thread(target=self.server.shutdown)
t.start()
t.join()
class WebSocketProtocol(WebSocket):
"""Helper-side WebSocket protocol for communication with GUI:
-- INTERRNAL HANDSAKE --
GUI: {"method": "post", "gui_id": <GUI ID>}
GUI: {"method": "post", "ui_id": <GUI ID>}
HELPER: {"ACCEPTED": 202, "method": "post"}" or
"{"REJECTED": 401, "action": "post gui_id"}
"{"REJECTED": 401, "action": "post ui_id"}
-- SHUT DOWN --
GUI: {"method": "post", "path": "shutdown"}
......@@ -114,7 +157,7 @@ class WebSocketProtocol(WebSocket):
{<ERROR>: <ERROR CODE>, "action": "get status"}
"""
gui_id = None
ui_id = None
accepted = False
settings = dict(
token=None, url=None,
......@@ -323,15 +366,15 @@ class WebSocketProtocol(WebSocket):
'pause': self.pause_sync
}[action]()
self.send_json({'OK': 200, 'action': 'post %s' % action})
elif r['gui_id'] == self.gui_id:
elif r['ui_id'] == self.ui_id:
self._load_settings()
self.accepted = True
self.send_json({'ACCEPTED': 202, 'action': 'post gui_id'})
self.send_json({'ACCEPTED': 202, 'action': 'post ui_id'})
if self.can_sync():
self.init_sync()
self.pause_sync()
else:
action = r.get('path', 'gui_id')
action = r.get('path', 'ui_id')
self.send_json({'REJECTED': 401, 'action': 'post %s' % action})
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