From bdcc8de5bc63e98cd6b977ed45a6fdb5ccd7eb9b Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 15 Oct 2013 20:00:18 +0200 Subject: [PATCH] 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): """