diff --git a/evennia/scripts/monitorhandler.py b/evennia/scripts/monitorhandler.py index b8eb7cadc3..fe7f888411 100644 --- a/evennia/scripts/monitorhandler.py +++ b/evennia/scripts/monitorhandler.py @@ -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() diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index eeaa57b601..5b8f716bdb 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -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])) diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py index afe432e8e7..2a2759fc5c 100644 --- a/evennia/server/portal/webclient_ajax.py +++ b/evennia/server/portal/webclient_ajax.py @@ -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]) diff --git a/evennia/server/server.py b/evennia/server/server.py index 0ccc0962f4..e80f9eaad3 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -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 diff --git a/evennia/web/webclient/static/webclient/js/evennia.js b/evennia/web/webclient/static/webclient/js/evennia.js index bb180e7fc6..d41b3fc81a 100644 --- a/evennia/web/webclient/static/webclient/js/evennia.js +++ b/evennia/web/webclient/static/webclient/js/evennia.js @@ -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 diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index da208990ce..d43562a0ee 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -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( "
" - + "Received unknown server command:
" + + "Unhandled event:
" + cmdname + ", " + JSON.stringify(args) + ", " + JSON.stringify(kwargs) + "

"); @@ -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 diff --git a/evennia/web/webclient/views.py b/evennia/web/webclient/views.py index 6e1a5611bb..9b011fafe9 100644 --- a/evennia/web/webclient/views.py +++ b/evennia/web/webclient/views.py @@ -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}