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]
# access object
INPUT_HANDLER = OOBHandler()
MONITOR_HANDLER = OOBHandler()

View file

@ -93,7 +93,7 @@ class WebSocketClient(Protocol, Session):
cmdarray = json.loads(string)
print "dataReceived:", 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):
"""
@ -147,6 +147,8 @@ class WebSocketClient(Protocol, Session):
text = args[0]
if text is None:
return
text = to_str(text, force_string=True)
options = kwargs.get("options", {})
raw = options.get("raw", False)
nomarkup = options.get("nomarkup", False)
@ -186,5 +188,5 @@ class WebSocketClient(Protocol, Session):
"""
if not cmdname == "options":
print "send_default", cmdname, args, kwargs
print "websocket.send_default", 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
on twisted and django. They are both a part of the Evennia
website url tree (so the testing website might be located
on http://localhost:8000/, whereas the webclient can be
found on http://localhost:8000/webclient.)
The AJAX/COMET web client consists of two components running on
twisted and django. They are both a part of the Evennia website url
tree (so the testing website might be located on
http://localhost:8000/, whereas the webclient can be found on
http://localhost:8000/webclient.)
/webclient - this url is handled through django's template
system and serves the html page for the client
@ -79,30 +79,26 @@ class WebClient(resource.Resource):
except KeyError:
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
as soon as possible.
Args:
suid (int): Session id.
string (str): The text to send.
data (dict): Optional data.
Notes:
The `data` keyword is deprecated.
data (list): A send structure [cmdname, [args], {kwargs}].
"""
request = self.requests.get(suid)
if request:
# we have a request waiting. Return immediately.
request.write(jsonify({'msg': string, 'data': data}))
request.write(jsonify(data))
request.finish()
del self.requests[suid]
else:
# no waiting request. Store data in buffer
dataentries = self.databuffer.get(suid, [])
dataentries.append(jsonify({'msg': string, 'data': data}))
dataentries.append(jsonify(data))
self.databuffer[suid] = dataentries
def client_disconnect(self, suid):
@ -128,9 +124,6 @@ class WebClient(resource.Resource):
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]
remote_addr = request.getClientIP()
@ -158,14 +151,13 @@ class WebClient(resource.Resource):
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
return '""'
sess = self.sessionhandler.session_from_suid(suid)
if sess:
sess = sess[0]
text = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
sess.sessionhandler.data_in(sess, text, data=data)
return ''
cmdarray = json.loads(request.args.get('data')[0])
sess.sessionhandler.data_in(sess, **{cmdarray[0]:[cmdarray[1], cmdarray[2]]})
return '""'
def mode_receive(self, request):
"""
@ -180,7 +172,7 @@ class WebClient(resource.Resource):
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
return '""'
dataentries = self.databuffer.get(suid, [])
if dataentries:
@ -210,7 +202,7 @@ class WebClient(resource.Resource):
except IndexError:
self.client_disconnect(suid)
pass
return ''
return '""'
def render_POST(self, request):
"""
@ -240,7 +232,7 @@ class WebClient(resource.Resource):
return self.mode_close(request)
else:
# 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.
"""
if reason:
self.client.lineSend(self.suid, reason)
self.client.lineSend(self.suid, ["text", [reason], {}])
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:
raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
nomarkup (bool): Clean out all ansi/html markers and tokens.
"""
# string handling is similar to telnet
try:
text = utils.to_str(text if text else "", encoding=self.encoding)
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
if raw:
self.client.lineSend(self.suid, text)
else:
self.client.lineSend(self.suid,
parse_html(text, strip_ansi=nomarkup))
return
except Exception:
logger.log_trace()
if args:
args = list(args)
text = args[0]
if text is None:
return
text = utils.to_str(text, force_string=True)
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:
mode = f.read()
if mode in ('True', 'reload'):
from evennia.server.oobhandler import OOB_HANDLER
OOB_HANDLER.restore()
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.restore()
from evennia.scripts.tickerhandler import TICKER_HANDLER
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 self.sessions.all_sessions_portal_sync()
self.at_server_reload_stop()
# only save OOB state on reload, not on shutdown/reset
from evennia.server.oobhandler import OOB_HANDLER
OOB_HANDLER.save()
# only save monitor state on reload, not on shutdown/reset
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.save()
else:
if mode == 'reset':
# 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
evennia/server/portal/webclient_ajax.py)
All messages is a valid JSON array on single form: ["cmdname",
kwargs], where kwargs is a JSON object that will be used as argument
to call the cmdname function.
All messages is a valid JSON array on single form:
["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
following official functions:
- Evennia.init(options)
This can be called by the frontend to intialize the library. The
argument is an js object with the following possible keys:
This stores the connections/emitters and creates the websocket/ajax connection.
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
can also be set to Evennia.CometConnection for backwards
compatibility. See below.
@ -34,15 +38,15 @@ following official functions:
A "connection" object must have the method
- msg(data) - this should relay data to the Server. This function should itself handle
the conversion to JSON before sending across the wire.
- When receiving data from the Server (always [cmdname, kwargs]), this must be
JSON-unpacked and the result redirected to Evennia.emit(data[0], data[1]).
- 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], data[2]).
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:
- on(cmdname, listener) - this ties a listener to the backend. This function
should be called as listener(kwargs) when the backend calls emit.
- off(cmdname) - remove the listener for this cmdname.
*/
(function() {
@ -63,7 +67,7 @@ An "emitter" object must have a function
// will use a default emitter. Must have
// an "emit" function.
// connection - This defaults to using either
// a WebsocketConnection or a CometConnection
// a WebsocketConnection or a AjaxCometConnection
// depending on what the browser supports. If given
// it must have a 'msg' method and make use of
// Evennia.emit to return data to Client.
@ -102,6 +106,9 @@ An "emitter" object must have a function
// value from the backend.
//
msg: function (cmdname, args, kwargs, callback) {
if (!cmdname) {
return;
}
kwargs.cmdid = cmdid++;
var outargs = args ? args : [];
var outkwargs = kwargs ? kwargs : {};
@ -110,7 +117,6 @@ An "emitter" object must have a function
if (typeof callback === 'function') {
cmdmap[cmdid] = callback;
}
log('client msg sending: ', 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
// 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 listeners = {};
@ -155,7 +162,6 @@ An "emitter" object must have a function
// kwargs (obj): Argument to the listener.
//
var emit = function (cmdname, args, kwargs) {
log("DefaultEmitter.emit:", cmdname, args, kwargs);
if (listeners[cmdname]) {
listeners[cmdname].apply(this, [args, kwargs]);
}
@ -172,7 +178,6 @@ An "emitter" object must have a function
// to listen to cmdname events.
//
var on = function (cmdname, listener) {
log("DefaultEmitter.on", cmdname, listener);
if (typeof(listener === 'function')) {
listeners[cmdname] = listener;
};
@ -192,28 +197,25 @@ An "emitter" object must have a function
// Websocket Connector
//
var WebsocketConnection = function () {
log("Trying websocket");
log("Trying websocket ...");
var websocket = new WebSocket(wsurl);
// Handle Websocket open event
websocket.onopen = function (event) {
log('Websocket connection openened. ', event);
Evennia.emit('socket:open', [], event);
Evennia.emit('connection.open', ["websocket"], event);
};
// Handle Websocket close event
websocket.onclose = function (event) {
log('WebSocket connection closed.');
Evennia.emit('socket:close', [], event);
Evennia.emit('connection.close', ["websocket"], event);
};
// Handle websocket errors
websocket.onerror = function (event) {
log("Websocket error to ", wsurl, event);
Evennia.emit('socket:error', [], event);
Evennia.emit('connection.error', ["websocket"], event);
if (websocket.readyState === websocket.CLOSED) {
log("Websocket failed. Falling back to Ajax...");
Evennia.connection = AjaxCometConnection();
}
};
// Handle incoming websocket data [cmdname, kwargs]
// Handle incoming websocket data [cmdname, args, kwargs]
websocket.onmessage = function (event) {
var data = event.data;
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
// Incoming data is on the form [cmdname, args, kwargs]
data = JSON.parse(data);
log("incoming " + data);
Evennia.emit(data[0], data[1], data[2]);
};
websocket.msg = function(data) {
// send data across the wire. Make sure to json it.
websocket.send(JSON.stringify(data));
};
websocket.close = function() {
// close connection.
}
return websocket;
};
@ -238,15 +241,38 @@ An "emitter" object must have a function
AjaxCometConnection = function() {
log("Trying ajax ...");
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",
async: true, cache: false, timeout: 30000,
dataType: "json",
data: {mode:'input', msg: [cmdname, args, kwargs], 'suid': client_hash},
success: function(data) {},
data: {mode:'input', data: JSON.stringify(data), 'suid': client_hash},
success: 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",
data: {mode: 'receive', 'suid': client_hash},
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
// don't trigger an emit event here,
// this is normal for ajax/comet
}
});
};
// Initialization will happen when this Connection is created.
// We need to store the client id so Evennia knows to separate
// the clients.
$.ajax({type: "POST", url: "/webclientdata",
async: true, cache: false, timeout: 50000,
datatype: "json",
success: function(data) {
client_hash = data.suid;
poll();
},
error: function(req, stat, err) {
log("Connection error: " + err);
}
});
// Kill the connection and do house cleaning on the server.
var close = function webclient_close(){
$.ajax({
type: "POST",
url: "/webclientdata",
async: false,
cache: false,
timeout: 50000,
dataType: "json",
data: {mode: 'close', 'suid': client_hash},
return {msg: msg, poll: poll};
success: function(data){
client_hash = '0';
Evennia.emit("connection.close", ["AJAX/COMET"], {});
log("AJAX/COMET connection closed cleanly.")
},
error: function(req, stat, err){
Evennia.emit("connection.err", ["AJAX/COMET close error"], err);
client_hash = '0';
}
});
};
// init
init();
return {msg: msg, poll: poll, close: close};
};
window.Evennia = Evennia;
})(); // end of auto-calling Evennia object defintion
// helper logging function
// Args:
// msg (str): Message to log to console.
//
// helper logging function (requires a js dev-console in the browser)
function log() {
if (Evennia.debug) {
console.log(JSON.stringify(arguments));
@ -304,6 +342,8 @@ function log() {
// Called when page has finished loading (kicks the client into gear)
$(document).ready(function() {
setTimeout( function () {
// the short timeout supposedly causes the load indicator
// in Chrome to stop spinning
Evennia.init()
},
500

View file

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

View file

@ -15,12 +15,6 @@ def webclient(request):
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"
# as an example we send the number of connected players to the template
pagevars = {'num_players_connected': nsess}