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:
parent
d5b3b59eb7
commit
7e62f02f0a
2 changed files with 77 additions and 18 deletions
|
|
@ -16,13 +16,12 @@ http://localhost:8000/webclient.)
|
||||||
handle these requests and act as a gateway
|
handle these requests and act as a gateway
|
||||||
to sessions connected over the webclient.
|
to sessions connected over the webclient.
|
||||||
"""
|
"""
|
||||||
import time
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from time import time
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
from twisted.web import server, resource
|
from twisted.web import server, resource
|
||||||
|
from twisted.internet.task import LoopingCall
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.encoding import force_unicode
|
from django.utils.encoding import force_unicode
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -30,8 +29,8 @@ from evennia.utils import utils
|
||||||
from evennia.utils.text2html import parse_html
|
from evennia.utils.text2html import parse_html
|
||||||
from evennia.server import session
|
from evennia.server import session
|
||||||
|
|
||||||
SERVERNAME = settings.SERVERNAME
|
_SERVERNAME = settings.SERVERNAME
|
||||||
ENCODINGS = settings.ENCODINGS
|
_KEEPALIVE = 30 # how often to check keepalive
|
||||||
|
|
||||||
# defining a simple json encoder for returning
|
# defining a simple json encoder for returning
|
||||||
# django data to the client. Might need to
|
# django data to the client. Might need to
|
||||||
|
|
@ -66,11 +65,8 @@ class WebClient(resource.Resource):
|
||||||
self.requests = {}
|
self.requests = {}
|
||||||
self.databuffer = {}
|
self.databuffer = {}
|
||||||
|
|
||||||
#def getChild(self, path, request):
|
self.last_alive = {}
|
||||||
# """
|
self.keep_alive = None
|
||||||
# This is the place to put dynamic content.
|
|
||||||
# """
|
|
||||||
# return self
|
|
||||||
|
|
||||||
def _responseFailed(self, failure, suid, request):
|
def _responseFailed(self, failure, suid, request):
|
||||||
"callback if a request is lost/timed out"
|
"callback if a request is lost/timed out"
|
||||||
|
|
@ -79,6 +75,33 @@ class WebClient(resource.Resource):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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):
|
def lineSend(self, suid, data):
|
||||||
"""
|
"""
|
||||||
This adds the data to the buffer and/or sends it to the client
|
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]
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
|
|
||||||
remote_addr = request.getClientIP()
|
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':
|
if suid == '0':
|
||||||
# creating a unique id hash string
|
# creating a unique id hash string
|
||||||
suid = md5(str(time.time())).hexdigest()
|
suid = md5(str(time())).hexdigest()
|
||||||
self.databuffer[suid] = []
|
self.databuffer[suid] = []
|
||||||
|
|
||||||
sess = WebClientSession()
|
sess = WebClientSession()
|
||||||
|
|
@ -138,8 +161,27 @@ class WebClient(resource.Resource):
|
||||||
sess.init_session("webclient", remote_addr, self.sessionhandler)
|
sess.init_session("webclient", remote_addr, self.sessionhandler)
|
||||||
sess.suid = suid
|
sess.suid = suid
|
||||||
sess.sessionhandler.connect(sess)
|
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})
|
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):
|
def mode_input(self, request):
|
||||||
"""
|
"""
|
||||||
This is called by render_POST when the client
|
This is called by render_POST when the client
|
||||||
|
|
@ -152,6 +194,8 @@ class WebClient(resource.Resource):
|
||||||
suid = request.args.get('suid', ['0'])[0]
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
if suid == '0':
|
if suid == '0':
|
||||||
return '""'
|
return '""'
|
||||||
|
|
||||||
|
self.last_alive[suid] = (time(), False)
|
||||||
sess = self.sessionhandler.session_from_suid(suid)
|
sess = self.sessionhandler.session_from_suid(suid)
|
||||||
if sess:
|
if sess:
|
||||||
sess = sess[0]
|
sess = sess[0]
|
||||||
|
|
@ -173,6 +217,7 @@ class WebClient(resource.Resource):
|
||||||
suid = request.args.get('suid', ['0'])[0]
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
if suid == '0':
|
if suid == '0':
|
||||||
return '""'
|
return '""'
|
||||||
|
self.last_alive[suid] = (time(), False)
|
||||||
|
|
||||||
dataentries = self.databuffer.get(suid, [])
|
dataentries = self.databuffer.get(suid, [])
|
||||||
if dataentries:
|
if dataentries:
|
||||||
|
|
@ -230,8 +275,11 @@ class WebClient(resource.Resource):
|
||||||
elif dmode == 'close':
|
elif dmode == 'close':
|
||||||
# the client is closing
|
# the client is closing
|
||||||
return self.mode_close(request)
|
return self.mode_close(request)
|
||||||
|
elif dmode == 'keepalive':
|
||||||
|
# A reply to our keepalive request - all is well
|
||||||
|
return self.mode_keepalive(request)
|
||||||
else:
|
else:
|
||||||
# this should not happen if client sends valid data.
|
# This should not happen if client sends valid data.
|
||||||
return '""'
|
return '""'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -245,6 +293,10 @@ class WebClientSession(session.Session):
|
||||||
This represents a session running in a webclient.
|
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."):
|
def disconnect(self, reason="Server disconnected."):
|
||||||
"""
|
"""
|
||||||
Disconnect from server.
|
Disconnect from server.
|
||||||
|
|
@ -254,6 +306,7 @@ class WebClientSession(session.Session):
|
||||||
"""
|
"""
|
||||||
self.client.lineSend(self.suid, ["connection_close", [reason], {}])
|
self.client.lineSend(self.suid, ["connection_close", [reason], {}])
|
||||||
self.client.client_disconnect(self.suid)
|
self.client.client_disconnect(self.suid)
|
||||||
|
self.sessionhandler.disconnect(self)
|
||||||
|
|
||||||
def data_out(self, **kwargs):
|
def data_out(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,6 @@ An "emitter" object must have a function
|
||||||
//
|
//
|
||||||
var WebsocketConnection = function () {
|
var WebsocketConnection = function () {
|
||||||
log("Trying websocket ...");
|
log("Trying websocket ...");
|
||||||
wsurl = "ws://blah";
|
|
||||||
var open = false;
|
var open = false;
|
||||||
var websocket = new WebSocket(wsurl);
|
var websocket = new WebSocket(wsurl);
|
||||||
// Handle Websocket open event
|
// Handle Websocket open event
|
||||||
|
|
@ -275,11 +274,12 @@ An "emitter" object must have a function
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send Client -> Evennia. Called by Evennia.msg
|
// Send Client -> Evennia. Called by Evennia.msg
|
||||||
var msg = function(data) {
|
var msg = function(data, inmode) {
|
||||||
$.ajax({type: "POST", url: "/webclientdata",
|
$.ajax({type: "POST", url: "/webclientdata",
|
||||||
async: true, cache: false, timeout: 30000,
|
async: true, cache: false, timeout: 30000,
|
||||||
dataType: "json",
|
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) {},
|
success: function(req, stat, err) {},
|
||||||
error: function(req, stat, err) {
|
error: function(req, stat, err) {
|
||||||
Evennia.emit("connection_error", ["AJAX/COMET send error"], err);
|
Evennia.emit("connection_error", ["AJAX/COMET send error"], err);
|
||||||
|
|
@ -298,8 +298,14 @@ An "emitter" object must have a function
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: {mode: 'receive', 'suid': client_hash},
|
data: {mode: 'receive', 'suid': client_hash},
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
log("ajax data received:", data);
|
// 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]);
|
Evennia.emit(data[0], data[1], data[2]);
|
||||||
|
}
|
||||||
poll(); // immiately start a new request
|
poll(); // immiately start a new request
|
||||||
},
|
},
|
||||||
error: function(req, stat, err) {
|
error: function(req, stat, err) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue