From 96c6ad4aff1522e01ba59f8220842d1ba25efa6f Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 27 Sep 2013 21:31:47 +0200 Subject: [PATCH 1/8] Fixed an issue with channel-sending crashing when ooc. Resolves Issue 429. --- src/commands/cmdhandler.py | 4 ++-- src/commands/default/admin.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index adf2e9a960..6745e6e097 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -332,7 +332,7 @@ def cmdhandler(called_on, raw_string, testing=False, callertype="session", sessi if syscmd: # replace system command with custom version cmd = syscmd - cmd.sessid = caller.sessid if callertype=="session" else None + cmd.sessid = session.sessid if session else None sysarg = "%s:%s" % (cmdname, args) raise ExecSystemCommand(cmd, sysarg) @@ -398,7 +398,7 @@ def cmdhandler(called_on, raw_string, testing=False, callertype="session", sessi syscmd.cmdstring = syscmd.key syscmd.args = sysarg syscmd.cmdset = cmdset - syscmd.sessid = caller.sessid if callertype=="session" else None + syscmd.sessid = session.sessid if session else None syscmd.raw_string = unformatted_raw_string if hasattr(syscmd, 'obj') and hasattr(syscmd.obj, 'scripts'): diff --git a/src/commands/default/admin.py b/src/commands/default/admin.py index 5598776d48..51d4afbdbc 100644 --- a/src/commands/default/admin.py +++ b/src/commands/default/admin.py @@ -527,7 +527,7 @@ class CmdPerm(MuxCommand): tstring = "" if 'del' in switches: # delete the given permission(s) from object. - obj.permission.remove(self.rhslist) + obj.permissions.remove(self.rhslist) cstring += "\nPermission(s) %s removed from %s (if they existed)." % (", ".join(self.rhslist), obj.name) tstring += "\n%s revokes the permission(s) %s from you." % (caller.name, ", ".join(self.rhslist)) else: From 16bbe009c33dab0578e96063ea45c3ea822c266b Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 7 Oct 2013 19:57:01 +0200 Subject: [PATCH 2/8] OOB passing data client->server. A few more default commands are needed. --- src/server/oobhandler.py | 6 +- src/server/portal/msdp.py | 72 +++++++++++------------ src/server/portal/portalsessionhandler.py | 1 + src/server/portal/telnet.py | 17 ++++-- src/server/serversession.py | 10 +++- src/server/sessionhandler.py | 45 +++++++++++++- 6 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 14d4040fd3..98dd32fdcd 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -26,10 +26,10 @@ from django.conf import settings from src.server.models import ServerConfig from src.server.sessionhandler import SESSIONS from src.scripts.scripts import Script -from src.create import create_script +from src.utils.create import create_script from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj from src.utils import logger -from src.utils.utils import variable_from_module, to_str +from src.utils.utils import variable_from_module, to_str, is_iter, make_iter _SA = object.__setattr__ _GA = object.__getattribute__ @@ -307,7 +307,7 @@ class OOBHandler(object): oob_tracker_name = "_track_db_value_change" self.track(attrobj, tracker_key, attr_name, sessid, property_name=oob_tracker_name) - def run(self, func_key, *args, **kwargs): + def execute_cmd(self, func_key, *args, **kwargs): """ Retrieve oobfunc from OOB_FUNCS and execute it immediately using *args and **kwargs diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 86e2847dfc..6eaa9a1eb5 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -132,7 +132,7 @@ class Msdp(object): """ self.protocol = protocol self.protocol.protocol_flags['MSDP'] = False - self.protocol.negotiationMap[MSDP] = self.msdp_to_func + self.protocol.negotiationMap[MSDP] = self.msdp_to_evennia self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp) self.msdp_reported = {} @@ -148,10 +148,7 @@ class Msdp(object): print "msdp supported" self.protocol.protocol_flags['MSDP'] = True - def parse_msdp(self, args): - "Called with arguments to subnegotiation" - - def func_to_msdp(self, cmdname, data): + def evennia_to_msdp(self, cmdname, data): """ handle return data from cmdname by converting it to a proper msdp structure. data can either be a single value (will be @@ -179,6 +176,7 @@ class Msdp(object): def make_array(name, datalist, string): "build a simple array. Arrays may not nest tables by definition." + print "make_array", datalist, string string += MSDP_VAR + name + MSDP_ARRAY_OPEN for val in datalist: string += MSDP_VAL + val @@ -190,11 +188,11 @@ class Msdp(object): elif hasattr(data, '__iter__'): msdp_string = make_array(cmdname, data, "") else: - msdp_string = MSDP_VAR + cmdname + MSDP_VAL + data + msdp_string = MSDP_VAR + cmdname + MSDP_VAL + data if data!=None else "" return msdp_string - def msdp_to_func(self, data): + def msdp_to_evennia(self, data): """ Handle a client's requested negotiation, converting it into a function mapping - either one of the MSDP @@ -229,57 +227,55 @@ class Msdp(object): 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) + # + + ret = "" # default MSDP functions if "LIST" in variables: - ret += self.func_to_msdp("LIST", self.msdp_cmd_list(variables["LIST"])) - del variables["LIST"] + ret += self.evennia_to_msdp("LIST", self.msdp_cmd_list(*(variables.pop("LIST"),))) if "REPORT" in variables: - ret += self.func_to_msdp("REPORT", self.msdp_cmd_report(*(variables["REPORT"],))) - del variables["REPORT"] + ret += self.evennia_to_msdp("REPORT", self.msdp_cmd_report(*(variables.pop("REPORT"),))) if "REPORT" in arrays: - ret += self.func_to_msdp("REPORT", self.msdp_cmd_report(*arrays["REPORT"])) - del arrays["REPORT"] + ret += self.evennia_to_msdp("REPORT", self.msdp_cmd_report(*arrays.pop("REPORT"))) if "RESET" in variables: - ret += self.func_to_msdp("RESET", self.msdp_cmd_reset(*(variables["RESET"],))) - del variables["RESET"] + ret += self.evennia_to_msdp("RESET", self.msdp_cmd_reset(*(variables.pop("RESET"),))) if "RESET" in arrays: - ret += self.func_to_msdp("RESET", self.msdp_cmd_reset(*arrays["RESET"])) - del arrays["RESET"] + ret += self.evennia_to_msdp("RESET", self.msdp_cmd_reset(*arrays.pop("RESET",))) if "SEND" in variables: - ret += self.func_to_msdp("SEND", self.msdp_cmd_send(*(variables["SEND"],))) - del variables["SEND"] + ret += self.evennia_to_msdp("SEND", self.msdp_cmd_send(*(variables.pop("SEND",)))) if "SEND" in arrays: - ret += self.func_to_msdp("SEND",self.msdp_cmd_send(*arrays["SEND"])) - del arrays["SEND"] + ret += self.evennia_to_msdp("SEND",self.msdp_cmd_send(*arrays.pop("SEND"))) # if there are anything left we look for a custom function for varname, var in variables.items(): # a simple function + argument ooc_func = MSDP_COMMANDS_CUSTOM.get(varname.upper()) if ooc_func: - ret += self.func_to_msdp(varname, ooc_func(var)) + ret += self.evennia_to_msdp(varname, ooc_func(var)) for arrayname, array in arrays.items(): # we assume the array are multiple arguments to the function ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper()) if ooc_func: - ret += self.func_to_msdp(arrayname, ooc_func(*array)) + ret += self.evennia_to_msdp(arrayname, ooc_func(*array)) for tablename, table in tables.items(): # we assume tables are keyword arguments to the function ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper()) if ooc_func: - ret += self.func_to_msdp(tablename, ooc_func(**table)) + ret += self.evennia_to_msdp(tablename, ooc_func(**table)) + # return any result if ret: - # send return value if it exists - self.msdp_send(ret) - ret = IAC + SB + MSDP + ret + IAC + SE - #ret = IAC + SB + MSDP + MSDP_VAR + "SEND" + MSDP_VAL + "Testsend" + IAC + SE - self.protocol._write(ret) - logger.log_infomsg("MSDP_RESULT: %s" % ret) + self.data_out(ret) - def msdp_send(self, msdp_string): + def data_out(self, msdp_string): """ Return a msdp-valid subnegotiation across the protocol. """ @@ -295,21 +291,21 @@ class Msdp(object): The List command allows for retrieving various info about the server/client """ if arg == 'COMMANDS': - return self.func_to_msdp(arg, MSDP_COMMANDS) + return self.evennia_to_msdp(arg, MSDP_COMMANDS) elif arg == 'LISTS': - return self.func_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES", + return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES", "REPORTED_VARIABLES", "SENDABLE_VARIABLES")) elif arg == 'CONFIGURABLE_VARIABLES': - return self.func_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID")) + return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID")) elif arg == 'REPORTABLE_VARIABLES': - return self.func_to_msdp(arg, MSDP_REPORTABLE.keys()) + return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys()) elif arg == 'REPORTED_VARIABLES': # the dynamically set items to report - return self.func_to_msdp(arg, self.msdp_reported.keys()) + return self.evennia_to_msdp(arg, self.msdp_reported.keys()) elif arg == 'SENDABLE_VARIABLES': - return self.func_to_msdp(arg, MSDP_SENDABLE.keys()) + return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys()) else: - return self.func_to_msdp("LIST", arg) + return self.evennia_to_msdp("LIST", arg) # default msdp commands diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index f0dbb40dac..f70d6296f8 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -138,6 +138,7 @@ class PortalSessionHandler(SessionHandler): 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 diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index 9ed178b2da..21fa14fef8 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -13,6 +13,7 @@ from src.server.session import Session from src.server.portal import ttype, mssp, msdp from src.server.portal.mccp import Mccp, mccp_compress, MCCP from src.utils import utils, ansi, logger +from src.utils.utils import make_iter, is_iter _RE_N = re.compile(r"\{n$") @@ -36,14 +37,13 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # negotiate ttype (client info) #self.ttype = ttype.Ttype(self) # negotiate mssp (crawler communication) - self.mssp = mssp.Mssp(self) + #self.mssp = mssp.Mssp(self) # msdp - #self.msdp = msdp.Msdp(self) + self.msdp = msdp.Msdp(self) # add this new connection to sessionhandler so # the Server becomes aware of it. self.sessionhandler.connect(self) - def enableRemote(self, option): """ This sets up the remote-activated options we allow for this protocol. @@ -69,7 +69,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): else: return super(TelnetProtocol, self).disableLocal(option) - def connectionLost(self, reason): """ This is executed when the connection is lost for @@ -163,6 +162,16 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): except Exception, e: self.sendLine(str(e)) return + if "oob" in kwargs: + oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob")) + if "MSDP" in self.protocol_flags: + print "oobstruct:", oobstruct + for cmdname, args in oobstruct: + print "cmdname, args:", cmdname, args + msdp_string = self.msdp.func_to_msdp(cmdname, args) + print "msdp_string:", msdp_string + self.msdp.data_out(msdp_string) + ttype = self.protocol_flags.get('TTYPE', {}) raw = kwargs.get("raw", False) nomarkup = not (ttype or ttype.get('256 COLORS') or ttype.get('ANSI') or not ttype.get("init_done")) diff --git a/src/server/serversession.py b/src/server/serversession.py index 5df372c0c9..49dd89851b 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -20,6 +20,7 @@ from src.server.session import Session IDLE_COMMAND = settings.IDLE_COMMAND _GA = object.__getattribute__ _ObjectDB = None +_OOB_HANDLER = None # load optional out-of-band function module OOB_PLUGIN_MODULE = settings.OOB_PLUGIN_MODULE @@ -178,8 +179,13 @@ class ServerSession(Session): cmdhandler.cmdhandler(self, text, callertype="session", sessid=self.sessid) self.update_session_counters() if "oob" in kwargs: - # relay to OOB handler - pass + # handle oob instructions + global _OOB_HANDLER + if not _OOB_HANDLER: + from src.servever.oobhandler import OOB_HANDLER as _OOB_HANDLER + oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None)) + for (funcname, args, kwargs) in oobstruct: + _OOBHANDLER.execute_cmd(funcname, *args, **kwargs) execute_cmd = data_in # alias diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 0ae60c2ff3..fa4634bbba 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -27,6 +27,7 @@ _ServerSession = None _ServerConfig = None _ScriptDB = None + # AMP signals PCONN = chr(1) # portal session connect PDISCONN = chr(2) # portal session disconnect @@ -357,6 +358,49 @@ class ServerSessionHandler(SessionHandler): return self.sessions.get(sessid) return None + def oobstruct_parser(self, oobstruct): + """ + Helper method for each session to use to parse oob structures + (The 'oob' kwarg of the msg() method) + allowed 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 + """ + 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((key.lower(), val) for key,val in oobstruct[1].items())) + elif isinstance(oobstruct[1], (tuple, list)): + # cmdname, (args,) + return (oobstruct[0].lower(), tuple(arg.lower() for arg in oobstruct[1]), {}) + else: + # cmdname, (args,), {kwargs} + return (oobstruct[0].lower(), tuple(arg.lower for arg in oobstruct[1]), + dict((key.lower(), val) for key, val in oobstruct[2].items())) + + # either multiple funcnames or multiple func tuples; descend recursively + out = [] + for oobpart in oobstruct: + out.append(self.oobstruct_parser(oobpart)[0]) + return tuple(out) + def announce_all(self, message): """ Send message to all connected sessions @@ -379,5 +423,4 @@ class ServerSessionHandler(SessionHandler): if session: session.data_in(text=text, **kwargs) - SESSIONS = ServerSessionHandler() From bdcc8de5bc63e98cd6b977ed45a6fdb5ccd7eb9b Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 15 Oct 2013 20:00:18 +0200 Subject: [PATCH 3/8] OOB MSDP working with direct sending of data from various parts of the system. Tracking as well as support for the default MSDP commands (LIST, REPORT etc) are not yet tested/implemented. --- src/server/amp.py | 8 +- src/server/oobhandler.py | 66 ++--- src/server/portal/msdp.py | 279 ++++++++++++---------- src/server/portal/portalsessionhandler.py | 1 - src/server/portal/telnet.py | 38 +-- src/server/serversession.py | 13 +- src/server/sessionhandler.py | 91 +++---- src/settings_default.py | 2 +- src/typeclasses/models.py | 186 +++++++++------ src/utils/utils.py | 4 +- 10 files changed, 390 insertions(+), 298 deletions(-) diff --git a/src/server/amp.py b/src/server/amp.py index 528aa0bab8..f6ec7b86e3 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -240,7 +240,7 @@ class AMPProtocol(amp.AMP): def errback(self, e, info): "error handler, to avoid dropping connections on server tracebacks." - e.trap(Exception) + f = e.trap(Exception) print "AMP Error for %(info)s: %(e)s" % {'info': info, 'e': e.getErrorMessage()} def send_split_msg(self, sessid, msg, data, command): @@ -286,7 +286,7 @@ class AMPProtocol(amp.AMP): data comes in multiple chunks; if so (nparts>1) we buffer the data and wait for the remaining parts to arrive before continuing. """ - #print "msg portal -> server (server side):", sessid, msg + #print "msg portal -> server (server side):", sessid, msg, data global MSGBUFFER if nparts > 1: # a multipart message @@ -311,7 +311,7 @@ class AMPProtocol(amp.AMP): try: return self.callRemote(MsgPortal2Server, sessid=sessid, - msg=msg, + msg=to_str(msg) if msg!=None else "", ipart=0, nparts=1, data=dumps(data)).addErrback(self.errback, "MsgPortal2Server") @@ -351,7 +351,7 @@ class AMPProtocol(amp.AMP): try: return self.callRemote(MsgServer2Portal, sessid=sessid, - msg=to_str(msg), + msg=to_str(msg) if msg!=None else "", ipart=0, nparts=1, data=dumps(data)).addErrback(self.errback, "MsgServer2Portal") diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 98dd32fdcd..6286b69ccf 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -22,6 +22,7 @@ oob trackers should inherit from the OOBTracker class in this """ +from inspect import isfunction from django.conf import settings from src.server.models import ServerConfig from src.server.sessionhandler import SESSIONS @@ -29,16 +30,15 @@ from src.scripts.scripts import Script from src.utils.create import create_script from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj from src.utils import logger -from src.utils.utils import variable_from_module, to_str, is_iter, make_iter +from src.utils.utils import all_from_module, to_str, is_iter, make_iter _SA = object.__setattr__ _GA = object.__getattribute__ -_DA = object.__delattribute__ +_DA = object.__delattr__ -# trackers track property changes and keep returning until they are removed -_OOB_TRACKERS = variable_from_module(settings.OBB_PLUGIN_MODULE, "OBB_TRACKERS", default={}) -# functions return immediately -_OOB_FUNCS = variable_from_module(settings.OBB_PLUGIN_MODULE, "OBB_FUNCS", default={}) +# load from plugin module +_OOB_FUNCS = dict((key, func) for key, func in all_from_module(settings.OOB_PLUGIN_MODULE).items() if isfunction(func)) +_OOB_ERROR = _OOB_FUNCS.get("oob_error", None) class TrackerHandler(object): @@ -123,10 +123,8 @@ class OOBTracker(TrackerBase): def update(self, new_value, *args, **kwargs): "Called by cache when updating the tracked entitiy" - SESSIONS.session_from_sessid(self.sessid).msg(oob={"cmdkey":"trackreturn", - "name":self.fieldname, - "value":new_value}) - + SESSIONS.session_from_sessid(self.sessid).msg(oob=("trackreturn", + (self.fieldname, new_value))) class _RepeaterPool(object): """ @@ -194,14 +192,6 @@ class _RepeaterPool(object): self.scripts[interval].stop() -# Default OOB funcs - -def OOB_get_attr_val(caller, attrname): - "Get the given attrback from caller" - caller.msg(oob={"cmdkey":"get_attr", - "name":attrname, - "value":to_str(caller.attributes.get(attrname))}) - # Main OOB Handler class OOBHandler(object): @@ -214,6 +204,7 @@ class OOBHandler(object): """ Initialize handler """ + self.sessionhandler = SESSIONS self.oob_tracker_storage = {} self.oob_repeat_storage = {} self.oob_tracker_pool = _RepeaterPool() @@ -247,7 +238,7 @@ class OOBHandler(object): self.repeat(caller, func_key, interval, *args, **kwargs) - def track(self, obj, sessid, fieldname, tracker_key, *args, **kwargs): + def track(self, obj, sessid, fieldname, oobclass, *args, **kwargs): """ Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args, kwargs will be used to initialize the OOB hook before adding @@ -307,17 +298,6 @@ class OOBHandler(object): oob_tracker_name = "_track_db_value_change" self.track(attrobj, tracker_key, attr_name, sessid, property_name=oob_tracker_name) - def execute_cmd(self, func_key, *args, **kwargs): - """ - Retrieve oobfunc from OOB_FUNCS and execute it immediately - using *args and **kwargs - """ - oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found - try: - oobfunc(*args, **kwargs) - except Exception: - logger.log_trace() - def repeat(self, caller, func_key, interval=20, *args, **kwargs): """ Start a repeating action. Every interval seconds, @@ -339,7 +319,31 @@ class OOBHandler(object): self.oob_tracker_pool.remove(store_key, interval) self.oob_repeat_storage.pop(store_key, None) + def msg(self, sessid, funcname, *args, **kwargs): + "Shortcut to relay oob data back to portal" + session = self.sessionhandler.session_from_sessid(sessid) + if session: + session.msg(oob=(funcname, args, kwargs)) - + def execute_cmd(self, session, func_key, *args, **kwargs): + """ + Retrieve oobfunc from OOB_FUNCS and execute it immediately + using *args and **kwargs + """ + try: + oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found + oobfunc(self, session, *args, **kwargs) + except KeyError: + errmsg = "OOB Error: function '%s' not recognized." % func_key + if _OOB_ERROR: + _OOB_ERROR(self, session, errmsg, *args, **kwargs) + else: + logger.log_trace(errmsg) + except Exception, err: + errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (func_key, args, kwargs, err) + if _OOB_ERROR: + _OOB_ERROR(self, session, errmsg, *args, **kwargs) + else: + logger.log_trace(errmsg) # access object OOB_HANDLER = OOBHandler() diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 6eaa9a1eb5..6c21ac2800 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -12,7 +12,7 @@ etc. """ import re from django.conf import settings -from src.utils.utils import make_iter, mod_import +from src.utils.utils import make_iter, mod_import, to_str from src.utils import logger # MSDP-relevant telnet cmd/opt-codes @@ -28,10 +28,13 @@ IAC = chr(255) SB = chr(250) SE = chr(240) +force_str = lambda inp: to_str(inp, force_string=True) + # pre-compiled regexes regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)) # return 2-tuple regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)) # return 2-tuple (may be nested) -regex_varval = re.compile(r"%s(.*?)%s(.*)" % (MSDP_VAR, MSDP_VAL)) # return 2-tuple +regex_var = re.compile(MSDP_VAR) +regex_val = re.compile(MSDP_VAL) # MSDP default definition commands supported by Evennia (can be supplemented with custom commands as well) MSDP_COMMANDS = ("LIST", "REPORT", "RESET", "SEND", "UNREPORT") @@ -138,59 +141,75 @@ class Msdp(object): def no_msdp(self, option): "No msdp supported or wanted" - print "No msdp supported" pass def do_msdp(self, option): """ Called when client confirms that it can do MSDP. """ - print "msdp supported" self.protocol.protocol_flags['MSDP'] = True - def evennia_to_msdp(self, cmdname, data): + 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 - this supports nested tables and even arrays nested - inside tables, as opposed to the receive method. Arrays - cannot hold tables by definition (the table must be named - with MSDP_VAR, and an array can only contain MSDP_VALs). + Obs - this normally only returns tables and lists (var val val ...) rather than + arrays. It will convert *args to lists and **kwargs to tables and + if both are given to this method, this will result in a list followed + by a table, both having the same names. """ - def make_table(name, datadict, string): + def make_table(name, **kwargs): "build a table that may be nested with other tables or arrays." - string += MSDP_VAR + name + MSDP_VAL + MSDP_TABLE_OPEN - for key, val in datadict.items(): - if type(val) == type({}): - string += make_table(key, val, string) + 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(key, val, string) + string += make_array(string, key, *val) else: - string += MSDP_VAR + key + MSDP_VAL + val + string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) string += MSDP_TABLE_CLOSE return string - def make_array(name, datalist, string): - "build a simple array. Arrays may not nest tables by definition." - print "make_array", datalist, string - string += MSDP_VAR + name + MSDP_ARRAY_OPEN - for val in datalist: - string += MSDP_VAL + val + 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 - if isinstance(data, dict): - msdp_string = make_table(cmdname, data, "") - elif hasattr(data, '__iter__'): - msdp_string = make_array(cmdname, data, "") - else: - msdp_string = MSDP_VAR + cmdname + MSDP_VAL + data if data!=None else "" - return msdp_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 + # Default MSDP commands + + cupper = cmdname.upper() + if cupper == "LIST": + self.data_out(make_list("LIST", *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_list("SEND", *args)) + else: + # return list or tables. If both arg/kwarg is given, return one array and one table, both + # with the same name. + msdp_string = "" + if args: + msdp_string += make_list(cupper, *args) + if kwargs: + msdp_string += make_table(cupper, **kwargs) + self.data_out(msdp_string) def msdp_to_evennia(self, data): """ @@ -216,16 +235,24 @@ class Msdp(object): if hasattr(data, "__iter__"): data = "".join(data) - logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data) + #logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data) - for table in regex_table.findall(data): - tables[table[0].upper()] = dict(regex_varval.findall(table[1])) - for array in regex_array.findall(data): - arrays[array[0].upper()] = dict(regex_varval.findall(array[1])) - # get all stand-alone variables, but first we must clean out all tables and arrays (which also contain vars) - variables = dict((key.upper(), val) for key, val in regex_varval.findall(regex_array.sub("", regex_table.sub("", 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 + #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: @@ -235,122 +262,124 @@ class Msdp(object): # TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> tablename(varname=val, varname=val) # - - ret = "" - # default MSDP functions if "LIST" in variables: - ret += self.evennia_to_msdp("LIST", self.msdp_cmd_list(*(variables.pop("LIST"),))) + self.data_in("list", *variables.pop("LIST")) if "REPORT" in variables: - ret += self.evennia_to_msdp("REPORT", self.msdp_cmd_report(*(variables.pop("REPORT"),))) + self.data_in("report", *variables.pop("REPORT")) if "REPORT" in arrays: - ret += self.evennia_to_msdp("REPORT", self.msdp_cmd_report(*arrays.pop("REPORT"))) + self.data_in("report", *(arrays.pop("REPORT"))) + if "UNREPORT" in variables: + self.data_in("unreport", *(arrays.pop("UNREPORT"))) if "RESET" in variables: - ret += self.evennia_to_msdp("RESET", self.msdp_cmd_reset(*(variables.pop("RESET"),))) + self.data_in("reset", *variables.pop("RESET")) if "RESET" in arrays: - ret += self.evennia_to_msdp("RESET", self.msdp_cmd_reset(*arrays.pop("RESET",))) + self.data_in("reset", *(arrays.pop("RESET"))) if "SEND" in variables: - ret += self.evennia_to_msdp("SEND", self.msdp_cmd_send(*(variables.pop("SEND",)))) + self.data_in("send", *variables.pop("SEND")) if "SEND" in arrays: - ret += self.evennia_to_msdp("SEND",self.msdp_cmd_send(*arrays.pop("SEND"))) + self.data_in("send", *(arrays.pop("SEND"))) + + # if there are anything left consider it a call to a custom function - # if there are anything left we look for a custom function for varname, var in variables.items(): # a simple function + argument - ooc_func = MSDP_COMMANDS_CUSTOM.get(varname.upper()) - if ooc_func: - ret += self.evennia_to_msdp(varname, ooc_func(var)) + self.data_in(varname, (var,)) for arrayname, array in arrays.items(): # we assume the array are multiple arguments to the function - ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper()) - if ooc_func: - ret += self.evennia_to_msdp(arrayname, ooc_func(*array)) + self.data_in(arrayname, *array) for tablename, table in tables.items(): # we assume tables are keyword arguments to the function - ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper()) - if ooc_func: - ret += self.evennia_to_msdp(tablename, ooc_func(**table)) - - # return any result - if ret: - self.data_out(ret) + self.data_in(tablename, **table) def data_out(self, msdp_string): """ Return a msdp-valid subnegotiation across the protocol. """ - self.protocol._write(IAC + SB + MSDP + msdp_string + IAC + SE) + #print "msdp data_out (without IAC SE):", msdp_string + self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE) - # MSDP Commands - # Some given MSDP (varname, value) pairs can also be treated as command + argument. - # Generic msdp command map. The argument will be sent to the given command. - # See http://tintin.sourceforge.net/msdp/ for definitions of each command. - # These are client->server commands. - def msdp_cmd_list(self, arg): + def data_in(self, funcname, *args, **kwargs): """ - The List command allows for retrieving various info about the server/client + Send oob data to Evennia """ - if arg == 'COMMANDS': - return self.evennia_to_msdp(arg, MSDP_COMMANDS) - elif arg == 'LISTS': - return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES", - "REPORTED_VARIABLES", "SENDABLE_VARIABLES")) - elif arg == 'CONFIGURABLE_VARIABLES': - return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID")) - elif arg == 'REPORTABLE_VARIABLES': - return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys()) - elif arg == 'REPORTED_VARIABLES': - # the dynamically set items to report - return self.evennia_to_msdp(arg, self.msdp_reported.keys()) - elif arg == 'SENDABLE_VARIABLES': - return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys()) - else: - return self.evennia_to_msdp("LIST", arg) + #print "msdp data_in:", funcname, args, kwargs + self.protocol.data_in(text=None, oob=(funcname, args, kwargs)) - # default msdp commands + # # MSDP Commands + # # Some given MSDP (varname, value) pairs can also be treated as command + argument. + # # Generic msdp command map. The argument will be sent to the given command. + # # See http://tintin.sourceforge.net/msdp/ for definitions of each command. + # # These are client->server commands. + # def msdp_cmd_list(self, arg): + # """ + # The List command allows for retrieving various info about the server/client + # """ + # if arg == 'COMMANDS': + # return self.evennia_to_msdp(arg, MSDP_COMMANDS) + # elif arg == 'LISTS': + # return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES", + # "REPORTED_VARIABLES", "SENDABLE_VARIABLES")) + # elif arg == 'CONFIGURABLE_VARIABLES': + # return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID")) + # elif arg == 'REPORTABLE_VARIABLES': + # return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys()) + # elif arg == 'REPORTED_VARIABLES': + # # the dynamically set items to report + # return self.evennia_to_msdp(arg, self.msdp_reported.keys()) + # elif arg == 'SENDABLE_VARIABLES': + # return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys()) + # else: + # return self.evennia_to_msdp("LIST", arg) - def msdp_cmd_report(self, *arg): - """ - The report command instructs the server to start reporting a - reportable variable to the client. - """ - try: - return MSDP_REPORTABLE[arg](report=True) - except Exception: - logger.log_trace() + # # default msdp commands - def msdp_cmd_unreport(self, arg): - """ - Unreport a previously reported variable - """ - try: - MSDP_REPORTABLE[arg](report=False) - except Exception: - self.logger.log_trace() + # def msdp_cmd_report(self, *arg): + # """ + # The report command instructs the server to start reporting a + # reportable variable to the client. + # """ + # try: + # return MSDP_REPORTABLE[arg](report=True) + # except Exception: + # logger.log_trace() - def msdp_cmd_reset(self, arg): - """ - The reset command resets a variable to its initial state. - """ - try: - MSDP_REPORTABLE[arg](reset=True) - except Exception: - logger.log_trace() + # def msdp_cmd_unreport(self, arg): + # """ + # Unreport a previously reported variable + # """ + # try: + # MSDP_REPORTABLE[arg](report=False) + # except Exception: + # self.logger.log_trace() - def msdp_cmd_send(self, arg): - """ - Request the server to send a particular variable - to the client. + # def msdp_cmd_reset(self, arg): + # """ + # The reset command resets a variable to its initial state. + # """ + # try: + # MSDP_REPORTABLE[arg](reset=True) + # except Exception: + # logger.log_trace() - arg - this is a list of variables the client wants. - """ - ret = [] - if arg: - for var in make_iter(arg): - try: - ret.append(MSDP_REPORTABLE[var.upper()])# (send=True)) - except Exception: - ret.append("ERROR")#logger.log_trace() - return ret + # def msdp_cmd_send(self, *args): + # """ + # Request the server to send a particular variable + # to the client. + + # arg - this is a list of variables the client wants. + # """ + # ret = [] + # for var in make_iter(arg) + + + + + # for var in make_iter(arg): + # try: + # ret.append(MSDP_REPORTABLE[var.upper()])# (send=True)) + # except Exception: + # ret.append("ERROR")#logger.log_trace() + # return ret diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index f70d6296f8..fffbf28c25 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -127,7 +127,6 @@ class PortalSessionHandler(SessionHandler): in from the protocol to the server. data is serialized before passed on. """ - #print "portal_data_in:", string self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid, msg=text, data=kwargs) diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index 21fa14fef8..ca886690e4 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -85,13 +85,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): be handled in line mode. Some clients also sends an erroneous line break after IAC, which we must watch out for. """ - #print "dataRcv (%s):" % data, - #try: - # for b in data: - # print ord(b), - # print "" - #except Exception, e: - # print str(e) + ":", str(data) if data and data[0] == IAC or self.iaw_mode: try: @@ -102,8 +95,16 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): else: self.iaw_mode = False return - except Exception: - logger.log_trace() + except Exception, err1: + conv = "" + try: + for b in data: + conv += " " + repr(ord(b)) + except Exception, err2: + conv = str(err2) + ":", str(data) + out = "Telnet Error (%s): %s (%s)" % (err1, data, conv) + logger.log_trace(out) + return # if we get to this point the command must end with a linebreak. # We make sure to add it, to fix some clients messing this up. data = data.rstrip("\r\n") + "\n" @@ -130,7 +131,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): Telnet method called when data is coming in over the telnet connection. We pass it on to the game engine directly. """ - self.sessionhandler.data_in(self, string) + self.data_in(text=string) # Session hooks @@ -144,11 +145,17 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.data_out(reason) self.connectionLost(reason) + def data_in(self, text=None, **kwargs): + """ + Data Telnet -> Server + """ + self.sessionhandler.data_in(self, text=text, **kwargs) + def data_out(self, text=None, **kwargs): """ + Data Evennia -> Player. generic hook method for engine to call in order to send data through the telnet connection. - Data Evennia -> Player. valid telnet kwargs: raw=True - pass string through without any ansi processing (i.e. include Evennia @@ -165,11 +172,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): if "oob" in kwargs: oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob")) if "MSDP" in self.protocol_flags: - print "oobstruct:", oobstruct - for cmdname, args in oobstruct: - print "cmdname, args:", cmdname, args - msdp_string = self.msdp.func_to_msdp(cmdname, args) - print "msdp_string:", msdp_string + 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) ttype = self.protocol_flags.get('TTYPE', {}) diff --git a/src/server/serversession.py b/src/server/serversession.py index 49dd89851b..5f395d557f 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -137,6 +137,14 @@ class ServerSession(Session): return self.logged_in and self.puppet get_character = get_puppet + def get_puppet_or_player(self): + """ + Returns session if not logged in; puppet if one exists, otherwise return the player. + """ + if self.logged_in: + return self.puppet if self.puppet else self.player + return None + def log(self, message, channel=True): """ Emits session info to the appropriate outputs and info channels. @@ -182,10 +190,11 @@ class ServerSession(Session): # handle oob instructions global _OOB_HANDLER if not _OOB_HANDLER: - from src.servever.oobhandler import OOB_HANDLER as _OOB_HANDLER + from src.server.oobhandler import OOB_HANDLER as _OOB_HANDLER oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None)) for (funcname, args, kwargs) in oobstruct: - _OOBHANDLER.execute_cmd(funcname, *args, **kwargs) + if funcname: + _OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs) execute_cmd = data_in # alias diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index fa4634bbba..0b5b4dddd1 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -97,6 +97,55 @@ 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 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(), tuple(oobstruct[1]), {}) + else: + # cmdname, (args,), {kwargs} + return (oobstruct[0].lower(), tuple(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 (tuple(_parse(oobstruct)),) + else: + out = [] + for oobpart in oobstruct: + out.append(_parse(oobpart)) + return (tuple(out),) + return (_parse(oobstruct),) + #------------------------------------------------------------ # Server-SessionHandler class #------------------------------------------------------------ @@ -358,48 +407,6 @@ class ServerSessionHandler(SessionHandler): return self.sessions.get(sessid) return None - def oobstruct_parser(self, oobstruct): - """ - Helper method for each session to use to parse oob structures - (The 'oob' kwarg of the msg() method) - allowed 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 - """ - 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((key.lower(), val) for key,val in oobstruct[1].items())) - elif isinstance(oobstruct[1], (tuple, list)): - # cmdname, (args,) - return (oobstruct[0].lower(), tuple(arg.lower() for arg in oobstruct[1]), {}) - else: - # cmdname, (args,), {kwargs} - return (oobstruct[0].lower(), tuple(arg.lower for arg in oobstruct[1]), - dict((key.lower(), val) for key, val in oobstruct[2].items())) - - # either multiple funcnames or multiple func tuples; descend recursively - out = [] - for oobpart in oobstruct: - out.append(self.oobstruct_parser(oobpart)[0]) - return tuple(out) def announce_all(self, message): """ diff --git a/src/settings_default.py b/src/settings_default.py index ec2a0ce384..84d2b9a230 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -199,7 +199,7 @@ MSSP_META_MODULE = "" # Module holding OOB (Out of Band) hook objects. This allows for customization # and expansion of which hooks OOB protocols are allowed to call on the server # protocols for attaching tracker hooks for when various object field change -OOB_PLUGIN_MODULE = "" +OOB_PLUGIN_MODULE = "src.server.oob_defaults" # Tuple of modules implementing lock functions. All callable functions # inside these modules will be available as lock functions. LOCK_FUNC_MODULES = ("src.locks.lockfuncs",) diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 77c44ba2ba..67c704567a 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -437,7 +437,7 @@ class NickHandler(AttributeHandler): with categories nick_ """ def has(self, key, category="inputline"): - categry = "nick_%s" % category + category = "nick_%s" % category return super(NickHandler, self).has(key, category=category) def get(self, key=None, category="inputline", **kwargs): @@ -462,6 +462,34 @@ class NickHandler(AttributeHandler): return super(NickHandler, self).all(category=category) return _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith="nick_") +class NAttributeHandler(object): + """ + This stand-alone handler manages non-database saved properties by storing them + as properties on obj.ndb. It has the same methods as AttributeHandler, but they + are much simplified. + """ + def __init__(self, obj): + "initialized on the object" + self.ndb = _GA(obj, "ndb") + def has(self, key): + "Check if object has this attribute or not" + return _GA(self.ndb, key) # ndb returns None if not found + def get(self, key): + "Returns named key value" + return _GA(self.ndb, key) + def add(self, key, value): + "Add new key and value" + _SA(self.ndb, key, value) + def remove(self, key): + "Remove key from storage" + _DA(self.ndb, key) + def all(self): + "List all keys stored" + if callable(self.ndb.all): + return self.ndb.all() + else: + return [val for val in self.ndb.__dict__.keys() if not val.startswith('_')] + #------------------------------------------------------------ # # Tags @@ -645,6 +673,7 @@ class TypedObject(SharedMemoryModel): _SA(self, "dbobj", self) # this allows for self-reference _SA(self, "locks", LockHandler(self)) _SA(self, "permissions", PermissionHandler(self)) + _SA(self, "nattributes", NAttributeHandler(self)) class Meta: """ @@ -1148,6 +1177,9 @@ class TypedObject(SharedMemoryModel): if hperm in perms and hpos > ppos) return False + # + # Memory management + # def flush_from_cache(self): """ @@ -1157,6 +1189,60 @@ class TypedObject(SharedMemoryModel): """ self.__class__.flush_cached_instance(self) + # + # Attribute storage + # + + #@property db + def __db_get(self): + """ + Attribute handler wrapper. Allows for the syntax + obj.db.attrname = value + and + value = obj.db.attrname + and + del obj.db.attrname + and + all_attr = obj.db.all (unless there is no attribute named 'all', in which + case that will be returned instead). + """ + try: + return self._db_holder + except AttributeError: + class DbHolder(object): + "Holder for allowing property access of attributes" + def __init__(self, obj): + _SA(self, 'obj', obj) + _SA(self, "attrhandler", _GA(_GA(self, "obj"), "attributes")) + def __getattribute__(self, attrname): + if attrname == 'all': + # we allow to overload our default .all + attr = _GA(self, "attrhandler").get("all") + if attr: + return attr + return _GA(self, 'all') + return _GA(self, "attrhandler").get(attrname) + def __setattr__(self, attrname, value): + _GA(self, "attrhandler").add(attrname, value) + def __delattr__(self, attrname): + _GA(self, "attrhandler").remove(attrname) + def get_all(self): + return _GA(self, "attrhandler").all() + all = property(get_all) + self._db_holder = DbHolder(self) + return self._db_holder + #@db.setter + def __db_set(self, value): + "Stop accidentally replacing the db object" + string = "Cannot assign directly to db object! " + string += "Use db.attr=value instead." + raise Exception(string) + #@db.deleter + def __db_del(self): + "Stop accidental deletion." + raise Exception("Cannot delete the db object!") + db = property(__db_get, __db_set, __db_del) + # # Non-persistent (ndb) storage # @@ -1202,35 +1288,12 @@ class TypedObject(SharedMemoryModel): raise Exception("Cannot delete the ndb object!") ndb = property(__ndb_get, __ndb_set, __ndb_del) - #def nattr(self, attribute_name=None, value=None, delete=False): - # """ - # This allows for assigning non-persistent data on the object using - # a method call. Will return None if trying to access a non-existing property. - # """ - # if attribute_name == None: - # # act as a list method - # if callable(self.ndb.all): - # return self.ndb.all() - # else: - # return [val for val in self.ndb.__dict__.keys() - # if not val.startswith['_']] - # elif delete == True: - # if hasattr(self.ndb, attribute_name): - # _DA(_GA(self, "ndb"), attribute_name) - # elif value == None: - # # act as a getter. - # if hasattr(self.ndb, attribute_name): - # _GA(_GA(self, "ndb"), attribute_name) - # else: - # return None - # else: - # # act as a setter - # _SA(self.ndb, attribute_name, value) + # - # Attribute handler methods - DEPRECATED! + # ***** DEPRECATED METHODS BELOW ******* # # @@ -1366,56 +1429,31 @@ class TypedObject(SharedMemoryModel): # creating a new attribute - check access on storing object! _GA(self, "attributes").add(attribute_name, value, accessing_obj=accessing_object, default_access=default_access_create) - #@property - def __db_get(self): + def nattr(self, attribute_name=None, value=None, delete=False): """ - A second convenience wrapper for the the attribute methods. It - allows for the syntax - obj.db.attrname = value - and - value = obj.db.attrname - and - del obj.db.attrname - and - all_attr = obj.db.all (unless there is no attribute named 'all', in which - case that will be returned instead). + This allows for assigning non-persistent data on the object using + a method call. Will return None if trying to access a non-existing property. """ - try: - return self._db_holder - except AttributeError: - class DbHolder(object): - "Holder for allowing property access of attributes" - def __init__(self, obj): - _SA(self, 'obj', obj) - _SA(self, "attrhandler", _GA(_GA(self, "obj"), "attributes")) - def __getattribute__(self, attrname): - if attrname == 'all': - # we allow to overload our default .all - attr = _GA(self, "attrhandler").get("all") - if attr: - return attr - return _GA(self, 'all') - return _GA(self, "attrhandler").get(attrname) - def __setattr__(self, attrname, value): - _GA(self, "attrhandler").add(attrname, value) - def __delattr__(self, attrname): - _GA(self, "attrhandler").remove(attrname) - def get_all(self): - return _GA(self, "attrhandler").all() - all = property(get_all) - self._db_holder = DbHolder(self) - return self._db_holder - #@db.setter - def __db_set(self, value): - "Stop accidentally replacing the db object" - string = "Cannot assign directly to db object! " - string += "Use db.attr=value instead." - raise Exception(string) - #@db.deleter - def __db_del(self): - "Stop accidental deletion." - raise Exception("Cannot delete the db object!") - db = property(__db_get, __db_set, __db_del) + logger.log_depmsg("obj.nattr() is deprecated. Use obj.nattributes instead.") + if attribute_name == None: + # act as a list method + if callable(self.ndb.all): + return self.ndb.all() + else: + return [val for val in self.ndb.__dict__.keys() + if not val.startswith['_']] + elif delete == True: + if hasattr(self.ndb, attribute_name): + _DA(_GA(self, "ndb"), attribute_name) + elif value == None: + # act as a getter. + if hasattr(self.ndb, attribute_name): + _GA(_GA(self, "ndb"), attribute_name) + else: + return None + else: + # act as a setter + _SA(self.ndb, attribute_name, value) diff --git a/src/utils/utils.py b/src/utils/utils.py index 368eb07764..3ef27553de 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -729,10 +729,10 @@ def mod_import(module): def all_from_module(module): """ - Return all global-level variables from a module + Return all global-level variables from a module as a dict """ mod = mod_import(module) - return [val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))] + return dict((key, val) for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))) def variable_from_module(module, variable=None, default=None): """ From 46c2e372bfdd49e16cfe83e7f548b47c2bfbfe8b Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 16 Oct 2013 23:39:04 +0200 Subject: [PATCH 4/8] Added working report functionality for db-fields. Not tested for Attributes yet. Also working oob-repeat functionality, but still a traceback at reload. --- src/server/oobhandler.py | 204 +++++++++++++++++++++----------------- src/server/portal/msdp.py | 92 ++++------------- src/server/server.py | 11 +- src/utils/dbserialize.py | 6 +- 4 files changed, 144 insertions(+), 169 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 6286b69ccf..0a5008ff8d 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -28,7 +28,7 @@ from src.server.models import ServerConfig from src.server.sessionhandler import SESSIONS from src.scripts.scripts import Script from src.utils.create import create_script -from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj +from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj from src.utils import logger from src.utils.utils import all_from_module, to_str, is_iter, make_iter @@ -57,25 +57,27 @@ class TrackerHandler(object): # initiate store only with valid on-object fieldnames self.tracktargets = dict((key, {}) for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")()) - def add(self, fieldname, trackerkey, trackerobj): + def add(self, fieldname, tracker): """ Add tracker to the handler. Raises KeyError if fieldname does not exist. """ - self.tracktargets[fieldname][trackerkey] = trackerobj + trackerkey = tracker.__class__.__name__ + self.tracktargets[fieldname][trackerkey] = tracker self.ntrackers += 1 - def remove(self, fieldname, trackerkey, *args, **kwargs): + def remove(self, fieldname, trackerclass, *args, **kwargs): """ Remove tracker from handler. Raises KeyError if tracker is not found. """ - oobobj = self.tracktargets[fieldname][trackerkey] + trackerkey = trackerclass.__name__ + tracker = self.tracktargets[fieldname][trackerkey] try: - oobobj.at_delete(*args, **kwargs) + tracker.at_delete(*args, **kwargs) except Exception: logger.log_trace() - del oobobj + del tracker self.ntrackers -= 1 if self.ntrackers <= 0: # if there are no more trackers, clean this handler @@ -85,9 +87,9 @@ class TrackerHandler(object): """ Called by the field when it updates to a new value """ - for trackerobj in self.tracktargets[fieldname].values(): + for tracker in self.tracktargets[fieldname].values(): try: - trackerobj.update(fieldname, new_value) + tracker.update(new_value) except Exception: logger.log_trace() @@ -104,82 +106,61 @@ class TrackerBase(object): "Called when tracker is removed" pass -# Default tracker OOB class - -class OOBTracker(TrackerBase): +class _RepeaterScript(Script): """ - A OOB object that passively sends data to a stored sessid whenever - a named database field changes. + Repeating and subscription-enabled script for triggering OOB + functions. Maintained in a _RepeaterPool. """ - def __init__(self, fieldname, sessid, *args, **kwargs): - """ - name - name of entity to track, such as "db_key" - track_type - one of "field", "prop" or "attr" for Database fields, - non-database Property or Attribute - sessid - sessid of session to report to - """ - self.fieldname = fieldname - self.sessid = sessid + def at_script_creation(self): + "Called when script is initialized" + self.key = "oob_func" + self.desc = "OOB functionality script" + self.persistent = False #oob scripts should always be non-persistent + self.ndb.subscriptions = {} - def update(self, new_value, *args, **kwargs): - "Called by cache when updating the tracked entitiy" - SESSIONS.session_from_sessid(self.sessid).msg(oob=("trackreturn", - (self.fieldname, new_value))) + def at_repeat(self): + """ + Calls subscriptions every self.interval seconds + """ + for (func_key, obj, session, interval, args, kwargs) in self.ndb.subscriptions.values(): + OOB_HANDLER.execute_cmd(session, func_key, *args, **kwargs) + + def subscribe(self, store_key, obj, session, func_key, interval, *args, **kwargs): + """ + Sign up a subscriber to this oobfunction. Subscriber is + a database object with a dbref. + """ + self.ndb.subscriptions[store_key] = (func_key, obj, session, interval, args, kwargs) + + def unsubscribe(self, store_key): + """ + Unsubscribe from oobfunction. Returns True if removal was + successful, False otherwise + """ + self.ndb.subscriptions.pop(store_key, None) class _RepeaterPool(object): """ This maintains a pool of _RepeaterScript scripts, ordered one per interval. It will automatically cull itself once a given interval's script has no more subscriptions. + + This is used and accessed from oobhandler.repeat/unrepeat """ - class _RepeaterScript(Script): - """ - Repeating script for triggering OOB functions. Maintained in the pool. - """ - def at_script_creation(self): - "Called when script is initialized" - self.key = "oob_func" - self.desc = "OOB functionality script" - self.persistent = False #oob scripts should always be non-persistent - self.ndb.subscriptions = {} - - def at_repeat(self): - """ - Calls subscriptions every self.interval seconds - """ - for (func_key, caller, interval, args, kwargs) in self.ndb.subscriptions.values(): - try: - _OOB_FUNCS[func_key](caller, *args, **kwargs) - except Exception: - logger.log_trace() - - def subscribe(self, store_key, caller, func_key, interval, *args, **kwargs): - """ - Sign up a subscriber to this oobfunction. Subscriber is - a database object with a dbref. - """ - self.ndb.subscriptions[store_key] = (func_key, caller, interval, args, kwargs) - - def unsubscribe(self, store_key): - """ - Unsubscribe from oobfunction. Returns True if removal was - successful, False otherwise - """ - self.ndb.subscriptions.pop(store_key, None) - def __init__(self): self.scripts = {} - def add(self, store_key, caller, func_key, interval, *args, **kwargs): + def add(self, store_key, obj, sessid, func_key, interval, *args, **kwargs): """ Add a new tracking """ if interval not in self.scripts: # if no existing interval exists, create new script to fill the gap - new_tracker = create_script(self._RepeaterScript, key="oob_repeater_%is" % interval, interval=interval) + new_tracker = create_script(_RepeaterScript, key="oob_repeater_%is" % interval, interval=interval) self.scripts[interval] = new_tracker - self.scripts[interval].subscribe(store_key, caller, func_key, interval, *args, **kwargs) + session = SESSIONS.session_from_sessid(sessid) + self.scripts[interval].subscribe(store_key, obj, session, func_key, interval, *args, **kwargs) def remove(self, store_key, interval): """ @@ -215,8 +196,10 @@ class OOBHandler(object): ServerConf field """ if self.oob_tracker_storage: + print "saved tracker_storage:", self.oob_tracker_storage ServerConfig.objects.conf(key="oob_tracker_storage", value=dbserialize(self.oob_tracker_storage)) if self.oob_repeat_storage: + print "saved repeat_storage:", self.oob_repeat_storage ServerConfig.objects.conf(key="oob_repeat_storage", value=dbserialize(self.oob_repeat_storage)) def restore(self): @@ -228,17 +211,23 @@ class OOBHandler(object): tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage") if tracker_storage: self.oob_tracker_storage = dbunserialize(tracker_storage) - for tracker_key, (obj, sessid, fieldname, args, kwargs) in self.oob_tracker_storage.items(): - self.track(obj, sessid, fieldname, tracker_key, *args, **kwargs) + print "recovered from tracker_storage:", self.oob_tracker_storage + for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values(): + self.track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs) + # make sure to purce the storage + ServerConfig.objects.conf(key="oob_tracker_storage", delete=True) repeat_storage = ServerConfig.objects.conf(key="oob_repeat_storage") if repeat_storage: self.oob_repeat_storage = dbunserialize(repeat_storage) - for func_key, (caller, func_key, interval, args, kwargs) in self.oob_repeat_storage.items(): - self.repeat(caller, func_key, interval, *args, **kwargs) + print "recovered from repeat_storage:", self.oob_repeat_storage + for (obj, sessid, func_key, interval, args, kwargs) in self.oob_repeat_storage.values(): + self.repeat(unpack_dbobj(obj), sessid, func_key, interval, *args, **kwargs) + # make sure to purge the storage + ServerConfig.objects.conf(key="oob_repeat_storage", delete=True) - def track(self, obj, sessid, fieldname, oobclass, *args, **kwargs): + def track(self, obj, sessid, fieldname, trackerclass, *args, **kwargs): """ Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args, kwargs will be used to initialize the OOB hook before adding @@ -247,46 +236,59 @@ class OOBHandler(object): will be used as the property name when assigning the OOB to obj, otherwise tracker_key is ysed as the property name. """ - oobclass = _OOB_TRACKERS[tracker_key] # raise traceback if not found + try: + obj = obj.dbobj + except AttributeError: + pass if not "_trackerhandler" in _GA(obj, "__dict__"): # assign trackerhandler to object _SA(obj, "_trackerhandler", TrackerHandler(obj)) # initialize object - oob = oobclass(obj, sessid, fieldname, *args, **kwargs) - _GA(obj, "_trackerhandler").add(oob, fieldname) + tracker = trackerclass(self, fieldname, sessid, *args, **kwargs) + _GA(obj, "_trackerhandler").add(fieldname, tracker) # store calling arguments as a pickle for retrieval later - storekey = (pack_dbobj(obj), sessid, fieldname) - stored = (obj, sessid, fieldname, args, kwargs) + obj_packed = pack_dbobj(obj) + storekey = (obj_packed, sessid, fieldname) + stored = (obj_packed, sessid, fieldname, trackerclass, args, kwargs) self.oob_tracker_storage[storekey] = stored - def untrack(self, obj, sessid, fieldname, tracker_key, *args, **kwargs): + def untrack(self, obj, sessid, fieldname, trackerclass, *args, **kwargs): """ Remove the OOB from obj. If oob implements an at_delete hook, this will be called with args, kwargs """ + try: + obj = obj.dbobj + except AttributeError: + pass + try: # call at_delete hook - _GA(obj, "_trackerhandler").remove(fieldname, tracker_key, *args, **kwargs) + _GA(obj, "_trackerhandler").remove(fieldname, trackerclass, *args, **kwargs) except AttributeError: pass # remove the pickle from storage store_key = (pack_dbobj(obj), sessid, fieldname) self.oob_tracker_storage.pop(store_key, None) - def track_field(self, obj, sessid, field_name, tracker_key="oobtracker"): + def track_field(self, obj, sessid, field_name, trackerclass): """ Shortcut wrapper method for specifically tracking a database field. - Uses OOBTracker by default (change tracker_key to redirect) - Will create a tracker with a property name that the field cache - expects + Takes the tracker class as argument. """ # all database field names starts with db_* field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name - oob_tracker_name = "_track_%s_change" % field_name # field cache looks for name on this form - self.track(obj, tracker_key, field_name, sessid, property_name=oob_tracker_name) + self.track(obj, sessid, field_name, trackerclass) - def track_attribute(self, obj, sessid, attr_name, tracker_key="oobtracker"): + def untrack_field(self, obj, sessid, field_name): + """ + Shortcut for untracking a database field. Uses OOBTracker by defualt + """ + field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name + self.untrack(obj, sessid, field_name) + + def track_attribute(self, obj, sessid, attr_name, trackerclass): """ Shortcut wrapper method for specifically tracking the changes of an Attribute on an object. Will create a tracker on the Attribute Object and @@ -295,10 +297,17 @@ class OOBHandler(object): # get the attribute object if we can attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: - oob_tracker_name = "_track_db_value_change" - self.track(attrobj, tracker_key, attr_name, sessid, property_name=oob_tracker_name) + self.track(attrobj, sessid, attr_name, trackerclass) - def repeat(self, caller, func_key, interval=20, *args, **kwargs): + def untrack_attribute(self, obj, sessid, attr_name, tracker_key="oobtracker"): + """ + Shortcut for deactivating tracking for a given attribute. + """ + attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) + if attrobj: + self.untrack(attrobj, sessid, attr_name) + + def repeat(self, obj, sessid, func_key, interval=20, *args, **kwargs): """ Start a repeating action. Every interval seconds, the oobfunc corresponding to func_key is called with @@ -306,22 +315,32 @@ class OOBHandler(object): """ if not func_key in _OOB_FUNCS: raise KeyError("%s is not a valid OOB function name.") - store_key = (pack_dbobj(caller), func_key, interval) + try: + obj = obj.dbobj + except AttributeError: + pass + store_obj = pack_dbobj(obj) + store_key = (store_obj, sessid, func_key, interval) # prepare to store - self.oob_repeat_storage[store_key] = (caller, func_key, interval, args, kwargs) - self.oob_tracker_pool.add(store_key, caller, func_key, interval, *args, **kwargs) + self.oob_repeat_storage[store_key] = (store_obj, sessid, func_key, interval, args, kwargs) + self.oob_tracker_pool.add(store_key, obj, sessid, func_key, interval, *args, **kwargs) - def unrepeat(self, caller, func_key, interval=20): + def unrepeat(self, obj, sessid, func_key, interval=20): """ Stop a repeating action """ - store_key = (pack_dbobj(caller), func_key, interval) + try: + obj = obj.dbobj + except AttributeError: + pass + store_key = (pack_dbobj(obj), sessid, func_key, interval) self.oob_tracker_pool.remove(store_key, interval) self.oob_repeat_storage.pop(store_key, None) def msg(self, sessid, funcname, *args, **kwargs): "Shortcut to relay oob data back to portal" session = self.sessionhandler.session_from_sessid(sessid) + print "oobhandler msg:", sessid, session, funcname, args, kwargs if session: session.msg(oob=(funcname, args, kwargs)) @@ -331,6 +350,7 @@ class OOBHandler(object): using *args and **kwargs """ try: + print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys() oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found oobfunc(self, session, *args, **kwargs) except KeyError: diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 6c21ac2800..6aea76b570 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -45,69 +45,6 @@ MSDP_COMMANDS_CUSTOM = {} # this maps MSDP command names to Evennia commands found in OOB_FUNC_MODULE. It # is up to these commands to return data on proper form. This is overloaded if # OOB_REPORTABLE is defined in the custom OOB module below. -MSDP_REPORTABLE = { - # General - "CHARACTER_NAME": "get_character_name", - "SERVER_ID": "get_server_id", - "SERVER_TIME": "get_server_time", - # Character - "AFFECTS": "char_affects", - "ALIGNMENT": "char_alignment", - "EXPERIENCE": "char_experience", - "EXPERIENCE_MAX": "char_experience_max", - "EXPERIENCE_TNL": "char_experience_tnl", - "HEALTH": "char_health", - "HEALTH_MAX": "char_health_max", - "LEVEL": "char_level", - "RACE": "char_race", - "CLASS": "char_class", - "MANA": "char_mana", - "MANA_MAX": "char_mana_max", - "WIMPY": "char_wimpy", - "PRACTICE": "char_practice", - "MONEY": "char_money", - "MOVEMENT": "char_movement", - "MOVEMENT_MAX": "char_movement_max", - "HITROLL": "char_hitroll", - "DAMROLL": "char_damroll", - "AC": "char_ac", - "STR": "char_str", - "INT": "char_int", - "WIS": "char_wis", - "DEX": "char_dex", - "CON": "char_con", - # Combat - "OPPONENT_HEALTH": "opponent_health", - "OPPONENT_HEALTH_MAX":"opponent_health_max", - "OPPONENT_LEVEL": "opponent_level", - "OPPONENT_NAME": "opponent_name", - # World - "AREA_NAME": "area_name", - "ROOM_EXITS": "area_room_exits", - "ROOM_NAME": "room_name", - "ROOM_VNUM": "room_dbref", - "WORLD_TIME": "world_time", - # Configurable variables - "CLIENT_ID": "client_id", - "CLIENT_VERSION": "client_version", - "PLUGIN_ID": "plugin_id", - "ANSI_COLORS": "ansi_colours", - "XTERM_256_COLORS": "xterm_256_colors", - "UTF_8": "utf_8", - "SOUND": "sound", - "MXP": "mxp", - # GUI variables - "BUTTON_1": "button1", - "BUTTON_2": "button2", - "BUTTON_3": "button3", - "BUTTON_4": "button4", - "BUTTON_5": "button5", - "GAUGE_1": "gauge1", - "GAUGE_2": "gauge2", - "GAUGE_3": "gauge3", - "GAUGE_4": "gauge4", - "GAUGE_5": "gauge5"} -MSDP_SENDABLE = MSDP_REPORTABLE # try to load custom OOB module OOB_MODULE = None#mod_import(settings.OOB_FUNC_MODULE) @@ -156,10 +93,13 @@ class Msdp(object): converted to a string), a list (will be converted to an MSDP_ARRAY), or a dictionary (will be converted to MSDP_TABLE). - Obs - this normally only returns tables and lists (var val val ...) rather than - arrays. It will convert *args to lists and **kwargs to tables and - if both are given to this method, this will result in a list followed - by a table, both having the same names. + 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): @@ -173,7 +113,7 @@ class Msdp(object): else: string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) string += MSDP_TABLE_CLOSE - return string + return stringk def make_array(name, *args): "build a array. Arrays may not nest tables by definition." @@ -188,8 +128,17 @@ class Msdp(object): 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": self.data_out(make_list("LIST", *args)) @@ -200,15 +149,14 @@ class Msdp(object): elif cupper == "RESET": self.data_out(make_list("RESET", *args)) elif cupper == "SEND": - self.data_out(make_list("SEND", *args)) + self.data_out(make_named_list("SEND", **kwargs)) else: - # return list or tables. If both arg/kwarg is given, return one array and one table, both - # with the same name. + # return list or named lists. msdp_string = "" if args: msdp_string += make_list(cupper, *args) if kwargs: - msdp_string += make_table(cupper, **kwargs) + msdp_string += make_named_list(cupper, **kwargs) self.data_out(msdp_string) def msdp_to_evennia(self, data): diff --git a/src/server/server.py b/src/server/server.py index 21429e4b53..e900b0694a 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -214,10 +214,14 @@ class Evennia(object): [(o.typeclass, o.at_init()) for o in ObjectDB.get_all_cached_instances()] [(p.typeclass, p.at_init()) for p in PlayerDB.get_all_cached_instances()] + with open(SERVER_RESTART, 'r') as f: + mode = f.read() + if mode in ('True', 'reload'): + from src.server.oobhandler import OOB_HANDLER + OOB_HANDLER.restore() + if SERVER_STARTSTOP_MODULE: # call correct server hook based on start file value - with open(SERVER_RESTART, 'r') as f: - mode = f.read() if mode in ('True', 'reload'): # True was the old reload flag, kept for compatibilty SERVER_STARTSTOP_MODULE.at_server_reload_start() @@ -280,6 +284,9 @@ class Evennia(object): yield self.sessions.all_sessions_portal_sync() ServerConfig.objects.conf("server_restart_mode", "reload") + from src.server.oobhandler import OOB_HANDLER + OOB_HANDLER.save() + if SERVER_STARTSTOP_MODULE: SERVER_STARTSTOP_MODULE.at_server_reload_stop() diff --git a/src/utils/dbserialize.py b/src/utils/dbserialize.py index 73308a2eeb..fa2fc9b205 100644 --- a/src/utils/dbserialize.py +++ b/src/utils/dbserialize.py @@ -196,7 +196,7 @@ def pack_dbobj(item): # build the internal representation as a tuple ("__packed_dbobj__", key, creation_time, id) return natural_key and ('__packed_dbobj__', natural_key, _TO_DATESTRING(obj), _GA(obj, "id")) or item -def _unpack_dbobj(item): +def unpack_dbobj(item): """ Check and convert internal representations back to Django database models. The fact that item is a packed dbobj should be checked before this call. @@ -267,7 +267,7 @@ def from_pickle(data, db_obj=None): return item elif _IS_PACKED_DBOBJ(item): # this must be checked before tuple - return _unpack_dbobj(item) + return unpack_dbobj(item) elif dtype == tuple: return tuple(process_item(val) for val in item) elif dtype == dict: @@ -289,7 +289,7 @@ def from_pickle(data, db_obj=None): return item elif _IS_PACKED_DBOBJ(item): # this must be checked before tuple - return _unpack_dbobj(item) + return unpack_dbobj(item) elif dtype == tuple: return tuple(process_tree(val) for val in item) elif dtype == list: From e521501cb9c060be8536f656f6a919bca2882540 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 17 Oct 2013 16:11:42 +0200 Subject: [PATCH 5/8] Tested working oob repeat functionality. --- src/server/oobhandler.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 0a5008ff8d..e9f47b9985 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -122,15 +122,16 @@ class _RepeaterScript(Script): """ Calls subscriptions every self.interval seconds """ - for (func_key, obj, session, interval, args, kwargs) in self.ndb.subscriptions.values(): + for (func_key, sessid, interval, args, kwargs) in self.ndb.subscriptions.values(): + session = SESSIONS.session_from_sessid(sessid) OOB_HANDLER.execute_cmd(session, func_key, *args, **kwargs) - def subscribe(self, store_key, obj, session, func_key, interval, *args, **kwargs): + def subscribe(self, store_key, sessid, func_key, interval, *args, **kwargs): """ Sign up a subscriber to this oobfunction. Subscriber is a database object with a dbref. """ - self.ndb.subscriptions[store_key] = (func_key, obj, session, interval, args, kwargs) + self.ndb.subscriptions[store_key] = (func_key, sessid, interval, args, kwargs) def unsubscribe(self, store_key): """ @@ -151,7 +152,7 @@ class _RepeaterPool(object): def __init__(self): self.scripts = {} - def add(self, store_key, obj, sessid, func_key, interval, *args, **kwargs): + def add(self, store_key, sessid, func_key, interval, *args, **kwargs): """ Add a new tracking """ @@ -159,8 +160,7 @@ class _RepeaterPool(object): # if no existing interval exists, create new script to fill the gap new_tracker = create_script(_RepeaterScript, key="oob_repeater_%is" % interval, interval=interval) self.scripts[interval] = new_tracker - session = SESSIONS.session_from_sessid(sessid) - self.scripts[interval].subscribe(store_key, obj, session, func_key, interval, *args, **kwargs) + self.scripts[interval].subscribe(store_key, sessid, func_key, interval, *args, **kwargs) def remove(self, store_key, interval): """ @@ -172,6 +172,14 @@ class _RepeaterPool(object): # no more subscriptions for this interval. Clean out the script. self.scripts[interval].stop() + def stop(self): + """ + Stop all scripts in pool. This is done at server reload since restoring the pool + will automatically re-populate the pool. + """ + for script in self.scripts.values(): + script.stop() + # Main OOB Handler @@ -201,6 +209,7 @@ class OOBHandler(object): if self.oob_repeat_storage: print "saved repeat_storage:", self.oob_repeat_storage ServerConfig.objects.conf(key="oob_repeat_storage", value=dbserialize(self.oob_repeat_storage)) + self.oob_tracker_pool.stop() def restore(self): """ @@ -226,7 +235,6 @@ class OOBHandler(object): # make sure to purge the storage ServerConfig.objects.conf(key="oob_repeat_storage", delete=True) - def track(self, obj, sessid, fieldname, trackerclass, *args, **kwargs): """ Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args, @@ -323,7 +331,7 @@ class OOBHandler(object): store_key = (store_obj, sessid, func_key, interval) # prepare to store self.oob_repeat_storage[store_key] = (store_obj, sessid, func_key, interval, args, kwargs) - self.oob_tracker_pool.add(store_key, obj, sessid, func_key, interval, *args, **kwargs) + self.oob_tracker_pool.add(store_key, sessid, func_key, interval, *args, **kwargs) def unrepeat(self, obj, sessid, func_key, interval=20): """ @@ -344,6 +352,8 @@ class OOBHandler(object): if session: session.msg(oob=(funcname, args, kwargs)) + # access method - called from msg() + def execute_cmd(self, session, func_key, *args, **kwargs): """ Retrieve oobfunc from OOB_FUNCS and execute it immediately From 57e7c6a11e7ac888aec6480553cfc9fc3091f052 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 17 Oct 2013 17:11:24 +0200 Subject: [PATCH 6/8] Fixed removal of oob repeats. --- src/server/oobhandler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index e9f47b9985..cb9840f2be 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -307,13 +307,13 @@ class OOBHandler(object): if attrobj: self.track(attrobj, sessid, attr_name, trackerclass) - def untrack_attribute(self, obj, sessid, attr_name, tracker_key="oobtracker"): + def untrack_attribute(self, obj, sessid, attr_name, trackerclass): """ Shortcut for deactivating tracking for a given attribute. """ attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: - self.untrack(attrobj, sessid, attr_name) + self.untrack(attrobj, sessid, attr_name, trackerclass) def repeat(self, obj, sessid, func_key, interval=20, *args, **kwargs): """ @@ -348,7 +348,7 @@ class OOBHandler(object): def msg(self, sessid, funcname, *args, **kwargs): "Shortcut to relay oob data back to portal" session = self.sessionhandler.session_from_sessid(sessid) - print "oobhandler msg:", sessid, session, funcname, args, kwargs + #print "oobhandler msg:", sessid, session, funcname, args, kwargs if session: session.msg(oob=(funcname, args, kwargs)) @@ -360,7 +360,7 @@ class OOBHandler(object): using *args and **kwargs """ try: - print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys() + #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys() oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found oobfunc(self, session, *args, **kwargs) except KeyError: From d7fc0c7e160388ce30dbd9c531b1eaf599215da8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 17 Oct 2013 19:07:01 +0200 Subject: [PATCH 7/8] Made OOB tracking work for Attributes. Tested repeat across reloads. --- src/server/oobhandler.py | 40 ++++++++++++++++++++++------------------ src/utils/dbserialize.py | 4 +++- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index cb9840f2be..949dfe5a63 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -52,7 +52,9 @@ class TrackerHandler(object): """ This is initiated and stored on the object as a property _trackerhandler. """ - self.obj = obj.dbobj + try: obj = obj.dbobj + except AttributeError: pass + self.obj = obj self.ntrackers = 0 # initiate store only with valid on-object fieldnames self.tracktargets = dict((key, {}) for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")()) @@ -204,10 +206,10 @@ class OOBHandler(object): ServerConf field """ if self.oob_tracker_storage: - print "saved tracker_storage:", self.oob_tracker_storage + #print "saved tracker_storage:", self.oob_tracker_storage ServerConfig.objects.conf(key="oob_tracker_storage", value=dbserialize(self.oob_tracker_storage)) if self.oob_repeat_storage: - print "saved repeat_storage:", self.oob_repeat_storage + #print "saved repeat_storage:", self.oob_repeat_storage ServerConfig.objects.conf(key="oob_repeat_storage", value=dbserialize(self.oob_repeat_storage)) self.oob_tracker_pool.stop() @@ -220,7 +222,7 @@ class OOBHandler(object): tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage") if tracker_storage: self.oob_tracker_storage = dbunserialize(tracker_storage) - print "recovered from tracker_storage:", self.oob_tracker_storage + #print "recovered from tracker_storage:", self.oob_tracker_storage for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values(): self.track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs) # make sure to purce the storage @@ -229,7 +231,7 @@ class OOBHandler(object): repeat_storage = ServerConfig.objects.conf(key="oob_repeat_storage") if repeat_storage: self.oob_repeat_storage = dbunserialize(repeat_storage) - print "recovered from repeat_storage:", self.oob_repeat_storage + #print "recovered from repeat_storage:", self.oob_repeat_storage for (obj, sessid, func_key, interval, args, kwargs) in self.oob_repeat_storage.values(): self.repeat(unpack_dbobj(obj), sessid, func_key, interval, *args, **kwargs) # make sure to purge the storage @@ -242,19 +244,17 @@ class OOBHandler(object): it to obj. If property_key is not given, but the OOB has a class property property_name, this will be used as the property name when assigning the OOB to - obj, otherwise tracker_key is ysed as the property name. + obj, otherwise tracker_key is used as the property name. """ - try: - obj = obj.dbobj - except AttributeError: - pass + try: obj = obj.dbobj + except AttributeError: pass + if not "_trackerhandler" in _GA(obj, "__dict__"): # assign trackerhandler to object _SA(obj, "_trackerhandler", TrackerHandler(obj)) # initialize object tracker = trackerclass(self, fieldname, sessid, *args, **kwargs) _GA(obj, "_trackerhandler").add(fieldname, tracker) - # store calling arguments as a pickle for retrieval later obj_packed = pack_dbobj(obj) storekey = (obj_packed, sessid, fieldname) @@ -266,10 +266,8 @@ class OOBHandler(object): Remove the OOB from obj. If oob implements an at_delete hook, this will be called with args, kwargs """ - try: - obj = obj.dbobj - except AttributeError: - pass + try: obj = obj.dbobj + except AttributeError: pass try: # call at_delete hook @@ -303,14 +301,18 @@ class OOBHandler(object): name in a way the Attribute expects. """ # get the attribute object if we can + try: obj = obj.dbobj + except AttributeError: pass attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: - self.track(attrobj, sessid, attr_name, trackerclass) + self.track(attrobj, sessid, "db_value", trackerclass, attr_name) def untrack_attribute(self, obj, sessid, attr_name, trackerclass): """ Shortcut for deactivating tracking for a given attribute. """ + try: obj = obj.dbobj + except AttributeError: pass attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: self.untrack(attrobj, sessid, attr_name, trackerclass) @@ -363,17 +365,19 @@ class OOBHandler(object): #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys() oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found oobfunc(self, session, *args, **kwargs) - except KeyError: - errmsg = "OOB Error: function '%s' not recognized." % func_key + except KeyError,e: + errmsg = "OOB Error: function '%s' not recognized: %s" % (func_key, e) if _OOB_ERROR: _OOB_ERROR(self, session, errmsg, *args, **kwargs) else: logger.log_trace(errmsg) + raise except Exception, err: errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (func_key, args, kwargs, err) if _OOB_ERROR: _OOB_ERROR(self, session, errmsg, *args, **kwargs) else: logger.log_trace(errmsg) + raise # access object OOB_HANDLER = OOBHandler() diff --git a/src/utils/dbserialize.py b/src/utils/dbserialize.py index fa2fc9b205..61e402010d 100644 --- a/src/utils/dbserialize.py +++ b/src/utils/dbserialize.py @@ -209,7 +209,9 @@ def unpack_dbobj(item): except ObjectDoesNotExist: return None # even if we got back a match, check the sanity of the date (some databases may 're-use' the id) - return _TO_DATESTRING(obj.dbobj) == item[2] and obj or None + try: dbobj = obj.dbobj + except AttributeError: dbobj = obj + return _TO_DATESTRING(dbobj) == item[2] and obj or None # # Access methods From 6f8d1f9ce1cdd24e8518a54bfd7e22f349bd895e Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 17 Oct 2013 20:15:48 +0200 Subject: [PATCH 8/8] Added some more msdp commands --- src/server/oobhandler.py | 2 +- src/server/portal/msdp.py | 28 ++++------------------------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 949dfe5a63..2bdaaad234 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -37,7 +37,7 @@ _GA = object.__getattribute__ _DA = object.__delattr__ # load from plugin module -_OOB_FUNCS = dict((key, func) for key, func in all_from_module(settings.OOB_PLUGIN_MODULE).items() if isfunction(func)) +_OOB_FUNCS = dict((key.lower(), func) for key, func in all_from_module(settings.OOB_PLUGIN_MODULE).items() if isfunction(func)) _OOB_ERROR = _OOB_FUNCS.get("oob_error", None) diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 6aea76b570..08b73da480 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -7,8 +7,6 @@ http://tintin.sourceforge.net/msdp/. MSDP manages out-of-band communication between the client and server, for updating health bars etc. -!TODO - this is just a partial implementation and not used by telnet yet. - """ import re from django.conf import settings @@ -36,27 +34,6 @@ regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE regex_var = re.compile(MSDP_VAR) regex_val = re.compile(MSDP_VAL) -# MSDP default definition commands supported by Evennia (can be supplemented with custom commands as well) -MSDP_COMMANDS = ("LIST", "REPORT", "RESET", "SEND", "UNREPORT") - -# fallbacks if no custom OOB module is available -MSDP_COMMANDS_CUSTOM = {} -# MSDP_REPORTABLE is a standard suggestions for making it easy to create generic guis. -# this maps MSDP command names to Evennia commands found in OOB_FUNC_MODULE. It -# is up to these commands to return data on proper form. This is overloaded if -# OOB_REPORTABLE is defined in the custom OOB module below. - -# try to load custom OOB module -OOB_MODULE = None#mod_import(settings.OOB_FUNC_MODULE) -if OOB_MODULE: - # loading customizations from OOB_FUNC_MODULE if available - try: MSDP_REPORTABLE = OOB_MODULE.OOB_REPORTABLE # replaces the default MSDP definitions - except AttributeError: pass - try: MSDP_SENDABLE = OOB_MODULE.OOB_SENDABLE - except AttributeError: MSDP_SENDABLE = MSDP_REPORTABLE - try: MSDP_COMMANDS_CUSTOM = OOB_MODULE.OOB_COMMANDS - except: pass - # Msdp object handler class Msdp(object): @@ -141,7 +118,10 @@ class Msdp(object): cupper = cmdname.upper() if cupper == "LIST": - self.data_out(make_list("LIST", *args)) + 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":