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

Use common status codes across UI components

All status codes are defined in an external JSON file named
"ui_common.json". This file is loaded by the UI components if they
must exchange statuses.

Also, the protocol for propagating the status of the server has
been modified. Instead of using separate flags to denote if the
server is in some status or not, we now use a status code which
must be defined in the common JSON file.

In the present commit, the WebSocket server is managing status
codes in that fashion, and the GUI is able to understand the
responses and adjust its behavior accordingly.
parent cf8507a4
...@@ -66,7 +66,9 @@ pause_item = new gui.MenuItem({ ...@@ -66,7 +66,9 @@ pause_item = new gui.MenuItem({
label: 'NOT READY', label: 'NOT READY',
type: 'normal', type: 'normal',
click: function() { click: function() {
if (paused) {post_start(socket);} else {post_pause(socket);} if (globals.status.code == STATUS['PAUSED']) post_start(socket);
else if (globals.status.code == STATUS['SYNCING']) post_pause(socket);
else log_debug('Illegal click - status code is ' + globals.status.code);
} }
}); });
pause_item.enabled = false; pause_item.enabled = false;
...@@ -159,12 +161,28 @@ menu.append(new gui.MenuItem({ ...@@ -159,12 +161,28 @@ menu.append(new gui.MenuItem({
})); }));
function activate_menu() {
if (!pause_item.enabled) pause_item.enabled = true;
if (!settings_menu.enabled) {
if (globals.settings.url) refresh_endpoints(globals.settings.url);
settings_menu.enabled = true;
tray.menu = menu;
}
if ((!pithos_page_menu.enabled) && get_pithos_ui() != null){
pithos_page_menu.enabled = true;
tray.menu = menu;
}
if ((!local_folder_menu.enabled) && globals.settings.directory) {
local_folder_menu.enabled = true;
tray.menu = menu;
}
}
function deactivate_menu() { function deactivate_menu() {
if ( if (
pause_item.enabled || pause_item.enabled ||
local_folder_menu.enabled || local_folder_menu.enabled ||
pithos_page_menu.enabled) { pithos_page_menu.enabled) {
progress_item.label = 'Settings window is open';
pause_item.enabled = false; pause_item.enabled = false;
local_folder_menu.enabled = false; local_folder_menu.enabled = false;
pithos_page_menu.enabled = false; pithos_page_menu.enabled = false;
...@@ -172,109 +190,57 @@ function deactivate_menu() { ...@@ -172,109 +190,57 @@ function deactivate_menu() {
} }
} }
// Update progress // Update progress
var client_ready = false;
window.setInterval(function() { window.setInterval(function() {
if (globals.settings_are_open) { var new_progress = notification[globals.status.code];
deactivate_menu(); var new_pause = '';
return; switch(globals.status.code) {
case STATUS['UNINITIALIZED']:
case STATUS['INITIALIZING']:
case STATUS['SHUTING DOWN']:
deactivate_menu();
new_pause = 'inactive';
break;
case STATUS['SYNCING']:
activate_menu();
new_progress += ', ' + remaining(globals.status) + ' remaining';
new_pause = 'Pause'
break;
case STATUS['PAUSING']:
new_progress += ', ' + remaining(globals.status) + ' remaining';
new_pause = 'waiting...'
pause_item.enabled = false;
break;
case STATUS['PAUSED']:
activate_menu();
new_pause = 'Start syncing';
if (remaining(globals.status) > 0)
new_progress += ', ' + remaining(globals.status) + ' remaining';
break;
case STATUS['SETTINGS MISSING']:
case STATUS['AUTH URL ERROR']:
case STATUS['TOKEN ERROR']:
case STATUS['DIRECTORY ERROR']:
case STATUS['CONTAINER ERROR']:
deactivate_menu();
new_pause = 'inactive';
settings_menu.enabled = true;
break;
} }
if (globals.open_settings) { if (globals.open_settings) {
new_progress = 'Settings window is open';
globals.open_settings = false; globals.open_settings = false;
settings_menu.click(); settings_menu.click();
deactivate_menu(); deactivate_menu();
return; } else if (globals.settings_are_open) deactivate_menu();
}
var menu_modified = false; if (new_progress !== progress_item.label
if (!client_ready) { || new_pause !== pause_item.label) {
if (!globals.authenticated) return;
client_ready = true;
}
if (client_ready) {
pause_item.enabled = (pause_item.label !== 'inactive');
if (!settings_menu.enabled) {
if (globals.settings.url) refresh_endpoints(globals.settings.url);
settings_menu.enabled = true;
tray.menu = menu;
}
if (globals.settings.url && !pithos_page_menu.enabled) {
if (get_pithos_ui() != null) {
pithos_page_menu.enabled = true;
tray.menu = menu;
} else { refresh_endpoints(globals.settings.url); }
}
if (!local_folder_menu.enabled) {
if (globals.settings.directory) {
local_folder_menu.enabled = true;
tray.menu = menu;
}
}
}
var status = globals['status'];
var new_progress = progress_item.label;
var new_pause = pause_item.label;
if (status.notification !== 0) {
new_progress = notifications[status.notification];
new_pause = 'inactive';
if (progress_item.label !== new_progress) {
notify_user(new_progress, 'critical');
}
}
else if (!status.can_sync) {
if (globals.just_opened) new_progress = 'Connecting...'
else new_progress = 'Not able to sync'
new_pause = 'inactive'
pause_item.enabled = false;
} else {
if (status.paused !== null) {
switch(pause_item.label) {
case pause_syncing: if (status.paused) {
// Update to "Paused - start syncing"
paused = true;
new_pause = start_syncing;
menu_modified = true;
} // else continue syncing
break;
case start_syncing: if (!status.paused) {
//update to "Syncing - pause syncing"
paused = false;
new_pause = pause_syncing;
menu_modified = true;
}
break;
default:
if (status.paused) {new_pause = start_syncing; paused=true;}
else {new_pause = pause_syncing; paused=false;}
pause_item.enabled = true;
menu_modified = true;
}
}
var remaining = status.unsynced - status.synced;
if (status.paused){
if (remaining)
new_progress = 'Pausing, ' + remaining + ' remain';
else new_progress = 'Paused';
} else {
if (remaining)
new_progress = 'Syncing, ' + remaining + ' remain';
else new_progress = 'Running, all synced';
}
}
if (new_pause !== pause_item.label.slice(0, new_pause.length)) {
pause_item.label = new_pause;
menu_modified = true;
}
if (new_progress !== progress_item.label) {
progress_item.label = new_progress; progress_item.label = new_progress;
menu_modified = true; pause_item.label = new_pause;
tray.menu = menu;
} }
if (menu_modified) tray.menu = menu;
get_status(socket); get_status(socket);
}, 1500); }, 1500);
......
var gui = require('nw.gui'); var gui = require('nw.gui');
var notification = {
0: 'Not initialized',
1: 'Initializing ...',
2: 'Shutting down',
100: 'Syncing',
101: 'Pausing',
102: 'Paused',
200: 'Settings are incomplete',
201: 'Cloud URL error',
202: 'Authentication error',
203: 'Local directory error',
204: 'Remote container error',
1000: 'Critical error'
}
function is_up(code) { return (code / 100 >> 0) === 1; }
function has_settings_error(code) { return (code / 200 >> 0) === 2; }
function remaining(status) { return status.unsynced - status.synced; }
var ntf_title = { var ntf_title = {
'info': 'Notification', 'info': 'Notification',
'warning': 'Warning', 'warning': 'Warning',
...@@ -16,7 +35,7 @@ var notify_menu = new gui.MenuItem({ ...@@ -16,7 +35,7 @@ var notify_menu = new gui.MenuItem({
icon: 'static/images/play_pause.png', icon: 'static/images/play_pause.png',
iconIsTemplate: false, iconIsTemplate: false,
click: function() { click: function() {
console.log('Notification is clecked'); console.log('Notification is clicked');
} }
}); });
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
var DEBUG = false; var DEBUG = true;
var gui = require('nw.gui'); var gui = require('nw.gui');
var path = require('path'); var path = require('path');
...@@ -21,6 +21,7 @@ var fs = require('fs'); ...@@ -21,6 +21,7 @@ var fs = require('fs');
// Read config file // Read config file
var cnf = JSON.parse(fs.readFileSync(gui.App.argv[0], encoding='utf-8')); var cnf = JSON.parse(fs.readFileSync(gui.App.argv[0], encoding='utf-8'));
var UI_COMMON = JSON.parse(fs.readFileSync(path.join('..', 'ui_common.json')));
function log_debug(msg) { if (DEBUG) console.log(msg); } function log_debug(msg) { if (DEBUG) console.log(msg); }
...@@ -28,12 +29,7 @@ function send_json(socket, msg) { ...@@ -28,12 +29,7 @@ function send_json(socket, msg) {
socket.send(JSON.stringify(msg)); socket.send(JSON.stringify(msg));
} }
var notifications = { var STATUS = UI_COMMON['STATUS'];
0: 'Syncer is consistent',
1: 'Local directory is not accessible',
2: 'Remote container is not accessible',
100: 'Unknown error'
}
var globals = { var globals = {
settings: { settings: {
...@@ -43,10 +39,8 @@ var globals = { ...@@ -43,10 +39,8 @@ var globals = {
directory: null, directory: null,
exclude: null exclude: null
}, },
status: { status: {synced: 0, unsynced: 0, code: STATUS['UNINITIALIZED']},
synced: 0, unsynced: 0, paused: null, can_sync: false, notification: 0},
authenticated: false, authenticated: false,
just_opened: false,
open_settings: false, open_settings: false,
settings_are_open: false settings_are_open: false
} }
...@@ -87,7 +81,7 @@ function put_settings(socket, new_settings) { ...@@ -87,7 +81,7 @@ function put_settings(socket, new_settings) {
function get_status(socket) { function get_status(socket) {
send_json(socket, {'method': 'get', 'path': 'status'}); send_json(socket, {'method': 'get', 'path': 'status'});
} // expected response {"synced":.., "unsynced":.., "paused":.., "can_sync":..} } // expected response {"synced":.., "unsynced":.., "code":..}
// Connect to helper // Connect to helper
...@@ -111,7 +105,6 @@ socket.onmessage = function(e) { ...@@ -111,7 +105,6 @@ socket.onmessage = function(e) {
get_settings(this); get_settings(this);
get_status(this); get_status(this);
globals.authenticated = true; globals.authenticated = true;
globals.just_opened = true;
} else { } else {
log_debug('Helper: ' + JSON.stringify(r)); log_debug('Helper: ' + JSON.stringify(r));
closeWindows(); closeWindows();
...@@ -139,10 +132,8 @@ socket.onmessage = function(e) { ...@@ -139,10 +132,8 @@ socket.onmessage = function(e) {
break; break;
case 'get status': case 'get status':
globals['status'] = r; globals['status'] = r;
if (globals.just_opened) { if (!globals.open_settings)
globals.just_opened = false; globals.open_settings = has_settings_error(r.code);
globals.open_settings = !r.can_sync;
}
break; break;
default: default:
console.log('Incomprehensible response ' + JSON.stringify(r)); console.log('Incomprehensible response ' + JSON.stringify(r));
......
...@@ -34,6 +34,10 @@ CURPATH = os.path.dirname(os.path.abspath(__file__)) ...@@ -34,6 +34,10 @@ CURPATH = os.path.dirname(os.path.abspath(__file__))
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
SYNCERS = utils.ThreadSafeDict() SYNCERS = utils.ThreadSafeDict()
with open(os.path.join(CURPATH, 'ui_common.json')) as f:
UI_COMMON = json.load(f)
STATUS = UI_COMMON['STATUS']
def retry_on_locked_db(method, *args, **kwargs): def retry_on_locked_db(method, *args, **kwargs):
"""If DB is locked, wait and try again""" """If DB is locked, wait and try again"""
...@@ -66,8 +70,6 @@ class SessionHelper(object): ...@@ -66,8 +70,6 @@ class SessionHelper(object):
self.db = sqlite3.connect(self.session_db) self.db = sqlite3.connect(self.session_db)
self._init_db_relation() self._init_db_relation()
# self.session = self._load_active_session() or self._create_session()
# self.db.close()
def _init_db_relation(self): def _init_db_relation(self):
"""Create the session relation""" """Create the session relation"""
...@@ -152,7 +154,7 @@ class SessionHelper(object): ...@@ -152,7 +154,7 @@ class SessionHelper(object):
return not bool(self.load_active_session()) return not bool(self.load_active_session())
def heartbeat(self): def heartbeat(self):
"""General session heartbeat - when heart stops, WSGI server dies""" """Periodically update the session database timestamp"""
db, alive = sqlite3.connect(self.session_db), True db, alive = sqlite3.connect(self.session_db), True
while alive: while alive:
time.sleep(2) time.sleep(2)
...@@ -249,19 +251,16 @@ class WebSocketProtocol(WebSocket): ...@@ -249,19 +251,16 @@ class WebSocketProtocol(WebSocket):
-- GET STATUS -- -- GET STATUS --
GUI: {"method": "get", "path": "status"} GUI: {"method": "get", "path": "status"}
HELPER: { HELPER: {"code": <int>,
"can_sync": <boolean>, "synced": <int>,
"notification": <int>, "unsynced": <int>,
"paused": <boolean>, "action": "get status"
"action": "get status"} or } or {<ERROR>: <ERROR CODE>, "action": "get status"}
{<ERROR>: <ERROR CODE>, "action": "get status"}
""" """
notification = { status = utils.ThreadSafeDict()
0: 'Syncer is consistent', with status.lock() as d:
1: 'Local directory is not accessible', d.update(code=STATUS['UNINITIALIZED'], synced=0, unsynced=0)
2: 'Remote container is not accessible',
100: 'unknown error'
}
ui_id = None ui_id = None
session_db, session_relation = None, None session_db, session_relation = None, None
accepted = False accepted = False
...@@ -269,21 +268,37 @@ class WebSocketProtocol(WebSocket): ...@@ -269,21 +268,37 @@ class WebSocketProtocol(WebSocket):
token=None, url=None, token=None, url=None,
container=None, directory=None, container=None, directory=None,
exclude=None) exclude=None)
status = dict(
notification=0, synced=0, unsynced=0, paused=True, can_sync=False)
cnf = AgkyraConfig() cnf = AgkyraConfig()
essentials = ('url', 'token', 'container', 'directory') essentials = ('url', 'token', 'container', 'directory')
def get_status(self, key=None):
""":return: updated status dict or value of specified key"""
if self.syncer and self.can_sync():
self._consume_messages()
with self.status.lock() as d:
if self.syncer.paused:
d['code'] = STATUS['PAUSED']
elif d['code'] != STATUS['PAUSING'] or (
d['unsynced'] == d['synced']):
d['code'] = STATUS['SYNCING']
with self.status.lock() as d:
return d.get(key, None) if key else dict(d)
def set_status(self, **kwargs):
with self.status.lock() as d:
d.update(kwargs)
@property @property
def syncer(self): def syncer(self):
""":returns: the first syncer object or None"""
with SYNCERS.lock() as d: with SYNCERS.lock() as d:
for sync_key, sync_obj in d.items(): for sync_key, sync_obj in d.items():
return sync_obj return sync_obj
return None return None
def clean_db(self): def clean_db(self):
"""Clean DB from session traces""" """Clean DB from current session trace"""
LOG.debug('Remove session traces') LOG.debug('Remove current session trace')
db = sqlite3.connect(self.session_db) db = sqlite3.connect(self.session_db)
db.execute('BEGIN') db.execute('BEGIN')
db.execute('DELETE FROM %s WHERE ui_id="%s"' % ( db.execute('DELETE FROM %s WHERE ui_id="%s"' % (
...@@ -292,7 +307,7 @@ class WebSocketProtocol(WebSocket): ...@@ -292,7 +307,7 @@ class WebSocketProtocol(WebSocket):
db.close() db.close()
def shutdown_syncer(self, syncer_key=0): def shutdown_syncer(self, syncer_key=0):
"""Shutdown the service""" """Shutdown the syncer backend object"""
LOG.debug('Shutdown syncer') LOG.debug('Shutdown syncer')
with SYNCERS.lock() as d: with SYNCERS.lock() as d:
syncer = d.pop(syncer_key, None) syncer = d.pop(syncer_key, None)
...@@ -302,7 +317,7 @@ class WebSocketProtocol(WebSocket): ...@@ -302,7 +317,7 @@ class WebSocketProtocol(WebSocket):
syncer.wait_sync_threads() syncer.wait_sync_threads()
def heartbeat(self): def heartbeat(self):
"""Check if socket should be alive""" """Update session DB timestamp as long as session is alive"""
db, alive = sqlite3.connect(self.session_db), True db, alive = sqlite3.connect(self.session_db), True
while alive: while alive:
time.sleep(1) time.sleep(1)
...@@ -320,6 +335,7 @@ class WebSocketProtocol(WebSocket): ...@@ -320,6 +335,7 @@ class WebSocketProtocol(WebSocket):
alive = True alive = True
db.close() db.close()
self.shutdown_syncer() self.shutdown_syncer()
self.set_status(code=STATUS['UNINITIALIZED'])
self.close() self.close()
def _get_default_sync(self): def _get_default_sync(self):
...@@ -358,10 +374,12 @@ class WebSocketProtocol(WebSocket): ...@@ -358,10 +374,12 @@ class WebSocketProtocol(WebSocket):
self.settings['url'] = self.cnf.get_cloud(cloud, 'url') self.settings['url'] = self.cnf.get_cloud(cloud, 'url')
except Exception: except Exception:
self.settings['url'] = None self.settings['url'] = None
self.set_status(code=STATUS['SETTINGS MISSING'])
try: try:
self.settings['token'] = self.cnf.get_cloud(cloud, 'token') self.settings['token'] = self.cnf.get_cloud(cloud, 'token')
except Exception: except Exception:
self.settings['url'] = None self.settings['url'] = None
self.set_status(code=STATUS['SETTINGS MISSING'])
# for option in ('container', 'directory', 'exclude'): # for option in ('container', 'directory', 'exclude'):
for option in ('container', 'directory'): for option in ('container', 'directory'):
...@@ -369,6 +387,7 @@ class WebSocketProtocol(WebSocket): ...@@ -369,6 +387,7 @@ class WebSocketProtocol(WebSocket):
self.settings[option] = self.cnf.get_sync(sync, option) self.settings[option] = self.cnf.get_sync(sync, option)
except KeyError: except KeyError:
LOG.debug('No %s is set' % option) LOG.debug('No %s is set' % option)
self.set_status(code=STATUS['SETTINGS MISSING'])
LOG.debug('Finished loading settings') LOG.debug('Finished loading settings')
...@@ -418,35 +437,31 @@ class WebSocketProtocol(WebSocket): ...@@ -418,35 +437,31 @@ class WebSocketProtocol(WebSocket):
return all([ return all([
self.settings[e] == self.settings[e] for e in self.essentials]) self.settings[e] == self.settings[e] for e in self.essentials])
def _consume_messages(self): def _consume_messages(self, max_consumption=10):
"""Update status by consuming and understanding syncer messages""" """Update status by consuming and understanding syncer messages"""
if self.can_sync(): if self.can_sync():
msg = self.syncer.get_next_message() msg = self.syncer.get_next_message()
if not msg: if not msg:
if self.status['unsynced'] == self.status['synced']: with self.status.lock() as d:
self.status['unsynced'] = 0 if d['unsynced'] == d['synced']:
self.status['synced'] = 0 d.update(unsynced=0, synced=0)
while (msg): while msg:
if isinstance(msg, messaging.SyncMessage): if isinstance(msg, messaging.SyncMessage):
# LOG.info('Start syncing "%s"' % msg.objname) self.set_status(unsynced=self.get_status('unsynced') + 1)
self.status['unsynced'] += 1
elif isinstance(msg, messaging.AckSyncMessage): elif isinstance(msg, messaging.AckSyncMessage):
# LOG.info('Finished syncing "%s"' % msg.objname) self.set_status(synced=self.get_status('synced') + 1)
self.status['synced'] += 1
# elif isinstance(msg, messaging.CollisionMessage):
# LOG.info('Collision for "%s"' % msg.objname)
# elif isinstance(msg, messaging.ConflictStashMessage):
# LOG.info('Conflict for "%s"' % msg.objname)
elif isinstance(msg, messaging.LocalfsSyncDisabled): elif isinstance(msg, messaging.LocalfsSyncDisabled):
# LOG.debug('Local FS is dissabled, noooo!') self.set_status(code=STATUS['DIRECTORY ERROR'])
self.status['notification'] = 1
self.syncer.stop_all_daemons() self.syncer.stop_all_daemons()
eli