From 18a575e02816622706f740e9938515869b58a375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Mon, 14 Sep 2015 14:52:30 +0200 Subject: Reimplemented communication with the server. * Using engine.io to use web sockets or long polling. * Changed to a single-page app. --- index.html | 162 ++- js/codeq.js | 3 +- js/codeq/comms.js | 271 +++- js/codeq/login.js | 2 +- js/codeq/startup.js | 97 ++ js/engine.io.js | 4040 +++++++++++++++++++++++++++++++++++++++++++++++++++ js/prolog.js | 6 +- prolog.html | 1 + 8 files changed, 4546 insertions(+), 36 deletions(-) create mode 100644 js/codeq/startup.js create mode 100644 js/engine.io.js diff --git a/index.html b/index.html index d8b0566..ae17a88 100644 --- a/index.html +++ b/index.html @@ -1,38 +1,140 @@ - CodeQ Login + + + + + + + + + + + + + + + + CodeQ -

CodeQ Login

-
- - - - - - - - - - - - - - - - - - -
Username:
Password:
Problem: - - -
- -
+ + +
+

CodeQ Login

+
+ + + + + + + + + + + + + + + + + + +
Username:
Password:
Problem: + + +
+ +
+
+ + + +
+ + - + + + + + + + + + + + + + + + + + + + + + diff --git a/js/codeq.js b/js/codeq.js index ed59e4e..f54b46e 100644 --- a/js/codeq.js +++ b/js/codeq.js @@ -726,7 +726,8 @@ window.phandler = null; // TODO: this is for debug only codeq.urlPrefix = '/svc/'; - $(document).ready(function () { +// $(document).ready + var deprecated = (function () { // var s = location.hash; // if (s.length == 0) return; // empty hash // if (s.charAt(0) == '#') s = s.substring(1); diff --git a/js/codeq/comms.js b/js/codeq/comms.js index 25c1f8c..f32b4db 100644 --- a/js/codeq/comms.js +++ b/js/codeq/comms.js @@ -1,4 +1,4 @@ -(function () { +/*(function () { var send = function (service, json) { if (json instanceof Object) json = codeq.jsonize(json); @@ -150,4 +150,273 @@ }); } }; +})();*/ + +(function () { + var BinaryMessageError = function (message) { + var e = new Error(message); + this.message = message; + if (e.stack) this.stack = e.stack; + }; + BinaryMessageError.prototype = new Error(); + BinaryMessageError.prototype.name = 'BinaryMessageError'; + + var MessageParseError = function (message) { + var e; + if (message instanceof Error) { + e = message; + this.message = 'Message parse error: ' + e.toString(); + } + else { + e = new Error(message); + this.message = message; + } + if (e.stack) this.stack = e.stack; + }; + MessageParseError.prototype = new Error(); + MessageParseError.prototype.name = 'MessageParseError'; + + var currentTID = 0, // transaction ID of the next message + sid = null, // session ID, changes after each successful initial login + socket = null, + reconnectTimer = null, + login_username = null, + login_password = null, + + waiters = {}, // keyed by TID: {packet, created, promise} + connected = false, + connectPromise = null, + handleIncomingMessageError = function (error, data) { + if (error instanceof BinaryMessageError) { + codeq.log.error('Incoming message is not a string, attempting reconnect'); + connected = false; + socket.close(); // connection will be reestablished in the close event handler + } + else if (error instanceof MessageParseError) { + codeq.log.error('Incoming message is not a JSON message, ignoring: ' + data); + } + else { + codeq.log.error('Unknown error, attempting reconnect: ' + error); + connected = false; + socket.close(); // connection will be reestablished in the close event handler + } + }, + parseData = function (data) { + if (typeof data !== 'string') { + throw new BinaryMessageError('Message is not a string'); + } + + try { + return JSON.parse(data); + } + catch (e) { + codeq.log.error('Incoming message is not a JSON message, ignoring: ' + data); + throw new MessageParseError(e); + } + }, + onMessage = function (data) { + var m, waiter; + + try { + m = parseData(data); + } + catch (e) { + handleIncomingMessageError(e, data); + return; + } + + if ('tid' in m) { + waiter = waiters[m.tid]; + if (waiter) { + delete waiters[m.tid]; + waiter.promise.resolve(m); + codeq.log.debug('Delivered an incoming message, TID=' + m.tid); + return; + } + else { + codeq.log.debug('There is noone waiting for the incoming message, TID=' + m.tid); + } + } + else { + codeq.log.debug('Incoming message without a TID, handing it off to handlers'); + } + + // TODO handle message delivery + }, + onConnect = function () { + var tid, sortedWaiters = [], i; + connected = true; + if (connectPromise) { + // manual connect + connectPromise.resolve(); + connectPromise = null; + } + else { + // auto re-connect + } + // resend anything not already sent + for (tid in waiters) { + if (waiters.hasOwnProperty(tid)) { + sortedWaiters.push(waiters[tid]); + } + } + // sort by ascending transaction ID before sending + sortedWaiters.sort(function (a, b) {return a.tid - b.tid;}); + for (i = 0; i < sortedWaiters.length; i++) { + (function (waiter) { + if (!waiter.sent) { + socket.send(JSON.stringify(waiter.packet), function () { + waiter.sent = true; + }); + } + })(sortedWaiters[i]); + } + }, + onClose = function () { + connected = false; + if (connectPromise) { + // resulted from manual connect, don't attempt reconnect + connectPromise.reject(new Error('Connection failed')); + connectPromise = null; + socket.off('open', onConnect); + socket.off('close', onClose); + socket.off('message', onMessage); + socket = null; + } + else { + // spurious disconnect, attempt reconnect + reconnectTimer = setTimeout(function () { + reconnectTimer = null; + socket.open(); + }, 250); // reconnect in quarter of a second + } + }; + + codeq.comms = { + 'connect': function () { + var deferred = Q.defer(); + + if (connectPromise) { + // connection already taking place + connectPromise.then(deferred.resolve, deferred.reject, deferred.notify); + } + else if (socket) { + // already connected + if (connected) { + deferred.resolve(); + } + else { + // auto re-connecting + connectPromise = deferred; + } + } + else { + // create a new connection + connectPromise = deferred; + socket = eio('ws://' + location.host, { + 'upgrade': true, + 'path': '/ws', + 'transports': ['polling', 'websocket'] + }); + socket.on('open', onConnect); + socket.on('close', onClose); + socket.on('message', onMessage); + } + + return deferred.promise; + }, + 'disconnect': function () { + if (socket) { + socket.off('open', onConnect); + socket.off('close', onClose); + socket.off('message', onMessage); + socket.close(); + socket = null; + } + if (reconnectTimer !== null) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + connected = false; + if (connectPromise) { + connectPromise.reject(new Error('Forced disconnect')); + connectPromise = null; + } + login_password = null; // manual disconnect will prevent auto-login + sid = null; + }, + 'send': function (packet) { + return Q.Promise(function (resolve, reject, notify) { + var waiter = { + 'packet': packet, + 'created': Date.now(), + 'promise': {'resolve': resolve, 'reject': reject, 'notify': notify}, + 'sent': false + }; + packet.tid = currentTID; + if (sid !== null) packet.sid = sid; + waiters[currentTID] = waiter; + currentTID++; + if (connected) { + socket.send(JSON.stringify(packet), function () { waiter.sent = true; }); + } + }); + }, + 'login': function (username, password) { + var packet = {'action': 'login', 'username': username, 'password': password}, + myself = this, + performLogin = function () { + return myself.send(packet).then(function (response) { + if (response.code === 0) { + // login successful, remember the login credentials for auto-reconnects + login_username = username; + login_password = password; + sid = response.new_sid || response.sid; + } + return response; + }); + }; + if (socket) return performLogin(); + // this is the only method where we do connect() first, if we're not already connected + return this.connect().then(performLogin); + }, + + sendActivity: function commsSendActivity (trace, solution, problem_id) { + return this.send({'action': 'activity', 'trace': trace, 'solution': solution, 'problem_id': problem_id}); + }, + + sendQuery: function commsSendQuery (query, problem_id) { + query['action'] = 'query'; + return this.send(query); + }, + + sendPush: function commsSendPush (json) { + json['action'] = 'python_push'; + return this.send(json); + }, + + sendPull: function commsSendPull (json) { + json['action'] = 'python_pull'; + return this.send(json); + }, + + sendHint: function commsSendHint (json) { + json['action'] = 'hint'; + return this.send(json); + }, + + sendTest: function commsSendTest (json) { + json['action'] = 'test'; + return this.send(json); + }, + + getProblem: function commsGetProblem (language, problem_group, problem) { + return this.send({ + 'action': 'get_problem', + 'language': language, + 'problem_group': problem_group, + 'problem': problem + }); + } + }; })(); diff --git a/js/codeq/login.js b/js/codeq/login.js index e1daadf..ef67304 100644 --- a/js/codeq/login.js +++ b/js/codeq/login.js @@ -20,7 +20,7 @@ jqGroup = $('#problem_group'), jqProblems = $('#problems'), html = [], - mapping = {} + mapping = {}, onGroupChange = function () { var problems = mapping[jqGroup.val()], html = [], diff --git a/js/codeq/startup.js b/js/codeq/startup.js new file mode 100644 index 0000000..980d3d7 --- /dev/null +++ b/js/codeq/startup.js @@ -0,0 +1,97 @@ +$(document).ready(function () { + codeq.comms.connect().then(function () { + return codeq.comms.send({'action': 'list_problems'}); + }).then( + function success(data) { + var i, groups, group, problems, problem, first_group, + jqGroup = $('#problem_group'), + jqProblems = $('#problems'), + html = [], + mapping = {}, + onGroupChange = function () { + var problems = mapping[jqGroup.val()], + html = [], + i, p; + if (problems) { + for (i = 0; i < problems.length; i++) { + p = problems[i]; + html.push('\n') + } + } + jqProblems.html(html.join('')); + }; + + if (data && (data.code === 0)) { + $('#disabled').css('display', 'none'); + groups = data.problems; + for (i = 0; i < groups.length; i++) { + group = groups[i]; + var identifier = group.identifier.language + '/' + group.identifier.group; + var name = group.name.language + ': ' + group.name.group; + html.push('\n'); + mapping[identifier] = group.problems; + } + jqGroup.html(html.join('')); + first_group = html[1]; + html = null; + + jqGroup.on('click', onGroupChange); + jqGroup.val(first_group); + onGroupChange(); + + $("#submit").on('click', function () { + var identifier = $('#problem_group').val().split('/'), + problem = $('#problems').val(); + if (identifier.length < 2) alert('Choose a problem group'); + else if (!problem) alert('Choose a problem'); + else { + $('#disabled').css('display', ''); + codeq.comms.login($('#username').val(), $('#password').val()) + .then(function (data) { + $('#disabled').css('display', 'none'); + if (data.code !== 0) throw new Error('Login failed, code: ' + data.code + ', message: ' + data.message); + }) + .then(function () { + return codeq.comms.getProblem(identifier[0], identifier[1], problem); + }) + .then(function (data) { + if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message); + $('#disabled').css('display', 'none'); + switch (identifier[0]) { + case 'prolog': + // TODO: assignment to window for debug only + $('#screen_login').css('display', 'none'); + $('#screen_prolog').css('display', ''); + window.phandler = codeq.createPrologHandler(data.data); + break; + case 'python': + // TODO: assignment to window for debug only + $('#screen_login').css('display', 'none'); + $('#screen_prolog').css('display', ''); + window.phandler = codeq.createPythonHandler(data.data); + break; + default: + alert('Unknown language: ' + identifier[0]); + break; + } + }) + .fail(function (reason) { + $('#disabled').css('display', 'none'); + alert('Login request failed: ' + reason); + }) + .done(); + } + }); + } + else { + $('#disabled').css('cursor', ''); + alert('Obtaining list of problems failed: code=' + data.code + ', reason=' + data.message); + } + }, + + function failure(reason) { + $('#disabled').css('cursor', ''); + alert('Request to obtain list of problems failed: ' + reason); + } + ).done(); +}); \ No newline at end of file diff --git a/js/engine.io.js b/js/engine.io.js new file mode 100644 index 0000000..f502946 --- /dev/null +++ b/js/engine.io.js @@ -0,0 +1,4040 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.eio=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o