Made ajax/comet client fallback work correctly in the new framework.

This commit is contained in:
Griatch 2016-02-14 13:29:41 +01:00
parent 83570848d6
commit 166189a7a5
7 changed files with 168 additions and 108 deletions

View file

@ -419,4 +419,4 @@ class OOBHandler(TickerHandler):
for key, stored in self.oob_monitor_storage.items() if key[1] == sessid] for key, stored in self.oob_monitor_storage.items() if key[1] == sessid]
# access object # access object
INPUT_HANDLER = OOBHandler() MONITOR_HANDLER = OOBHandler()

View file

@ -93,7 +93,7 @@ class WebSocketClient(Protocol, Session):
cmdarray = json.loads(string) cmdarray = json.loads(string)
print "dataReceived:", cmdarray print "dataReceived:", cmdarray
if cmdarray: if cmdarray:
self.data_in(**{cmdarray[0]:[cmdarray[1], cmdarray[2]]}) self.data_in(**{cmdarray[0], [cmdarray[1], cmdarray[2]]})
def sendLine(self, line): def sendLine(self, line):
""" """
@ -147,6 +147,8 @@ class WebSocketClient(Protocol, Session):
text = args[0] text = args[0]
if text is None: if text is None:
return return
text = to_str(text, force_string=True)
options = kwargs.get("options", {}) options = kwargs.get("options", {})
raw = options.get("raw", False) raw = options.get("raw", False)
nomarkup = options.get("nomarkup", False) nomarkup = options.get("nomarkup", False)
@ -186,5 +188,5 @@ class WebSocketClient(Protocol, Session):
""" """
if not cmdname == "options": if not cmdname == "options":
print "send_default", cmdname, args, kwargs print "websocket.send_default", cmdname, args, kwargs
session.sendLine(json.dumps([cmdname, args, kwargs])) session.sendLine(json.dumps([cmdname, args, kwargs]))

View file

@ -1,11 +1,11 @@
""" """
AJAX fallback webclient AJAX/COMET fallback webclient
The AJAX web client consists of two components running The AJAX/COMET web client consists of two components running on
on twisted and django. They are both a part of the Evennia twisted and django. They are both a part of the Evennia website url
website url tree (so the testing website might be located tree (so the testing website might be located on
on http://localhost:8000/, whereas the webclient can be http://localhost:8000/, whereas the webclient can be found on
found on http://localhost:8000/webclient.) http://localhost:8000/webclient.)
/webclient - this url is handled through django's template /webclient - this url is handled through django's template
system and serves the html page for the client system and serves the html page for the client
@ -79,30 +79,26 @@ class WebClient(resource.Resource):
except KeyError: except KeyError:
pass pass
def lineSend(self, suid, string, data=None): 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
as soon as possible. as soon as possible.
Args: Args:
suid (int): Session id. suid (int): Session id.
string (str): The text to send. data (list): A send structure [cmdname, [args], {kwargs}].
data (dict): Optional data.
Notes:
The `data` keyword is deprecated.
""" """
request = self.requests.get(suid) request = self.requests.get(suid)
if request: if request:
# we have a request waiting. Return immediately. # we have a request waiting. Return immediately.
request.write(jsonify({'msg': string, 'data': data})) request.write(jsonify(data))
request.finish() request.finish()
del self.requests[suid] del self.requests[suid]
else: else:
# no waiting request. Store data in buffer # no waiting request. Store data in buffer
dataentries = self.databuffer.get(suid, []) dataentries = self.databuffer.get(suid, [])
dataentries.append(jsonify({'msg': string, 'data': data})) dataentries.append(jsonify(data))
self.databuffer[suid] = dataentries self.databuffer[suid] = dataentries
def client_disconnect(self, suid): def client_disconnect(self, suid):
@ -128,9 +124,6 @@ class WebClient(resource.Resource):
request (Request): Incoming request. request (Request): Incoming request.
""" """
#csess = request.getSession() # obs, this is a cookie, not
# an evennia session!
#csees.expireCallbacks.append(lambda : )
suid = request.args.get('suid', ['0'])[0] suid = request.args.get('suid', ['0'])[0]
remote_addr = request.getClientIP() remote_addr = request.getClientIP()
@ -158,14 +151,13 @@ 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 '""'
sess = self.sessionhandler.session_from_suid(suid) sess = self.sessionhandler.session_from_suid(suid)
if sess: if sess:
sess = sess[0] sess = sess[0]
text = request.args.get('msg', [''])[0] cmdarray = json.loads(request.args.get('data')[0])
data = request.args.get('data', [None])[0] sess.sessionhandler.data_in(sess, **{cmdarray[0]:[cmdarray[1], cmdarray[2]]})
sess.sessionhandler.data_in(sess, text, data=data) return '""'
return ''
def mode_receive(self, request): def mode_receive(self, request):
""" """
@ -180,7 +172,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 '""'
dataentries = self.databuffer.get(suid, []) dataentries = self.databuffer.get(suid, [])
if dataentries: if dataentries:
@ -210,7 +202,7 @@ class WebClient(resource.Resource):
except IndexError: except IndexError:
self.client_disconnect(suid) self.client_disconnect(suid)
pass pass
return '' return '""'
def render_POST(self, request): def render_POST(self, request):
""" """
@ -240,7 +232,7 @@ class WebClient(resource.Resource):
return self.mode_close(request) return self.mode_close(request)
else: else:
# this should not happen if client sends valid data. # this should not happen if client sends valid data.
return '' return '""'
# #
@ -261,28 +253,63 @@ class WebClientSession(session.Session):
reason (str): Motivation for the disconnect. reason (str): Motivation for the disconnect.
""" """
if reason: if reason:
self.client.lineSend(self.suid, reason) self.client.lineSend(self.suid, ["text", [reason], {}])
self.client.client_disconnect(self.suid) self.client.client_disconnect(self.suid)
def data_out(self, text=None, **kwargs): def data_out(self, **kwargs):
""" """
Data Evennia -> User access hook. Data Evennia -> User
Kwargs:
kwargs (any): Options to the protocol
"""
self.sessionhandler.data_out(self, **kwargs)
def send_text(self, *args, **kwargs):
"""
Send text data.
Args:
text (str): The first argument is always the text string to send. No other arguments
are considered.
Kwargs: Kwargs:
raw (bool): No parsing at all (leave ansi-to-html markers unparsed). raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
nomarkup (bool): Clean out all ansi/html markers and tokens. nomarkup (bool): Clean out all ansi/html markers and tokens.
""" """
# string handling is similar to telnet # string handling is similar to telnet
try: if args:
text = utils.to_str(text if text else "", encoding=self.encoding) args = list(args)
raw = kwargs.get("raw", False) text = args[0]
nomarkup = kwargs.get("nomarkup", False) if text is None:
if raw:
self.client.lineSend(self.suid, text)
else:
self.client.lineSend(self.suid,
parse_html(text, strip_ansi=nomarkup))
return return
except Exception: text = utils.to_str(text, force_string=True)
logger.log_trace()
options = kwargs.get("options", {})
raw = options.get("raw", False)
nomarkup = options.get("nomarkup", False)
if raw:
args[0] = text
else:
args[0] = parse_html(text, strip_ansi=nomarkup)
self.client.lineSend(self.suid, ["text", args, kwargs])
def send_default(self, cmdname, *args, **kwargs):
"""
Data Evennia -> User.
Args:
cmdname (str): The first argument will always be the oob cmd name.
*args (any): Remaining args will be arguments for `cmd`.
Kwargs:
options (dict): These are ignored for oob commands. Use command
arguments (which can hold dicts) to send instructions to the
client instead.
"""
if not cmdname == "options":
print "ajax.send_default", cmdname, args, kwargs
self.client.lineSend(self.suid, [cmdname, args, kwargs])

View file

@ -276,8 +276,8 @@ class Evennia(object):
with open(SERVER_RESTART, 'r') as f: with open(SERVER_RESTART, 'r') as f:
mode = f.read() mode = f.read()
if mode in ('True', 'reload'): if mode in ('True', 'reload'):
from evennia.server.oobhandler import OOB_HANDLER from evennia.scripts.monitorhandler import MONITOR_HANDLER
OOB_HANDLER.restore() MONITOR_HANDLER.restore()
from evennia.scripts.tickerhandler import TICKER_HANDLER from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore() TICKER_HANDLER.restore()
@ -352,9 +352,9 @@ class Evennia(object):
yield [(s.pause(manual_pause=False), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances() if s.is_active] yield [(s.pause(manual_pause=False), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances() if s.is_active]
yield self.sessions.all_sessions_portal_sync() yield self.sessions.all_sessions_portal_sync()
self.at_server_reload_stop() self.at_server_reload_stop()
# only save OOB state on reload, not on shutdown/reset # only save monitor state on reload, not on shutdown/reset
from evennia.server.oobhandler import OOB_HANDLER from evennia.scripts.monitorhandler import MONITOR_HANDLER
OOB_HANDLER.save() MONITOR_HANDLER.save()
else: else:
if mode == 'reset': if mode == 'reset':
# like shutdown but don't unset the is_connected flag and don't disconnect sessions # like shutdown but don't unset the is_connected flag and don't disconnect sessions

View file

@ -10,16 +10,20 @@ old and does not support websockets, it will instead fall back to a
long-polling (AJAX/COMET) type of connection (using long-polling (AJAX/COMET) type of connection (using
evennia/server/portal/webclient_ajax.py) evennia/server/portal/webclient_ajax.py)
All messages is a valid JSON array on single form: ["cmdname", All messages is a valid JSON array on single form:
kwargs], where kwargs is a JSON object that will be used as argument
to call the cmdname function. ["cmdname", args, kwargs],
where kwargs is a JSON object that will be used as argument to call
the cmdname function.
This library makes the "Evennia" object available. It has the This library makes the "Evennia" object available. It has the
following official functions: following official functions:
- Evennia.init(options) - Evennia.init(options)
This can be called by the frontend to intialize the library. The This stores the connections/emitters and creates the websocket/ajax connection.
argument is an js object with the following possible keys: This can be called as often as desired - the lib will still only be
initialized once. The argument is an js object with the following possible keys:
'connection': This defaults to Evennia.WebsocketConnection but 'connection': This defaults to Evennia.WebsocketConnection but
can also be set to Evennia.CometConnection for backwards can also be set to Evennia.CometConnection for backwards
compatibility. See below. compatibility. See below.
@ -34,15 +38,15 @@ following official functions:
A "connection" object must have the method A "connection" object must have the method
- msg(data) - this should relay data to the Server. This function should itself handle - msg(data) - this should relay data to the Server. This function should itself handle
the conversion to JSON before sending across the wire. the conversion to JSON before sending across the wire.
- When receiving data from the Server (always [cmdname, kwargs]), this must be - When receiving data from the Server (always data = [cmdname, args, kwargs]), this must be
JSON-unpacked and the result redirected to Evennia.emit(data[0], data[1]). JSON-unpacked and the result redirected to Evennia.emit(data[0], data[1], data[2]).
An "emitter" object must have a function An "emitter" object must have a function
- emit(cmdname, kwargs) - this will be called by the backend. - emit(cmdname, args, kwargs) - this will be called by the backend and is expected to
relay the data to its correct gui element.
- The default emitter also has the following methods: - The default emitter also has the following methods:
- on(cmdname, listener) - this ties a listener to the backend. This function - on(cmdname, listener) - this ties a listener to the backend. This function
should be called as listener(kwargs) when the backend calls emit. should be called as listener(kwargs) when the backend calls emit.
- off(cmdname) - remove the listener for this cmdname. - off(cmdname) - remove the listener for this cmdname.
*/ */
(function() { (function() {
@ -63,7 +67,7 @@ An "emitter" object must have a function
// will use a default emitter. Must have // will use a default emitter. Must have
// an "emit" function. // an "emit" function.
// connection - This defaults to using either // connection - This defaults to using either
// a WebsocketConnection or a CometConnection // a WebsocketConnection or a AjaxCometConnection
// depending on what the browser supports. If given // depending on what the browser supports. If given
// it must have a 'msg' method and make use of // it must have a 'msg' method and make use of
// Evennia.emit to return data to Client. // Evennia.emit to return data to Client.
@ -102,6 +106,9 @@ An "emitter" object must have a function
// value from the backend. // value from the backend.
// //
msg: function (cmdname, args, kwargs, callback) { msg: function (cmdname, args, kwargs, callback) {
if (!cmdname) {
return;
}
kwargs.cmdid = cmdid++; kwargs.cmdid = cmdid++;
var outargs = args ? args : []; var outargs = args ? args : [];
var outkwargs = kwargs ? kwargs : {}; var outkwargs = kwargs ? kwargs : {};
@ -110,7 +117,6 @@ An "emitter" object must have a function
if (typeof callback === 'function') { if (typeof callback === 'function') {
cmdmap[cmdid] = callback; cmdmap[cmdid] = callback;
} }
log('client msg sending: ', data);
this.connection.msg(data); this.connection.msg(data);
}, },
@ -139,7 +145,8 @@ An "emitter" object must have a function
// Basic emitter to distribute data being sent to the client from // Basic emitter to distribute data being sent to the client from
// the Server. An alternative can be overridden in Evennia.init. // the Server. An alternative can be overridden by giving it
// in Evennia.init({emitter:myemitter})
// //
var DefaultEmitter = function () { var DefaultEmitter = function () {
var listeners = {}; var listeners = {};
@ -155,7 +162,6 @@ An "emitter" object must have a function
// kwargs (obj): Argument to the listener. // kwargs (obj): Argument to the listener.
// //
var emit = function (cmdname, args, kwargs) { var emit = function (cmdname, args, kwargs) {
log("DefaultEmitter.emit:", cmdname, args, kwargs);
if (listeners[cmdname]) { if (listeners[cmdname]) {
listeners[cmdname].apply(this, [args, kwargs]); listeners[cmdname].apply(this, [args, kwargs]);
} }
@ -172,7 +178,6 @@ An "emitter" object must have a function
// to listen to cmdname events. // to listen to cmdname events.
// //
var on = function (cmdname, listener) { var on = function (cmdname, listener) {
log("DefaultEmitter.on", cmdname, listener);
if (typeof(listener === 'function')) { if (typeof(listener === 'function')) {
listeners[cmdname] = listener; listeners[cmdname] = listener;
}; };
@ -192,28 +197,25 @@ An "emitter" object must have a function
// Websocket Connector // Websocket Connector
// //
var WebsocketConnection = function () { var WebsocketConnection = function () {
log("Trying websocket"); log("Trying websocket ...");
var websocket = new WebSocket(wsurl); var websocket = new WebSocket(wsurl);
// Handle Websocket open event // Handle Websocket open event
websocket.onopen = function (event) { websocket.onopen = function (event) {
log('Websocket connection openened. ', event); Evennia.emit('connection.open', ["websocket"], event);
Evennia.emit('socket:open', [], event);
}; };
// Handle Websocket close event // Handle Websocket close event
websocket.onclose = function (event) { websocket.onclose = function (event) {
log('WebSocket connection closed.'); Evennia.emit('connection.close', ["websocket"], event);
Evennia.emit('socket:close', [], event);
}; };
// Handle websocket errors // Handle websocket errors
websocket.onerror = function (event) { websocket.onerror = function (event) {
log("Websocket error to ", wsurl, event); Evennia.emit('connection.error', ["websocket"], event);
Evennia.emit('socket:error', [], event);
if (websocket.readyState === websocket.CLOSED) { if (websocket.readyState === websocket.CLOSED) {
log("Websocket failed. Falling back to Ajax..."); log("Websocket failed. Falling back to Ajax...");
Evennia.connection = AjaxCometConnection(); Evennia.connection = AjaxCometConnection();
} }
}; };
// Handle incoming websocket data [cmdname, kwargs] // Handle incoming websocket data [cmdname, args, kwargs]
websocket.onmessage = function (event) { websocket.onmessage = function (event) {
var data = event.data; var data = event.data;
if (typeof data !== 'string' && data.length < 0) { if (typeof data !== 'string' && data.length < 0) {
@ -222,14 +224,15 @@ An "emitter" object must have a function
// Parse the incoming data, send to emitter // Parse the incoming data, send to emitter
// Incoming data is on the form [cmdname, args, kwargs] // Incoming data is on the form [cmdname, args, kwargs]
data = JSON.parse(data); data = JSON.parse(data);
log("incoming " + data);
Evennia.emit(data[0], data[1], data[2]); Evennia.emit(data[0], data[1], data[2]);
}; };
websocket.msg = function(data) { websocket.msg = function(data) {
// send data across the wire. Make sure to json it. // send data across the wire. Make sure to json it.
websocket.send(JSON.stringify(data)); websocket.send(JSON.stringify(data));
}; };
websocket.close = function() {
// close connection.
}
return websocket; return websocket;
}; };
@ -238,15 +241,38 @@ An "emitter" object must have a function
AjaxCometConnection = function() { AjaxCometConnection = function() {
log("Trying ajax ..."); log("Trying ajax ...");
var client_hash = '0'; var client_hash = '0';
// Send Client -> Evennia. Called by Evennia.send.
var msg = function(cmdname, args, kwargs) { // initialize connection and get hash
var init = function() {
$.ajax({type: "POST", url: "/webclientdata",
async: true, cache: false, timeout: 50000,
datatype: "json",
data: {mode: "init", suid: client_hash},
success: function(data) {
data = JSON.parse(data);
log ("connection.open", ["AJAX/COMET"], data);
client_hash = data.suid;
poll();
},
error: function(req, stat, err) {
Evennia.emit("connection.error", ["AJAX/COMET init error"], err);
log("AJAX/COMET: Connection error: " + err);
}
});
};
// Send Client -> Evennia. Called by Evennia.msg
var msg = function(data) {
log("AJAX.msg:", data);
$.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', msg: [cmdname, args, kwargs], 'suid': client_hash}, data: {mode:'input', data: JSON.stringify(data), 'suid': client_hash},
success: function(data) {}, success: function(req, stat, err) {},
error: function(req, stat, err) { error: function(req, stat, err) {
log("COMET: Server returned error. " + err); Evennia.emit("connection.error", ["AJAX/COMET send error"], err);
log("AJAX/COMET: Server returned error.",req,stat,err);
} }
}); });
}; };
@ -261,40 +287,52 @@ 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) {
Evennia.emit(data[0], data[1], data[2]) Evennia.emit(data[0], data[1], data[2]);
log("AJAX/COMET: Evennia->client", data);
poll(); // immiately start a new request
}, },
error: function() { error: function(req, stat, err) {
poll() // timeout; immediately re-poll poll() // timeout; immediately re-poll
// don't trigger an emit event here,
// this is normal for ajax/comet
} }
}); });
}; };
// Initialization will happen when this Connection is created. // Kill the connection and do house cleaning on the server.
// We need to store the client id so Evennia knows to separate var close = function webclient_close(){
// the clients. $.ajax({
$.ajax({type: "POST", url: "/webclientdata", type: "POST",
async: true, cache: false, timeout: 50000, url: "/webclientdata",
datatype: "json", async: false,
success: function(data) { cache: false,
client_hash = data.suid; timeout: 50000,
poll(); dataType: "json",
data: {mode: 'close', 'suid': client_hash},
success: function(data){
client_hash = '0';
Evennia.emit("connection.close", ["AJAX/COMET"], {});
log("AJAX/COMET connection closed cleanly.")
}, },
error: function(req, stat, err) { error: function(req, stat, err){
log("Connection error: " + err); Evennia.emit("connection.err", ["AJAX/COMET close error"], err);
client_hash = '0';
} }
}); });
};
return {msg: msg, poll: poll}; // init
init();
return {msg: msg, poll: poll, close: close};
}; };
window.Evennia = Evennia; window.Evennia = Evennia;
})(); // end of auto-calling Evennia object defintion })(); // end of auto-calling Evennia object defintion
// helper logging function // helper logging function (requires a js dev-console in the browser)
// Args:
// msg (str): Message to log to console.
//
function log() { function log() {
if (Evennia.debug) { if (Evennia.debug) {
console.log(JSON.stringify(arguments)); console.log(JSON.stringify(arguments));
@ -304,6 +342,8 @@ function log() {
// Called when page has finished loading (kicks the client into gear) // Called when page has finished loading (kicks the client into gear)
$(document).ready(function() { $(document).ready(function() {
setTimeout( function () { setTimeout( function () {
// the short timeout supposedly causes the load indicator
// in Chrome to stop spinning
Evennia.init() Evennia.init()
}, },
500 500

View file

@ -60,7 +60,6 @@ function doSendText() {
outtext = inputfield.val(); outtext = inputfield.val();
input_history.add(outtext); input_history.add(outtext);
inputfield.val(""); inputfield.val("");
log("sending outtext", outtext);
Evennia.msg("text", [outtext], {}); Evennia.msg("text", [outtext], {});
} }
@ -112,7 +111,7 @@ function onDefault(cmdname, args, kwargs) {
mwin = $("#messagewindow"); mwin = $("#messagewindow");
mwin.append( mwin.append(
"<div class='msg err'>" "<div class='msg err'>"
+ "Received unknown server command:<br>" + "Unhandled event:<br>"
+ cmdname + ", " + cmdname + ", "
+ JSON.stringify(args) + ", " + JSON.stringify(args) + ", "
+ JSON.stringify(kwargs) + "<p></div>"); + JSON.stringify(kwargs) + "<p></div>");
@ -136,7 +135,6 @@ $(document).ready(function() {
// initialize once. // initialize once.
Evennia.init(); Evennia.init();
// register listeners // register listeners
log("register listeners ...");
Evennia.emitter.on("text", onText); Evennia.emitter.on("text", onText);
Evennia.emitter.on("prompt", onPrompt); Evennia.emitter.on("prompt", onPrompt);
Evennia.emitter.on("default", onDefault); Evennia.emitter.on("default", onDefault);
@ -145,7 +143,6 @@ $(document).ready(function() {
// set an idle timer to send idle every 3 minutes, // set an idle timer to send idle every 3 minutes,
// to avoid proxy servers timing out on us // to avoid proxy servers timing out on us
setInterval(function() { setInterval(function() {
log('Idle tick.');
Evennia.msg("text", ["idle"], {}); Evennia.msg("text", ["idle"], {});
}, },
60000*3 60000*3

View file

@ -15,12 +15,6 @@ def webclient(request):
Webclient page template loading. Webclient page template loading.
""" """
# analyze request to find which port we are on
if int(request.META["SERVER_PORT"]) == 8000:
# we relay webclient to the portal port
print("Called from port 8000!")
#return redirect("http://localhost:8001/webclient/", permanent=True)
nsess = len(PlayerDB.objects.get_connected_players()) or "none" nsess = len(PlayerDB.objects.get_connected_players()) or "none"
# as an example we send the number of connected players to the template # as an example we send the number of connected players to the template
pagevars = {'num_players_connected': nsess} pagevars = {'num_players_connected': nsess}