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.

This commit is contained in:
Griatch 2016-04-16 22:09:10 +02:00
parent 795061907f
commit c3bb30c46f
2 changed files with 77 additions and 18 deletions

View file

@ -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):
"""

View file

@ -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) {