diff --git a/evennia/server/caches.py b/evennia/server/caches.py deleted file mode 100644 index 1bc10b5957..0000000000 --- a/evennia/server/caches.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Central caching module. - -""" - -from sys import getsizeof -import os -import threading -from collections import defaultdict - -from evennia.server.models import ServerConfig -from evennia.utils.utils import uses_database, to_str, get_evennia_pids - -_GA = object.__getattribute__ -_SA = object.__setattr__ -_DA = object.__delattr__ - -_IS_SUBPROCESS = os.getpid() in get_evennia_pids() -_IS_MAIN_THREAD = threading.currentThread().getName() == "MainThread" - -# -# Set up the cache stores -# - -_ATTR_CACHE = {} -_PROP_CACHE = defaultdict(dict) - -#------------------------------------------------------------ -# Cache key hash generation -#------------------------------------------------------------ - -if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4': - # mysql <5.6.4 don't support millisecond precision - _DATESTRING = "%Y:%m:%d-%H:%M:%S:000000" -else: - _DATESTRING = "%Y:%m:%d-%H:%M:%S:%f" - - -def hashid(obj, suffix=""): - """ - Returns a per-class unique hash that combines the object's - class name with its idnum and creation time. This makes this id unique also - between different typeclassed entities such as scripts and - objects (which may still have the same id). - """ - if not obj: - return obj - try: - hid = _GA(obj, "_hashid") - except AttributeError: - try: - date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id") - except AttributeError: - try: - # maybe a typeclass, try to go to dbobj - obj = _GA(obj, "dbobj") - date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id") - except AttributeError: - # this happens if hashing something like ndb. We have to - # rely on memory adressing in this case. - date, idnum = "InMemory", id(obj) - if not idnum or not date: - # this will happen if setting properties on an object which - # is not yet saved - return None - # we have to remove the class-name's space, for eventual use - # of memcached - hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum) - hid = hid.replace(" ", "") - # we cache the object part of the hashid to avoid too many - # object lookups - _SA(obj, "_hashid", hid) - # build the complete hashid - hid = "%s%s" % (hid, suffix) - return to_str(hid) - - -#------------------------------------------------------------ -# Cache callback handlers -#------------------------------------------------------------ - -# callback to field pre_save signal (connected in evennia.server.server) -#def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs): -# """ -# Called at the beginning of the field save operation. The save method -# must be called with the update_fields keyword in order to be most efficient. -# This method should NOT save; rather it is the save() that triggers this -# function. Its main purpose is to allow to plug-in a save handler and oob -# handlers. -# """ -# if raw: -# return -# if update_fields: -# # this is a list of strings at this point. We want field objects -# update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) -# else: -# # meta.fields are already field objects; get them all -# update_fields = _GA(_GA(instance, "_meta"), "fields") -# for field in update_fields: -# fieldname = field.name -# handlername = "_at_%s_presave" % fieldname -# handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None -# if callable(handler): -# handler() - - -def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs): - """ - Called at the beginning of the field save operation. The save method - must be called with the update_fields keyword in order to be most efficient. - This method should NOT save; rather it is the save() that triggers this - function. Its main purpose is to allow to plug-in a save handler and oob - handlers. - """ - if raw: - return - if update_fields: - # this is a list of strings at this point. We want field objects - update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) - else: - # meta.fields are already field objects; get them all - update_fields = _GA(_GA(instance, "_meta"), "fields") - for field in update_fields: - fieldname = field.name - handlername = "_at_%s_postsave" % fieldname - handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None - if callable(handler): - handler() - trackerhandler = _GA(instance, "_trackerhandler") if "_trackerhandler" in _GA(instance, '__dict__') else None - if trackerhandler: - trackerhandler.update(fieldname, _GA(instance, fieldname)) - -#------------------------------------------------------------ -# Attribute lookup cache -#------------------------------------------------------------ - -def get_attr_cache(obj): - "Retrieve lookup cache" - hid = hashid(obj) - return _ATTR_CACHE.get(hid, None) - - -def set_attr_cache(obj, store): - "Set lookup cache" - global _ATTR_CACHE - hid = hashid(obj) - _ATTR_CACHE[hid] = store - -#------------------------------------------------------------ -# Property cache - this is a generic cache for properties stored on models. -#------------------------------------------------------------ - -# access methods - -def get_prop_cache(obj, propname): - "retrieve data from cache" - hid = hashid(obj, "-%s" % propname) - return _PROP_CACHE[hid].get(propname, None) if hid else None - - -def set_prop_cache(obj, propname, propvalue): - "Set property cache" - hid = hashid(obj, "-%s" % propname) - if hid: - _PROP_CACHE[hid][propname] = propvalue - - -def del_prop_cache(obj, propname): - "Delete element from property cache" - hid = hashid(obj, "-%s" % propname) - if hid: - if propname in _PROP_CACHE[hid]: - del _PROP_CACHE[hid][propname] - - -def flush_prop_cache(): - "Clear property cache" - global _PROP_CACHE - _PROP_CACHE = defaultdict(dict) - - -def get_cache_sizes(): - """ - Get cache sizes, expressed in number of objects and memory size in MB - """ - global _ATTR_CACHE, _PROP_CACHE - attr_n = len(_ATTR_CACHE) - attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0 - prop_n = sum(len(dic) for dic in _PROP_CACHE.values()) - prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0 - return (attr_n, attr_mb), (prop_n, prop_mb) - - diff --git a/evennia/server/oob_cmds.py b/evennia/server/oob_cmds.py index 42a762c3bf..1bee54cd2d 100644 --- a/evennia/server/oob_cmds.py +++ b/evennia/server/oob_cmds.py @@ -5,26 +5,40 @@ This module implements commands as defined by the MSDP standard (http://tintin.sourceforge.net/msdp/), but is independent of the actual transfer protocol (webclient, MSDP, GMCP etc). -This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions -(not classes) defined globally in this module will be made available -to the oob mechanism. +This module is pointed to by settings.OOB_PLUGIN_MODULES. It must +contain a global dictionary CMD_MAP which is a dictionary that maps +the call available in the OOB call to a function in this module. + +For example, if the OOB strings received looks like this: + + MDSP.LISTEN [desc, key] # GMCP (wrapping to MSDP) + LISTEN ARRAY VAL desc VAL key # MSDP + +and CMD_MAP = {"LISTEN", listen} then this would result in a call to a +function "listen" in this module, with the arguments *("desc", "key"). oob functions have the following call signature: + function(oobhandler, session, *args, **kwargs) -where oobhandler is a back-reference to the central OOB_HANDLER -instance and session is the active session to get return data. +here, oobhandler always holds a back-reference to the central oob +handler, session is the active session and *args, **kwargs are what +is sent from the oob call. -The function names are not case-sensitive (this allows for names -like "LIST" which would otherwise collide with Python builtins). - -A function named OOB_ERROR will retrieve error strings if it is +A function called with OOB_ERROR will retrieve error strings if it is defined. It will get the error message as its 3rd argument. -Data is usually returned via - session.msg(oob=(cmdname, (args,), {kwargs})) -Note that args, kwargs must be iterable/dict, non-iterables will -be interpreted as a new command name. + oob_error(oobhandler, session, error, *args, **kwargs) + +This allows for customizing error handling. + +Data is usually returned to the user via a return OOB call: + + session.msg(oob=(oobcmdname, (args,), {kwargs})) + +oobcmdnames (like "MSDP.LISTEN" / "LISTEN" above) are case-sensitive. Note that args, +kwargs must be iterable. Non-iterables will be interpreted as a new +command name (you can send multiple oob commands with one msg() call)) """ @@ -33,12 +47,14 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _NA_SEND = lambda o: "N/A" + #------------------------------------------------------------ # All OOB commands must be on the form # cmdname(oobhandler, session, *args, **kwargs) #------------------------------------------------------------ -def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs): + +def oob_error(oobhandler, session, errmsg, *args, **kwargs): """ A function with this name is special and is called by the oobhandler when an error occurs already at the execution stage (such as the oob function @@ -46,13 +62,14 @@ def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs): """ session.msg(oob=("err", ("ERROR " + errmsg,))) - -def ECHO(oobhandler, session, *args, **kwargs): +def oob_echo(oobhandler, session, *args, **kwargs): "Test/debug function, simply returning the args and kwargs" session.msg(oob=("echo", args, kwargs)) -##OOB{"SEND":"CHARACTER_NAME"} -def SEND(oobhandler, session, *args, **kwargs): +# MSDP standard commands + +##OOB{"SEND":"CHARACTER_NAME"} - from webclient +def oob_send(oobhandler, session, *args, **kwargs): """ This function directly returns the value of the given variable to the session. @@ -71,7 +88,7 @@ def SEND(oobhandler, session, *args, **kwargs): session.msg(oob=("err", ("You must log in first.",))) ##OOB{"REPORT":"TEST"} -def REPORT(oobhandler, session, *args, **kwargs): +def oob_report(oobhandler, session, *args, **kwargs): """ This creates a tracker instance to track the data given in *args. @@ -99,7 +116,7 @@ def REPORT(oobhandler, session, *args, **kwargs): ##OOB{"UNREPORT": "TEST"} -def UNREPORT(oobhandler, session, *args, **kwargs): +def oob_unreport(oobhandler, session, *args, **kwargs): """ This removes tracking for the given data given in *args. """ @@ -118,7 +135,7 @@ def UNREPORT(oobhandler, session, *args, **kwargs): ##OOB{"LIST":"COMMANDS"} -def LIST(oobhandler, session, mode, *args, **kwargs): +def oob_list(oobhandler, session, mode, *args, **kwargs): """ List available properties. Mode is the type of information desired: @@ -172,7 +189,7 @@ def _repeat_callback(oobhandler, session, *args, **kwargs): session.msg(oob=("repeat", ("Repeat!",))) ##OOB{"REPEAT":10} -def REPEAT(oobhandler, session, interval, *args, **kwargs): +def oob_repeat(oobhandler, session, interval, *args, **kwargs): """ Test command for the repeat functionality. Note that the args/kwargs must not be db objects (or anything else non-picklable), rather use @@ -184,7 +201,7 @@ def REPEAT(oobhandler, session, interval, *args, **kwargs): ##OOB{"UNREPEAT":10} -def UNREPEAT(oobhandler, session, interval): +def oob_unrepeat(oobhandler, session, interval): """ Disable repeating callback """ @@ -219,3 +236,16 @@ OOB_REPORTABLE = { "ROOM_NAME": "db_location", "TEST" : "test" } + + +# this maps the commands to the names available to use from +# the oob call +CMD_MAP = {"OOB_ERROR": oob_error, # will get error messages + "SEND": oob_send, + "ECHO": oob_echo, + "REPORT": oob_report, + "UNREPORT": oob_unreport, + "LIST": oob_list, + "REPEAT": oob_repeat, + "UNREPEAT": oob_unrepeat} + diff --git a/evennia/server/portal/msdp.py b/evennia/server/portal/msdp.py deleted file mode 100644 index f155b47d26..0000000000 --- a/evennia/server/portal/msdp.py +++ /dev/null @@ -1,243 +0,0 @@ -""" - -MSDP (Mud Server Data Protocol) - -This implements the MSDP protocol as per -http://tintin.sourceforge.net/msdp/. MSDP manages out-of-band -communication between the client and server, for updating health bars -etc. - -""" -import re -from evennia.utils.utils import to_str - -# MSDP-relevant telnet cmd/opt-codes -MSDP = chr(69) -MSDP_VAR = chr(1) -MSDP_VAL = chr(2) -MSDP_TABLE_OPEN = chr(3) -MSDP_TABLE_CLOSE = chr(4) -MSDP_ARRAY_OPEN = chr(5) -MSDP_ARRAY_CLOSE = chr(6) - -IAC = chr(255) -SB = chr(250) -SE = chr(240) - -force_str = lambda inp: to_str(inp, force_string=True) - -# pre-compiled regexes -# returns 2-tuple -regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, - MSDP_ARRAY_OPEN, - MSDP_ARRAY_CLOSE)) -# returns 2-tuple (may be nested) -regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, - MSDP_TABLE_OPEN, - MSDP_TABLE_CLOSE)) -regex_var = re.compile(MSDP_VAR) -regex_val = re.compile(MSDP_VAL) - - -# Msdp object handler - -class Msdp(object): - """ - Implements the MSDP protocol. - """ - - def __init__(self, protocol): - """ - Initiates by storing the protocol - on itself and trying to determine - if the client supports MSDP. - """ - self.protocol = protocol - self.protocol.protocol_flags['MSDP'] = False - self.protocol.negotiationMap[MSDP] = self.msdp_to_evennia - self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp) - self.msdp_reported = {} - - def no_msdp(self, option): - "No msdp supported or wanted" - self.protocol.handshake_done() - - def do_msdp(self, option): - """ - Called when client confirms that it can do MSDP. - """ - self.protocol.protocol_flags['MSDP'] = True - self.protocol.handshake_done() - - def evennia_to_msdp(self, cmdname, *args, **kwargs): - """ - handle return data from cmdname by converting it to - a proper msdp structure. data can either be a single value (will be - converted to a string), a list (will be converted to an MSDP_ARRAY), - or a dictionary (will be converted to MSDP_TABLE). - - OBS - there is no actual use of arrays and tables in the MSDP - specification or default commands -- are returns are implemented - as simple lists or named lists (our name for them here, these - un-bounded structures are not named in the specification). So for - now, this routine will not explicitly create arrays nor tables, - although there are helper methods ready should it be needed in - the future. - """ - - def make_table(name, **kwargs): - "build a table that may be nested with other tables or arrays." - string = MSDP_VAR + force_str(name) + MSDP_VAL + MSDP_TABLE_OPEN - for key, val in kwargs.items(): - if isinstance(val, dict): - string += make_table(string, key, **val) - elif hasattr(val, '__iter__'): - string += make_array(string, key, *val) - else: - string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) - string += MSDP_TABLE_CLOSE - return string - - def make_array(name, *args): - "build a array. Arrays may not nest tables by definition." - string = MSDP_VAR + force_str(name) + MSDP_ARRAY_OPEN - string += MSDP_VAL.join(force_str(arg) for arg in args) - string += MSDP_ARRAY_CLOSE - return string - - def make_list(name, *args): - "build a simple list - an array without start/end markers" - string = MSDP_VAR + force_str(name) - string += MSDP_VAL.join(force_str(arg) for arg in args) - return string - - def make_named_list(name, **kwargs): - "build a named list - a table without start/end markers" - string = MSDP_VAR + force_str(name) - for key, val in kwargs.items(): - string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) - return string - - # Default MSDP commands - - print "MSDP outgoing:", cmdname, args, kwargs - - cupper = cmdname.upper() - if cupper == "LIST": - if args: - args = list(args) - mode = args.pop(0).upper() - self.data_out(make_array(mode, *args)) - elif cupper == "REPORT": - self.data_out(make_list("REPORT", *args)) - elif cupper == "UNREPORT": - self.data_out(make_list("UNREPORT", *args)) - elif cupper == "RESET": - self.data_out(make_list("RESET", *args)) - elif cupper == "SEND": - self.data_out(make_named_list("SEND", **kwargs)) - else: - # return list or named lists. - msdp_string = "" - if args: - msdp_string += make_list(cupper, *args) - if kwargs: - msdp_string += make_named_list(cupper, **kwargs) - self.data_out(msdp_string) - - def msdp_to_evennia(self, data): - """ - Handle a client's requested negotiation, converting - it into a function mapping - either one of the MSDP - default functions (LIST, SEND etc) or a custom one - in OOB_FUNCS dictionary. command names are case-insensitive. - - varname, var --> mapped to function varname(var) - arrayname, array --> mapped to function arrayname(*array) - tablename, table --> mapped to function tablename(**table) - - Note: Combinations of args/kwargs to one function is not supported - in this implementation (it complicates the code for limited - gain - arrayname(*array) is usually as complex as anyone should - ever need to go anyway (I hope!). - - """ - tables = {} - arrays = {} - variables = {} - - if hasattr(data, "__iter__"): - data = "".join(data) - - #logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data) - - for key, table in regex_table.findall(data): - tables[key] = {} - for varval in regex_var.split(table): - parts = regex_val.split(varval) - tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)}) - for key, array in regex_array.findall(data): - arrays[key] = [] - for val in regex_val.split(array): - arrays[key].append(val) - arrays[key] = tuple(arrays[key]) - for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))): - # get remaining varvals after cleaning away tables/arrays - parts = regex_val.split(varval) - variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", ) - - #print "MSDP: table, array, variables:", tables, arrays, variables - - # all variables sent through msdp to Evennia are considered commands - # with arguments. There are three forms of commands possible - # through msdp: - # - # VARNAME VAR -> varname(var) - # ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val) - # TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> - # tablename(varname=val, varname=val) - # - - # default MSDP functions - if "LIST" in variables: - self.data_in("list", *variables.pop("LIST")) - if "REPORT" in variables: - self.data_in("report", *variables.pop("REPORT")) - if "REPORT" in arrays: - self.data_in("report", *(arrays.pop("REPORT"))) - if "UNREPORT" in variables: - self.data_in("unreport", *(arrays.pop("UNREPORT"))) - if "RESET" in variables: - self.data_in("reset", *variables.pop("RESET")) - if "RESET" in arrays: - self.data_in("reset", *(arrays.pop("RESET"))) - if "SEND" in variables: - self.data_in("send", *variables.pop("SEND")) - if "SEND" in arrays: - self.data_in("send", *(arrays.pop("SEND"))) - - # if there are anything left consider it a call to a custom function - - for varname, var in variables.items(): - # a simple function + argument - self.data_in(varname, (var,)) - for arrayname, array in arrays.items(): - # we assume the array are multiple arguments to the function - self.data_in(arrayname, *array) - for tablename, table in tables.items(): - # we assume tables are keyword arguments to the function - self.data_in(tablename, **table) - - def data_out(self, msdp_string): - """ - Return a msdp-valid subnegotiation across the protocol. - """ - #print "msdp data_out (without IAC SE):", msdp_string - self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE) - - def data_in(self, funcname, *args, **kwargs): - """ - Send oob data to Evennia - """ - #print "msdp data_in:", funcname, args, kwargs - self.protocol.data_in(text=None, oob=(funcname, args, kwargs)) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 0252b18c61..cc1d552a86 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -179,6 +179,71 @@ class PortalSessionHandler(SessionHandler): return [sess for sess in self.get_sessions(include_unloggedin=True) if hasattr(sess, 'suid') and sess.suid == suid] + def announce_all(self, message): + """ + Send message to all connection sessions + """ + for session in self.sessions.values(): + session.data_out(message) + + def oobstruct_parser(self, oobstruct): + """ + Helper method for each session to use to parse oob structures + (The 'oob' kwarg of the msg() method). + + Allowed input oob structures are: + cmdname + ((cmdname,), (cmdname,)) + (cmdname,(arg, )) + (cmdname,(arg1,arg2)) + (cmdname,{key:val,key2:val2}) + (cmdname, (args,), {kwargs}) + ((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,))) + outputs an ordered structure on the form + ((cmdname, (args,), {kwargs}), ...), where the two last + parts of each tuple may be empty + """ + def _parse(oobstruct): + slen = len(oobstruct) + if not oobstruct: + return tuple(None, (), {}) + elif not hasattr(oobstruct, "__iter__"): + # a singular command name, without arguments or kwargs + return (oobstruct, (), {}) + # regardless of number of args/kwargs, the first element must be + # the function name. We will not catch this error if not, but + # allow it to propagate. + if slen == 1: + return (oobstruct[0], (), {}) + elif slen == 2: + if isinstance(oobstruct[1], dict): + # cmdname, {kwargs} + return (oobstruct[0], (), dict(oobstruct[1])) + elif isinstance(oobstruct[1], (tuple, list)): + # cmdname, (args,) + return (oobstruct[0], list(oobstruct[1]), {}) + else: + # cmdname, cmdname + return ((oobstruct[0], (), {}), (oobstruct[1].lower(), (), {})) + else: + # cmdname, (args,), {kwargs} + return (oobstruct[0], list(oobstruct[1]), dict(oobstruct[2])) + + if hasattr(oobstruct, "__iter__"): + # differentiate between (cmdname, cmdname), + # (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}), + # (cmdname,(args),{kwargs}), ...) + + if oobstruct and isinstance(oobstruct[0], basestring): + return (list(_parse(oobstruct)),) + else: + out = [] + for oobpart in oobstruct: + out.append(_parse(oobpart)) + return (list(out),) + return (_parse(oobstruct),) + + def data_in(self, session, text="", **kwargs): """ Called by portal sessions for relaying data coming @@ -189,20 +254,17 @@ class PortalSessionHandler(SessionHandler): msg=text, data=kwargs) - def announce_all(self, message): - """ - Send message to all connection sessions - """ - for session in self.sessions.values(): - session.data_out(message) - def data_out(self, sessid, text=None, **kwargs): """ Called by server for having the portal relay messages and data - to the correct session protocol. + to the correct session protocol. We also convert oob input to + a generic form here. """ session = self.sessions.get(sessid, None) if session: + # convert oob to the generic format + if "oob" in kwargs: + kwargs["oob"] = self.oobstruct_parser(kwargs["oob"]) session.data_out(text=text, **kwargs) PORTAL_SESSIONS = PortalSessionHandler() diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index b718ad6f34..dbaa6828a3 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -10,7 +10,7 @@ sessions etc. import re from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO from evennia.server.session import Session -from evennia.server.portal import ttype, mssp, msdp, naws +from evennia.server.portal import ttype, mssp, telnet_oob, naws from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP from evennia.server.portal.mxp import Mxp, mxp_parse from evennia.utils import utils, ansi, logger @@ -35,7 +35,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): client_address = self.transport.client # this number is counted down for every handshake that completes. # when it reaches 0 the portal/server syncs their data - self.handshakes = 6 # naws, ttype, mccp, mssp, msdp, mxp + self.handshakes = 6 # naws, ttype, mccp, mssp, oob, mxp self.init_session("telnet", client_address, self.factory.sessionhandler) # negotiate client size @@ -47,8 +47,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.mccp = Mccp(self) # negotiate mssp (crawler communication) self.mssp = mssp.Mssp(self) - # msdp - self.msdp = msdp.Msdp(self) + # oob communication (MSDP, GMCP) + self.oob = telnet_oob.TelnetOOB(self) # mxp support self.mxp = Mxp(self) # keepalive watches for dead links @@ -211,7 +211,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): through the telnet connection. valid telnet kwargs: - oob= - supply an Out-of-Band instruction. + oob=[(cmdname,args,kwargs), ...] - supply an Out-of-Band instruction. xterm256=True/False - enforce xterm256 setting. If not given, ttype result is used. If client does not suport xterm256, the @@ -237,13 +237,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.sendLine(str(e)) return if "oob" in kwargs: - oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob")) - if "MSDP" in self.protocol_flags: - for cmdname, args, kwargs in oobstruct: - #print "cmdname, args, kwargs:", cmdname, args, kwargs - msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs) - #print "msdp_string:", msdp_string - self.msdp.data_out(msdp_string) + # oob is a list of [(cmdname, arg, kwarg), ...] + if "OOB" in self.protocol_flags: + for cmdname, args, kwargs in kwargs["oob"]: + self.oob.data_out(cmdname, *args, **kwargs) # parse **kwargs, falling back to ttype if nothing is given explicitly ttype = self.protocol_flags.get('TTYPE', {}) diff --git a/evennia/server/portal/telnet_oob.py b/evennia/server/portal/telnet_oob.py new file mode 100644 index 0000000000..7681a87bee --- /dev/null +++ b/evennia/server/portal/telnet_oob.py @@ -0,0 +1,238 @@ +""" + +Telnet OOB (Out of band communication) + +This implements the following telnet oob protocols: +MSDP (Mud Server Data Protocol) +GMCP (Generic Mud Communication Protocol) + +This implements the MSDP protocol as per +http://tintin.sourceforge.net/msdp/ and the GMCP protocol as per +http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29 + +Following the lead of KaVir's protocol snippet, we first check if +client supports MSDP and if not, we fallback to GMCP with a MSDP +header where applicable. + +OOB manages out-of-band +communication between the client and server, for updating health bars +etc. See also GMCP which is another standard doing the same thing. + +""" +import re +import json +from evennia.utils.utils import to_str + +# MSDP-relevant telnet cmd/opt-codes +MSDP = chr(69) +MSDP_VAR = chr(1) +MSDP_VAL = chr(2) +MSDP_TABLE_OPEN = chr(3) +MSDP_TABLE_CLOSE = chr(4) +MSDP_ARRAY_OPEN = chr(5) +MSDP_ARRAY_CLOSE = chr(6) + +GMCP = chr(200) + +IAC = chr(255) +SB = chr(250) +SE = chr(240) + +force_str = lambda inp: to_str(inp, force_string=True) + +# pre-compiled regexes +# returns 2-tuple +msdp_regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, + MSDP_ARRAY_OPEN, + MSDP_ARRAY_CLOSE)) +# returns 2-tuple (may be nested) +msdp_regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, + MSDP_TABLE_OPEN, + MSDP_TABLE_CLOSE)) +msdp_regex_var = re.compile(MSDP_VAR) +msdp_regex_val = re.compile(MSDP_VAL) + +# Msdp object handler + +class Telnet_OOB(object): + """ + Implements the MSDP and GMCP protocols. + """ + + def __init__(self, protocol): + """ + Initiates by storing the protocol + on itself and trying to determine + if the client supports MSDP. + """ + self.protocol = protocol + self.protocol.protocol_flags['OOB'] = False + self.MSDP = False + self.GMCP = False + # detect MSDP first + self.protocol.negotiationMap[MSDP] = self.data_in + self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp) + self.oob_reported = {} + + def no_msdp(self, option): + "No msdp supported or wanted" + # no msdp, check GMCP + self.protocol.negotiationMap[GMCP] = self.data_in + self.protocol.will(GMCP).addCallbacks(self.do_oob, self.no_oob) + + def do_msdp(self, option): + "MSDP supported by client" + self.MSDP = True + self.protocol.protocol_flags['OOB'] = True + self.protocol.handshake_done() + + def no_gmcp(self, option): + "Neither MSDP nor GMCP supported" + self.protocol.handshake_done() + + def do_gmcp(self, option): + """ + Called when client confirms that it can do MSDP or GMCP. + """ + self.GMCP = True + self.protocol.protocol_flags['OOB'] = True + self.protocol.handshake_done() + + # encoders + + def encode_msdp(self, cmdname, *args, **kwargs): + """ + handle return data from cmdname by converting it to + a proper msdp structure. These are the combinations we + support: + + cmdname string -> cmdname string + cmdname *args -> cmdname MSDP_ARRAY + cmdname **kwargs -> cmdname MSDP_TABLE + + OBS - whereas there are also definitions for making arrays and tables in + the specification, these are not actually used in the default + msdp commands -- returns are implemented as simple lists or + named lists (our name for them here, these un-bounded + structures are not named in the specification). So for now, + this routine will not explicitly create arrays nor tables, + although there are helper methods ready should it be needed in + the future. + """ + msdp_string = "" + if args: + if len(args) == 1: + msdp_string = "%s %s" % (cmdname.upper(), args[0]) + else: + msdp_string = "%s%s%s%s" % (MSDP_VAR, cmdname.upper(), "".join( + "%s%s" % (MSDP_VAL, val) for val in args)) + elif kwargs: + msdp_string = "%s%s%s" % (MSDP_VAR. cmdname.upper(), "".join( + ["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) for key, val in kwargs.items()])) + return msdp_string + + def encode_gmcp(self, cmdname, *args, **kwargs): + """ + Gmcp messages are on one of the following outgoing forms: + + cmdname string -> cmdname string + cmdname *args -> cmdname [arg, arg, arg, ...] + cmdname **kwargs -> cmdname {key:arg, key:arg, ...} + + cmdname is generally recommended to be a string on the form + Module.Submodule.Function + """ + if args: + gmcp_string = "%s %s" % (cmdname, json.dumps(args)) + elif kwargs: + gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs)) + return gmcp_string + + def decode_msdp(self, data): + """ + Decodes incoming MSDP data + + cmdname, var --> cmdname arg + cmdname, array --> cmdname *args + cmdname, table --> cmdname **kwargs + + """ + tables = {} + arrays = {} + variables = {} + + if hasattr(data, "__iter__"): + data = "".join(data) + + #logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data) + + # decode + for key, table in msdp_regex_table.findall(data): + tables[key] = {} + for varval in msdp_regex_var.split(table): + parts = msdp_regex_val.split(varval) + tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)}) + for key, array in msdp_regex_array.findall(data): + arrays[key] = [] + for val in msdp_regex_val.split(array): + arrays[key].append(val) + arrays[key] = tuple(arrays[key]) + for varval in msdp_regex_var.split(msdp_regex_array.sub("", msdp_regex_table.sub("", data))): + # get remaining varvals after cleaning away tables/arrays + parts = msdp_regex_val.split(varval) + variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", ) + + # send to the sessionhandler + if data: + for varname, var in variables.items(): + # a simple function + argument + self.protocol.data_in(oob=(varname, var, {})) + for arrayname, array in arrays.items(): + # we assume the array are multiple arguments to the function + self.protocol.data_in(oob=(arrayname, array, {})) + for tablename, table in tables.items(): + # we assume tables are keyword arguments to the function + self.protocol.data_in(oob=(tablename, (), table)) + + def decode_gmcp(self, data): + """ + Decodes incoming GMCP data on the form 'varname ' + + cmdname string -> cmdname arg + cmdname [arg, arg,...] -> cmdname *args + cmdname {key:arg, key:arg, ...} -> cmdname **kwargs + + """ + if data: + splits = data.split(" ", 1) + cmdname = splits[0] + if len(splits) < 2: + self.protocol.data_in(oob=(cmdname, (), {})) + else: + struct = json.loads(splits[1]) + self.protocol.data_in(oob=(cmdname, + struct if isinstance(struct, list) else (), + struct if isinstance(struct, dict) else {})) + + # access methods + + def data_out(self, cmdname, *args, **kwargs): + """ + Return a msdp-valid subnegotiation across the protocol. + """ + if self.MSDP: + encoded_oob = force_str(self.encode_msdp(cmdname, *args, **kwargs)) + self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE) + else: + encoded_oob = force_str(self.encode_gmcp(cmdname, *args, **kwargs)) + self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE) + + def data_in(self, data): + """ + Send oob data to Evennia. The self.decode_* methods send to + protocol.data_in() themselves. + """ + if self.MSDP: + self.decode_msdp(data) + else: + self.decode_gmcp(data) diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index dedf81e45b..b7a36c2b75 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -22,11 +22,7 @@ from evennia.server.session import Session IDLE_COMMAND = settings.IDLE_COMMAND _GA = object.__getattribute__ _ObjectDB = None -_OOB_HANDLER = None -# load optional out-of-band function module (this acts as a verification) -OOB_PLUGIN_MODULES = [utils.mod_import(mod) - for mod in make_iter(settings.OOB_PLUGIN_MODULES) if mod] INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED # i18n @@ -193,10 +189,9 @@ class ServerSession(Session): Send User->Evennia. This will in effect execute a command string on the server. - Especially handled keywords: + Note that oob data is already sent to the + oobhandler at this point. - oob - this should hold a dictionary of oob command calls from - the oob-supporting protocol. """ #explicitly check for None since text can be an empty string, which is #also valid @@ -218,16 +213,6 @@ class ServerSession(Session): categories=("inputline", "channels"), include_player=False) cmdhandler(self, text, callertype="session", sessid=self.sessid) self.update_session_counters() - if "oob" in kwargs: - # handle oob instructions - global _OOB_HANDLER - if not _OOB_HANDLER: - from evennia.server.oobhandler import OOB_HANDLER as _OOB_HANDLER - oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None)) - #print "session.data_in: oobstruct:",oobstruct - for (funcname, args, kwargs) in oobstruct: - if funcname: - _OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs) execute_cmd = data_in # alias diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index d144977fe9..d26f619b16 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -27,6 +27,7 @@ _PlayerDB = None _ServerSession = None _ServerConfig = None _ScriptDB = None +_OOB_HANDLER = None # AMP signals @@ -102,63 +103,6 @@ class SessionHandler(object): """ return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items()) - def oobstruct_parser(self, oobstruct): - """ - Helper method for each session to use to parse oob structures - (The 'oob' kwarg of the msg() method). - - Allowed input oob structures are: - cmdname - ((cmdname,), (cmdname,)) - (cmdname,(arg, )) - (cmdname,(arg1,arg2)) - (cmdname,{key:val,key2:val2}) - (cmdname, (args,), {kwargs}) - ((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,))) - outputs an ordered structure on the form - ((cmdname, (args,), {kwargs}), ...), where the two last - parts of each tuple may be empty - """ - def _parse(oobstruct): - slen = len(oobstruct) - if not oobstruct: - return tuple(None, (), {}) - elif not hasattr(oobstruct, "__iter__"): - # a singular command name, without arguments or kwargs - return (oobstruct.lower(), (), {}) - # regardless of number of args/kwargs, the first element must be - # the function name. We will not catch this error if not, but - # allow it to propagate. - if slen == 1: - return (oobstruct[0].lower(), (), {}) - elif slen == 2: - if isinstance(oobstruct[1], dict): - # cmdname, {kwargs} - return (oobstruct[0].lower(), (), dict(oobstruct[1])) - elif isinstance(oobstruct[1], (tuple, list)): - # cmdname, (args,) - return (oobstruct[0].lower(), list(oobstruct[1]), {}) - else: - # cmdname, cmdname - return ((oobstruct[0].lower(), (), {}), (oobstruct[1].lower(), (), {})) - else: - # cmdname, (args,), {kwargs} - return (oobstruct[0].lower(), list(oobstruct[1]), dict(oobstruct[2])) - - if hasattr(oobstruct, "__iter__"): - # differentiate between (cmdname, cmdname), - # (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}), - # (cmdname,(args),{kwargs}), ...) - - if oobstruct and isinstance(oobstruct[0], basestring): - return (list(_parse(oobstruct)),) - else: - out = [] - for oobpart in oobstruct: - out.append(_parse(oobpart)) - return (list(out),) - return (_parse(oobstruct),) - #------------------------------------------------------------ # Server-SessionHandler class @@ -482,11 +426,22 @@ class ServerSessionHandler(SessionHandler): def data_in(self, sessid, text="", **kwargs): """ - Data Portal -> Server + Data Portal -> Server. + We also intercept OOB communication here. """ session = self.sessions.get(sessid, None) if session: text = text and to_unicode(strip_control_sequences(text), encoding=session.encoding) + if "oob" in kwargs: + # incoming data is always on the form (cmdname, args, kwargs) + global _OOB_HANDLER + if not _OOB_HANDLER: + from evennia.server.oobhandler import OOB_HANDLER as _OOB_HANDLER + funcname, args, kwargs = kwargs.pop("oob") + if funcname: + _OOB_HANDLER.execute_cmd(session, funcname, *args, **kwargs) + + # pass the rest off to the session session.data_in(text=text, **kwargs) SESSIONS = ServerSessionHandler() diff --git a/evennia/utils/idmapper/base.py b/evennia/utils/idmapper/base.py index 1929911240..fff36f1e92 100755 --- a/evennia/utils/idmapper/base.py +++ b/evennia/utils/idmapper/base.py @@ -13,6 +13,7 @@ import os, threading, gc, time from weakref import WeakValueDictionary from twisted.internet.reactor import callFromThread from django.core.exceptions import ObjectDoesNotExist, FieldError +from django.db.models.signals import post_save from django.db.models.base import Model, ModelBase from django.db.models.signals import post_save, pre_delete, post_syncdb from evennia.utils import logger @@ -22,11 +23,40 @@ from manager import SharedMemoryManager AUTO_FLUSH_MIN_INTERVAL = 60.0 * 5 # at least 5 mins between cache flushes - _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ +def at_field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs): + """ + Called at the beginning of the field save operation. The save method + must be called with the update_fields keyword in order to be most efficient. + This method should NOT save; rather it is the save() that triggers this + function. Its main purpose is to allow to plug-in a save handler and oob + handlers. + """ + if raw: + return + if update_fields: + # this is a list of strings at this point. We want field objects + update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) + else: + # meta.fields are already field objects; get them all + update_fields = _GA(_GA(instance, "_meta"), "fields") + for field in update_fields: + fieldname = field.name + handlername = "_at_%s_postsave" % fieldname + handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None + if callable(handler): + handler() + trackerhandler = _GA(instance, "_trackerhandler") if "_trackerhandler" in _GA(instance, '__dict__') else None + if trackerhandler: + trackerhandler.update(fieldname, _GA(instance, fieldname)) + +# connect the signal to catch field changes +post_save.connect(at_post_field_save, dispatch_uid="at_post_field_save") + + # References to db-updated objects are stored here so the # main process can be informed to re-cache itself. PROC_MODIFIED_COUNT = 0