From c3bb30c46f8a817f089ebd4ba66105ae4e3653f4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 16 Apr 2016 22:09:10 +0200 Subject: [PATCH] Implemented an AJAX keepalive from the Evennia side to make sure a dead AJAX connection (commonly if the user closes the window/browser without properly logging out), will eventually time out and log off the Player. Resolves #951. --- evennia/server/portal/webclient_ajax.py | 79 ++++++++++++++++--- .../webclient/static/webclient/js/evennia.js | 16 ++-- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py index a889e72a5c..f894ed6206 100644 --- a/evennia/server/portal/webclient_ajax.py +++ b/evennia/server/portal/webclient_ajax.py @@ -16,13 +16,12 @@ http://localhost:8000/webclient.) handle these requests and act as a gateway to sessions connected over the webclient. """ -import time import json +from time import time from hashlib import md5 - from twisted.web import server, resource - +from twisted.internet.task import LoopingCall from django.utils.functional import Promise from django.utils.encoding import force_unicode from django.conf import settings @@ -30,8 +29,8 @@ from evennia.utils import utils from evennia.utils.text2html import parse_html from evennia.server import session -SERVERNAME = settings.SERVERNAME -ENCODINGS = settings.ENCODINGS +_SERVERNAME = settings.SERVERNAME +_KEEPALIVE = 30 # how often to check keepalive # defining a simple json encoder for returning # django data to the client. Might need to @@ -66,11 +65,8 @@ class WebClient(resource.Resource): self.requests = {} self.databuffer = {} - #def getChild(self, path, request): - # """ - # This is the place to put dynamic content. - # """ - # return self + self.last_alive = {} + self.keep_alive = None def _responseFailed(self, failure, suid, request): "callback if a request is lost/timed out" @@ -79,6 +75,33 @@ class WebClient(resource.Resource): except KeyError: pass + def _keepalive(self): + """ + Callback for checking the connection is still alive. + """ + now = time() + to_remove = [] + keep_alives = ((suid, remove) for suid, (t, remove) + in self.last_alive.iteritems() if now - t > _KEEPALIVE) + for suid, remove in keep_alives: + if remove: + # keepalive timeout. Line is dead. + to_remove.append(suid) + else: + # normal timeout - send keepalive + self.last_alive[suid] = (now, True) + self.lineSend(suid, ["ajax_keepalive", [], {}]) + # remove timed-out sessions + for suid in to_remove: + sess = self.sessionhandler.session_from_suid(suid) + if sess: + sess[0].disconnect() + self.last_alive.pop(suid, None) + if not self.last_alive: + # no more ajax clients. Stop the keepalive + self.keep_alive.stop() + self.keep_alive = None + def lineSend(self, suid, data): """ This adds the data to the buffer and/or sends it to the client @@ -127,10 +150,10 @@ class WebClient(resource.Resource): suid = request.args.get('suid', ['0'])[0] remote_addr = request.getClientIP() - host_string = "%s (%s:%s)" % (SERVERNAME, request.getRequestHostname(), request.getHost().port) + host_string = "%s (%s:%s)" % (_SERVERNAME, request.getRequestHostname(), request.getHost().port) if suid == '0': # creating a unique id hash string - suid = md5(str(time.time())).hexdigest() + suid = md5(str(time())).hexdigest() self.databuffer[suid] = [] sess = WebClientSession() @@ -138,8 +161,27 @@ class WebClient(resource.Resource): sess.init_session("webclient", remote_addr, self.sessionhandler) sess.suid = suid sess.sessionhandler.connect(sess) + + self.last_alive[suid] = (time(), False) + if not self.keep_alive: + # the keepalive is not running; start it. + self.keep_alive = LoopingCall(self._keepalive) + self.keep_alive.start(_KEEPALIVE, now=False) + return jsonify({'msg': host_string, 'suid': suid}) + def mode_keepalive(self, request): + """ + This is called by render_POST when the + client is replying to the keepalive. + """ + suid = request.args.get('suid', ['0'])[0] + if suid == '0': + return '""' + print "keepalive succeeded" + self.last_alive[suid] = (time(), False) + return '""' + def mode_input(self, request): """ This is called by render_POST when the client @@ -152,6 +194,8 @@ class WebClient(resource.Resource): suid = request.args.get('suid', ['0'])[0] if suid == '0': return '""' + + self.last_alive[suid] = (time(), False) sess = self.sessionhandler.session_from_suid(suid) if sess: sess = sess[0] @@ -173,6 +217,7 @@ class WebClient(resource.Resource): suid = request.args.get('suid', ['0'])[0] if suid == '0': return '""' + self.last_alive[suid] = (time(), False) dataentries = self.databuffer.get(suid, []) if dataentries: @@ -230,8 +275,11 @@ class WebClient(resource.Resource): elif dmode == 'close': # the client is closing return self.mode_close(request) + elif dmode == 'keepalive': + # A reply to our keepalive request - all is well + return self.mode_keepalive(request) else: - # this should not happen if client sends valid data. + # This should not happen if client sends valid data. return '""' @@ -245,6 +293,10 @@ class WebClientSession(session.Session): This represents a session running in a webclient. """ + def __init__(self, *args, **kwargs): + self.protocol_name = "ajax/comet" + super(WebClientSession, self).__init__(*args, **kwargs) + def disconnect(self, reason="Server disconnected."): """ Disconnect from server. @@ -254,6 +306,7 @@ class WebClientSession(session.Session): """ self.client.lineSend(self.suid, ["connection_close", [reason], {}]) self.client.client_disconnect(self.suid) + self.sessionhandler.disconnect(self) def data_out(self, **kwargs): """ diff --git a/evennia/web/webclient/static/webclient/js/evennia.js b/evennia/web/webclient/static/webclient/js/evennia.js index 114d81b8c9..b9b8875479 100644 --- a/evennia/web/webclient/static/webclient/js/evennia.js +++ b/evennia/web/webclient/static/webclient/js/evennia.js @@ -195,7 +195,6 @@ An "emitter" object must have a function // var WebsocketConnection = function () { log("Trying websocket ..."); - wsurl = "ws://blah"; var open = false; var websocket = new WebSocket(wsurl); // Handle Websocket open event @@ -275,11 +274,12 @@ An "emitter" object must have a function }; // Send Client -> Evennia. Called by Evennia.msg - var msg = function(data) { + var msg = function(data, inmode) { $.ajax({type: "POST", url: "/webclientdata", async: true, cache: false, timeout: 30000, dataType: "json", - data: {mode:'input', data: JSON.stringify(data), 'suid': client_hash}, + data: {mode: inmode == null ? 'input' : inmode, + data: JSON.stringify(data), 'suid': client_hash}, success: function(req, stat, err) {}, error: function(req, stat, err) { Evennia.emit("connection_error", ["AJAX/COMET send error"], err); @@ -298,8 +298,14 @@ An "emitter" object must have a function dataType: "json", data: {mode: 'receive', 'suid': client_hash}, success: function(data) { - log("ajax data received:", data); - Evennia.emit(data[0], data[1], data[2]); + // log("ajax data received:", data); + if (data[0] === "ajax_keepalive") { + // special ajax keepalive check - return immediately + msg("", "keepalive"); + } else { + // not a keepalive + Evennia.emit(data[0], data[1], data[2]); + } poll(); // immiately start a new request }, error: function(req, stat, err) {