From b0b0fa7983e7c98a4f0ec1db6835ab84b24493a9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 3 Jan 2013 09:18:49 +0100 Subject: [PATCH] First, untested version of the OOBhandler mechanism. --- src/scripts/scripts.py | 7 ++ src/server/caches.py | 115 ++++++++++++++++-- src/server/models.py | 2 +- src/server/msdp.py | 194 ++++++++++++++++--------------- src/server/oobhandler.py | 224 ++++++++++++++++++++++++++++++++++++ src/server/server.py | 6 +- src/server/serversession.py | 4 +- src/server/telnet.py | 1 - src/typeclasses/models.py | 7 +- 9 files changed, 454 insertions(+), 106 deletions(-) create mode 100644 src/server/oobhandler.py diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index 334f43bdc0..75fc9ca203 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -396,6 +396,13 @@ class DoNothing(Script): self.key = "sys_do_nothing" self.desc = _("This is an empty placeholder script.") +class Store(Script): + "Simple storage script" + def at_script_creation(self): + "Setup the script" + self.key = "sys_storage" + self.desc = _("This is a generic storage container.") + class CheckSessions(Script): "Check sessions regularly." def at_script_creation(self): diff --git a/src/server/caches.py b/src/server/caches.py index f40048c4af..4e84afab31 100644 --- a/src/server/caches.py +++ b/src/server/caches.py @@ -5,7 +5,6 @@ Central caching module. from sys import getsizeof from collections import defaultdict -from weakref import WeakKeyDictionary _GA = object.__getattribute__ _SA = object.__setattr__ @@ -16,6 +15,14 @@ _ATTR_CACHE = defaultdict(dict) _FIELD_CACHE = defaultdict(dict) _PROP_CACHE = defaultdict(dict) +# OOB hooks +_OOB_FIELD_UPDATE_HOOKS = defaultdict(dict) +_OOB_PROP_UPDATE_HOOKS = defaultdict(dict) +_OOB_ATTR_UPDATE_HOOKS = defaultdict(dict) +_OOB_NDB_UPDATE_HOOKS = defaultdict(dict) +_OOB_CUSTOM_UPDATE_HOOKS = defaultdict(dict) + +_OOB_HANDLER = None # set by oob handler when it initializes def get_cache_sizes(): """ @@ -46,16 +53,71 @@ def hashid(obj): try: hid = _GA(obj, "_hashid") except AttributeError: - date, idnum = _GA(obj, "db_date_created"), _GA(obj, "id") - if not idnum or not date: - # this will happen if setting properties on an object - # which is not yet saved - return None + try: + date, idnum = _GA(obj, "db_date_created"), _GA(obj, "id") + if not idnum or not date: + # this will happen if setting properties on an object + # which is not yet saved + return None + except AttributeError: + # this happens if hashing something like ndb. We have to + # rely on memory adressing in this case. + date, idnum = "Nondb", id(obj) # build the hashid hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum) _SA(obj, "_hashid", hid) return hid +# oob helper functions +def register_oob_update_hook(obj,name, entity="field"): + """ + Register hook function to be called when field/property/db/ndb is updated. + Given function will be called with function(obj, entityname, newvalue, *args, **kwargs) + entity - one of "field", "property", "db", "ndb" or "custom" + """ + hid = hashid(obj) + if hid: + if entity == "field": + global _OOB_FIELD_UPDATE_HOOKS + _OOB_FIELD_UPDATE_HOOKS[hid][name] = True + elif entity == "property": + global _OOB_PROP_UPDATE_HOOKS + _OOB_PROP_UPDATE_HOOKS[hid][name] = True + elif entity == "db": + global _OOB_ATTR_UPDATE_HOOKS + _OOB_ATTR_UPDATE_HOOKS[hid][name] = True + elif entity == "ndb": + global _OOB_NDB_UPDATE_HOOKS + _OOB_NDB_UPDATE_HOOKS[hid][name] = True + elif entity == "custom": + global _OOB_CUSTOM_UPDATE_HOOKS + _OOB_CUSTOM_UPDATE_HOOKS[hid][name] = True + else: + return None + return hid + +def unregister_oob_update_hook(obj, name, entity="property"): + """ + Un-register a report hook + """ + hid = hashid(obj) + if hid: + global _OOB_FIELD_UPDATE_HOOKS,_OOB_PROP_UPDATE_HOOKS, _OOB_ATTR_UPDATE_HOOKS + global _OOB_CUSTOM_UPDATE_HOOKS, _OOB_NDB_UPDATE_HOOKS + if entity == "field" and name in _OOB_FIELD_UPDATE_HOOKS: + del _OOB_FIELD_UPDATE_HOOKS[hid][name] + elif entity == "property" and name in _OOB_PROP_UPDATE_HOOKS: + del _OOB_PROP_UPDATE_HOOKS[hid][name] + elif entity == "db" and name in _OOB_ATTR_UPDATE_HOOKS: + del _OOB_ATTR_UPDATE_HOOKS[hid][name] + elif entity == "ndb" and name in _OOB_NDB_UPDATE_HOOKS: + del _OOB_NDB_UPDATE_HOOKS[hid][name] + elif entity == "custom" and name in _OOB_CUSTOM_UPDATE_HOOKS: + del _OOB_CUSTOM_UPDATE_HOOKS[hid][name] + else: + return None + return hid + # on-object database field cache def get_field_cache(obj, name): "On-model Cache handler." @@ -78,6 +140,9 @@ def set_field_cache(obj, name, val): if hid: global _FIELD_CACHE _FIELD_CACHE[hid][name] = val + # oob hook functionality + if _OOB_FIELD_UPDATE_HOOKS[hid].get(name): + _OOB_HANDLER.update(hid, name, val) def del_field_cache(obj, name): "On-model cache deleter" @@ -110,7 +175,7 @@ def get_prop_cache(obj, name, default=None): hid = hashid(obj) if hid: try: - return _PROP_CACHE[hid][name] + val = _PROP_CACHE[hid][name] except KeyError: return default _PROP_CACHE[hid][name] = val @@ -123,6 +188,11 @@ def set_prop_cache(obj, name, val): if hid: global _PROP_CACHE _PROP_CACHE[hid][name] = val + # oob hook functionality + oob_hook = _OOB_PROP_UPDATE_HOOKS[hid].get(name) + if oob_hook: + oob_hook[0](obj.typeclass, name, val, *oob_hook[1], **oob_hook[2]) + def del_prop_cache(obj, name): "On-model cache deleter" @@ -130,7 +200,7 @@ def del_prop_cache(obj, name): del _PROP_CACHE[hashid(obj)][name] except KeyError: pass -def flush_field_cache(obj=None): +def flush_prop_cache(obj=None): "On-model cache resetter" hid = hashid(obj) global _PROP_CACHE @@ -152,10 +222,14 @@ def set_attr_cache(obj, attrname, attrobj): """ Cache an attribute object """ - global _ATTR_CACHE hid = hashid(obj) if hid: + global _ATTR_CACHE _ATTR_CACHE[hid][attrname] = attrobj + # oob hook functionality + oob_hook = _OOB_ATTR_UPDATE_HOOKS[hid].get(attrname) + if oob_hook: + oob_hook[0](obj.typeclass, attrname, attrobj.value, *oob_hook[1], **oob_hook[2]) def del_attr_cache(obj, attrname): """ @@ -177,3 +251,26 @@ def flush_attr_cache(obj=None): else: # clean cache completely _ATTR_CACHE = defaultdict(dict) + + +def call_ndb_hooks(obj, attrname, value): + """ + No caching is done of ndb here, but + we use this as a way to call OOB hooks. + """ + hid = hashid(obj) + if hid: + oob_hook = _OOB_NDB_UPDATE_HOOKS[hid].get(attrname) + if oob_hook: + oob_hook[0](obj.typeclass, attrname, value, *oob_hook[1], **oob_hook[2]) + +def call_custom_hooks(obj, attrname, value): + """ + Custom handler for developers adding their own oob hooks, e.g. to + custom typeclass properties. + """ + hid = hashid(obj) + if hid: + oob_hook = _OOB_CUSTOM_UPDATE_HOOKS[hid].get(attrname) + if oob_hook: + oob_hook[0](obj.typeclass, attrname, value, *oob_hook[1], **oob_hook[2]) diff --git a/src/server/models.py b/src/server/models.py index 6291c50527..4bb11c1bf1 100644 --- a/src/server/models.py +++ b/src/server/models.py @@ -107,7 +107,7 @@ class ServerConfig(SharedMemoryModel): def __unicode__(self): return "%s : %s" % (self.key, self.value) - def store(key, value): + def store(self, key, value): """ Wrap the storage (handles pickling) """ diff --git a/src/server/msdp.py b/src/server/msdp.py index 6d9f24d938..f457e13548 100644 --- a/src/server/msdp.py +++ b/src/server/msdp.py @@ -15,13 +15,6 @@ from django.conf import settings from src.utils.utils import make_iter, mod_import from src.utils import logger -# custom functions -OOC_MODULE = mod_import(settings.OOB_FUNC_MODULE) -OOC_FUNCS = dict((key.upper(), var) for key, var in OOC_MODULE if not key.startswith('__') and callable(var)) - -# MSDP commands supported by Evennia -MSDP_COMMANDS = ("LIST", "REPORT", "RESET", "SEND", "UNREPORT") - # MSDP-relevant telnet cmd/opt-codes MSDP = chr(69) MSDP_VAR = chr(1) @@ -36,6 +29,92 @@ regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY 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 +# 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. +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 = 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): """ Implements the MSDP protocol. @@ -51,6 +130,7 @@ class Msdp(object): self.protocol.protocol_FLAGS['MSDP'] = False self.protocol.negotiationMap['MSDP'] = self.parse_msdp self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp) + self.msdp_reported = {} def no_msdp(self, option): "No msdp supported or wanted" @@ -110,7 +190,7 @@ class Msdp(object): Handle a client's requested negotiation, converting it into a function mapping - either one of the MSDP default functions (LIST, SEND etc) or a custom one - in OOC_FUNCS dictionary. command names are case-insensitive. + in OOB_FUNCS dictionary. command names are case-insensitive. varname, var --> mapped to function varname(var) arrayname, array --> mapped to function arrayname(*array) @@ -133,6 +213,7 @@ class Msdp(object): variables = dict((key.upper(), val) for key, val in regex_varval(regex_array.sub("", regex_table.sub("", data)))) ret = "" + # default MSDP functions if "LIST" in variables: ret += self.func_to_msdp("LIST", self.msdp_cmd_list(variables["LIST"])) @@ -150,7 +231,7 @@ class Msdp(object): ret += self.func_to_msdp("RESET", self.msdp_cmd_reset(*arrays["RESET"])) del arrays["RESET"] if "SEND" in variables: - ret += self.func_to_msdp("SEND", self.msdp_cmd_send((*variables["SEND"],))) + ret += self.func_to_msdp("SEND", self.msdp_cmd_send(*(variables["SEND"],))) del variables["SEND"] if "SEND" in arrays: ret += self.func_to_msdp("SEND",self.msdp_cmd_send(*arrays["SEND"])) @@ -159,17 +240,17 @@ class Msdp(object): # if there are anything left we look for a custom function for varname, var in variables.items(): # a simple function + argument - ooc_func = OOC_FUNCS.get(varname.upper()) + ooc_func = MSDP_COMMANDS_CUSTOM.get(varname.upper()) if ooc_func: ret += self.func_to_msdp(varname, ooc_func(var)) for arrayname, array in arrays.items(): # we assume the array are multiple arguments to the function - ooc_func = OOC_FUNCS.get(arrayname.upper()) + ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper()) if ooc_func: ret += self.func_to_msdp(arrayname, ooc_func(*array)) for tablename, table in tables.items(): # we assume tables are keyword arguments to the function - ooc_func = OOC_FUNCS.get(arrayname.upper()) + ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper()) if ooc_func: ret += self.func_to_msdp(tablename, ooc_func(**table)) return ret @@ -184,28 +265,31 @@ 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.func_to_msdp(arg, MSDP_COMMANDS) elif arg == 'LISTS': return self.func_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")) elif arg == 'REPORTABLE_VARIABLES': - return self.func_to_msdp(arg, self.MSDP_REPORTABLE.keys()) + return self.func_to_msdp(arg, MSDP_REPORTABLE.keys()) elif arg == 'REPORTED_VARIABLES': - return self.func_to_msdp(arg, self.MSDP_REPORTED.keys()) + # the dynamically set items to report + return self.func_to_msdp(arg, self.msdp_reported.keys()) elif arg == 'SENDABLE_VARIABLES': - return self.func_to_msdp(arg, self.MSDP_SEND.keys()) + return self.func_to_msdp(arg, MSDP_SENDABLE.keys()) else: return self.func_to_msdp("LIST", arg) + # default msdp commands + def msdp_cmd_report(self, *arg): """ The report command instructs the server to start reporting a reportable variable to the client. """ try: - self.MSDP_REPORTABLE[arg](report=True) + MSDP_REPORTABLE[arg](report=True) except Exception: logger.log_trace() @@ -214,7 +298,7 @@ class Msdp(object): Unreport a previously reported variable """ try: - self.MSDP_REPORTABLE[arg](report=False) + MSDP_REPORTABLE[arg](report=False) except Exception: self.logger.log_trace() @@ -223,7 +307,7 @@ class Msdp(object): The reset command resets a variable to its initial state. """ try: - self.MSDP_REPORTABLE[arg](reset=True) + MSDP_REPORTABLE[arg](reset=True) except Exception: logger.log_trace() @@ -237,79 +321,9 @@ class Msdp(object): ret = [] for var in make_iter(arg): try: - ret.append(self.MSDP_REPORTABLE[arg](send=True)) + ret.append(MSDP_REPORTABLE[arg](send=True)) except Exception: logger.log_trace() return ret - # MSDP_MAP 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. - 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"} diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py new file mode 100644 index 0000000000..4a2ecca250 --- /dev/null +++ b/src/server/oobhandler.py @@ -0,0 +1,224 @@ +""" +OOB - Out-of-band central handler + +This module presents a central API for requesting data from objects in +Evennia via OOB negotiation. It is meant specifically to be imported +and used by the module defined in settings.OOB_MODULE. + +Import src.server.oobhandler and use the methods in OOBHANDLER. + +The actual client protocol (MSDP, GMCP, whatever) does not matter at +this level, serialization is assumed to happen at the protocol level +only. + +This module offers the following basic functionality: + +track_passive - retrieve field, property, db/ndb attribute from an object, then continue reporting + changes henceforth. This is done efficiently and on-demand using hooks. This should be + used preferentially since it's very resource efficient. +track_active - this is an active reporting mechanism making use of a Script. This should normally + only be used if: + 1) you want changes to be reported SLOWER than the actual rate of update (such + as only wanting to show an average of change over time) + 2) the data you are reporting is NOT stored as a field/property/db/ndb on an object (such + as some sort of server statistic calculated on the fly). + +Trivial operations such as get/setting individual properties one time is best done directly from +the OOB_MODULE functions. + +""" + +from collections import defaultdict +from src.scripts.objects import ScriptDB +from src.scripts.script import Script +from src.server import caches +from src.server.caches import hashid +from src.utils import logger, create + +class _OOBTracker(Script): + """ + Active tracker script, handles subscriptions + """ + def at_script_creation(self): + "Called at script creation" + self.key = "oob_tracking_30" # default to 30 second interval + self.desc = "Active tracking of oob data" + self.interval = 30 + self.persistent = False + self.start_delay = True + # holds dictionary of key:(function, (args,), {kwargs}) to call + self.db.subs = {} + + def track(self, key, func, *args, **kwargs): + """ + Add sub to track. func(*args, **kwargs) will be called at self.interval. + key is a unique identifier for removing the tracking later. + """ + self.subs[key] = (func, args, kwargs) + + def untrack(self, key): + """ + Clear a tracking. Return True if untracked successfully, None if + no previous track was found. + """ + if key in self.subs: + del self.subs[key] + if not self.subs: + # we have no more subs. Stop this script. + self.stop() + return True + + def at_repeat(self): + """ + Loops through all subs, calling their given function + """ + for func, args, kwargs in self.subs: + try: + func(*args, **kwargs) + except Exception: + logger.log_trace() + +class _OOBStore(Script): + """ + Store OOB data between restarts + """ + def at_script_creation(self): + "Called at script creation" + self.key = "oob_save_store" + self.desc = "Stores OOB data" + self.persistent = True + def save_oob_data(self, data): + self.db.store = data + def get_oob_data(self): + return self.db.store + +class OOBhandler(object): + """ + Main Out-of-band handler + """ + def __init__(self): + "initialization" + self.track_passive_subs = defaultdict(dict) + scripts = ScriptDB.objects.filter(db_key__startswith="oob_tracking_") + self.track_active_subs = dict((s.interval, s) for s in scripts) + # set reference on caches module + caches._OOB_HANDLER = self + + def track_passive(self, tracker, tracked, name, function, entity="db", *args, **kwargs): + """ + Passively track changes to an object property, + attribute or non-db-attribute. Uses cache hooks to + do this on demand, without active tracking. + + tracker - object who is tracking + tracked - object being tracked + name - field/property/attribute/ndb nam to watch + function - function object to call when entity update. When entitye + is updated, this function will be called with called + with function(obj, name, new_value, *args, **kwargs) + *args - additional, optional arguments to send to function + entity (keyword) - the type of entity to track. One of + "property", "db", "ndb" or "custom" ("property" includes both + changes to database fields and cached on-model properties) + **kwargs - additional, optionak keywords to send to function + + Only entities that are being -cached- can be tracked. For custom + on-typeclass properties, a custom hook needs to be created, calling + the update() function in this module whenever the tracked entity changes. + """ + + # always store database object (in case typeclass changes along the way) + try: tracker = tracker.dbobj + except AttributeError: pass + try: tracked = tracked.dbobj + except AttributeError: pass + + thid = hashid(tracked) + if not thid: + return + oob_call = (function, tracked, name, args, kwargs) + if thid not in self.track_passive_subs: + if entity in ("db", "ndb", "custom"): + caches.register_oob_update_hook(tracked, name, entity=entity) + elif entity == "property": + # track property/field. We must first determine which cache to use. + if hasattr(tracked, 'db_%s' % name.lstrip("db_")): + hid = caches.register_oob_update_hook(tracked, name, entity="field") + else: + hid = caches.register_oob_update_hook(tracked, name, entity="property") + if not self.track_pass_subs[hid][name]: + self.track_pass_subs[hid][name] = {tracker:oob_call} + else: + self.track_passive_subs[hid][name][tracker] = oob_call + + def untrack_passive(self, tracker, tracked, name, entity="db"): + """ + Remove passive tracking from an object's entity. + entity - one of "property", "db", "ndb" or "custom" + """ + try: tracked = tracked.dbobj + except AttributeError: pass + + thid = hashid(tracked) + if not thid: + return + if len(self.track_passive_subs[thid][name]) == 1: + if entity in ("db", "ndb", "custom"): + caches.unregister_oob_update_hook(tracked, name, entity=entity) + elif entity == "property": + if hasattr(tracked, 'db_%s' % name.lstrip("db_")): + caches.unregister_oob_update_hook(tracked, name, entity="field") + else: + caches.unregister_oob_update_hook(tracked, name, entity="property") + + try: del self.track_passive_subs[thid][name][tracker] + except (KeyError, TypeError): pass + + def update(self, hid, name, new_val): + """ + This is called by the caches when the tracked object when its property/field/etc is updated, + to inform the oob handler and all subscribing to this particular entity has been updated with new_val. + """ + # tell all tracking objects of the update + for tracker, oob in self.track_passive_subs[hid][name].items(): + try: + # function(tracker, tracked, key, new_value, *args, **kwargs) + oob[0](tracker, oob[1], oob[2], new_val, *oob[3], **oob[4]) + except Exception: + logger.log_trace() + + # Track (active/proactive tracking) + + # creating and storing tracker scripts + def track_active(self, key, func, interval=30, *args, **kwargs): + """ + Create a tracking, re-use script with same interval if available, + otherwise create a new one. + + args: + key - interval-unique identifier needed for removing tracking later + func - function to call at interval seconds + (all other args become argjs into func) + keywords: + interval (default 30s) - how often to update tracker + (all other kwargs become kwargs into func) + """ + if interval in self.track_active_subs: + # tracker with given interval found. Add to its subs + self.track_active_subs[interval].track(key, func, *args, **kwargs) + else: + # create new tracker with given interval + new_tracker = create.create_script(_OOBTracker, key="oob_tracking_%i" % interval, interval=interval) + new_tracker.track(key, func, *args, **kwargs) + self.track_active_subs[interval] = new_tracker + + def untrack_active(self, key, interval): + """ + Remove tracking for a given interval and key + """ + tracker = self.track_active_subs.get(interval) + if tracker: + tracker.untrack(key) + +# handler object +OOBHANDLER = OOBhandler() diff --git a/src/server/server.py b/src/server/server.py index c4bec30789..d628844c67 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -142,7 +142,7 @@ class Evennia(object): if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any() # we have a changed default. Import relevant objects and run the update from src.objects.models import ObjectDB - from src.players.models import PlayerDB + #from src.players.models import PlayerDB for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches): # update the database print " one or more default cmdset/typeclass settings changed. Updating defaults stored in database ..." @@ -188,7 +188,7 @@ class Evennia(object): Called every server start """ from src.objects.models import ObjectDB - from src.players.models import PlayerDB + #from src.players.models import PlayerDB #update eventual changed defaults self.update_defaults() @@ -264,7 +264,7 @@ class Evennia(object): # call shutdown hooks on all cached objects from src.objects.models import ObjectDB - from src.players.models import PlayerDB + #from src.players.models import PlayerDB from src.server.models import ServerConfig if mode == 'reload': diff --git a/src/server/serversession.py b/src/server/serversession.py index 8305e407ca..501ef42571 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -228,6 +228,8 @@ class ServerSession(Session): example: data = {"get_hp": ([], {}), "update_counter", (["counter1"], {"now":True}) } + + All functions will be called with a back-reference to this session as first argument. """ outdata = {} @@ -243,7 +245,7 @@ class ServerSession(Session): func = OOB_FUNC_MODULE.__dict__.get(funcname, None) if func: try: - outdata[funcname] = func(entity, *argtuple[0], **argtuple[1]) + outdata[funcname] = func(self, entity, *argtuple[0], **argtuple[1]) except Exception: logger.log_trace() else: diff --git a/src/server/telnet.py b/src/server/telnet.py index 39ffbfcb9b..27e446e56d 100644 --- a/src/server/telnet.py +++ b/src/server/telnet.py @@ -35,7 +35,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.mccp = Mccp(self) # negotiate ttype (client info) self.ttype = ttype.Ttype(self) - # negotiate mssp (crawler communication) self.mssp = mssp.Mssp(self) diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 711ee4857a..21baa25222 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -42,6 +42,7 @@ from src.utils.idmapper.models import SharedMemoryModel from src.server.caches import get_field_cache, set_field_cache, del_field_cache from src.server.caches import get_attr_cache, set_attr_cache, del_attr_cache from src.server.caches import get_prop_cache, set_prop_cache, del_prop_cache +from src.server.caches import call_ndb_hooks from src.server.models import ServerConfig from src.typeclasses import managers from src.locks.lockhandler import LockHandler @@ -1584,7 +1585,7 @@ class TypedObject(SharedMemoryModel): return None else: # act as a setter - _SA(self.db, attribute_name, value) + _SA(self.ndb, attribute_name, value) #@property def __ndb_get(self): @@ -1609,6 +1610,10 @@ class TypedObject(SharedMemoryModel): return _GA(self, key) except AttributeError: return None + def __setattr__(self, key, value): + # hook the oob handler here + call_ndb_hooks(self, key, value) + _SA(self, key, value) self._ndb_holder = NdbHolder() return self._ndb_holder #@ndb.setter