From 03b4b9ddb4ab5c4711b9659f98b5b942b33ae894 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 11 Feb 2015 22:16:54 +0100 Subject: [PATCH] Continued work on reworking the oobhandler. --- evennia/server/oobhandler.py | 198 ++++++++++++++------------------- evennia/utils/idmapper/base.py | 48 ++++---- 2 files changed, 100 insertions(+), 146 deletions(-) diff --git a/evennia/server/oobhandler.py b/evennia/server/oobhandler.py index b0b5d2e43..2ac589e59 100644 --- a/evennia/server/oobhandler.py +++ b/evennia/server/oobhandler.py @@ -69,128 +69,99 @@ if not _OOB_ERROR: # but automatically through the OOBHandler. # -class TrackerHandler(object): +class FieldTracker(object): """ - This object is dynamically assigned to objects whenever one of its fields - are to be tracked. It holds an internal dictionary mapping to the fields - on that object. Each field can be tracked by any number of trackers (each - tied to a different callback). + This object should be stored on the + tracked object as "_oob_at__update". + the update() method will be called by the + save mechanism, which in turn will call the + user-customizable func() """ def __init__(self, obj): """ - This is initiated and stored on the object as a - property _trackerhandler. + This initializes the tracker with the object it sits on. """ 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")()) + self.subscribers = {} - def add(self, fieldname, tracker): + def add(self, session): """ - Add tracker to the handler. Raises KeyError if fieldname - does not exist. + Add a subscribing session to the tracker """ - trackerkey = tracker.__class__.__name__ - self.tracktargets[fieldname][trackerkey] = tracker - self.ntrackers += 1 + self.subscribers[session.sessid] = session - def remove(self, fieldname, trackerclass, *args, **kwargs): + def remove(self, session): """ - Remove identified tracker from TrackerHandler. - Raises KeyError if tracker is not found. + Remove a subsribing session from the tracker """ - trackerkey = trackerclass.__name__ - tracker = self.tracktargets[fieldname][trackerkey] - try: - tracker.at_remove(*args, **kwargs) - except Exception: - logger.log_trace() - del self.tracktargets[fieldname][trackerkey] - self.ntrackers -= 1 - if self.ntrackers <= 0: - # if there are no more trackers, clean this handler - del self + self.subscribers.pop(session.sessid) - def update(self, fieldname, new_value): + def trigger_update(self, fieldname, new_value): """ - Called by the field when it updates to a new value + Called by the save() mechanism when the given + field has updated. """ - for tracker in self.tracktargets[fieldname].values(): + for session in self.subscribers.values(): try: - tracker.update(new_value) + self.at_field_update(session, fieldname, new_value) except Exception: - logger.log_trace() + pass + def at_field_update(self, session, fieldname, new_value): + """ + This needs to be overloaded for each tracking + command. -# On-object Trackers to load with TrackerHandler - -class TrackerBase(object): - """ - Base class for OOB Tracker objects. Inherit from this - to define custom trackers. - """ - def __init__(self, *args, **kwargs): - pass - - def update(self, *args, **kwargs): - "Called by tracked objects" - pass - - def at_remove(self, *args, **kwargs): - "Called when tracker is removed" + Args: + session (Session): the session subscribing + to this update. + fieldname (str): the name of the updated field. + value (any): the new value now in this field. + """ pass -class ReportFieldTracker(TrackerBase): +class ReportFieldTracker(FieldTracker): """ Tracker that passively sends data to a stored sessid whenever a named database field changes. The TrackerHandler calls this with the correct arguments. """ - def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs): + def at_field_update(self, session, fieldname, new_value): """ - name - name of entity to track, such as "db_key" - sessid - sessid of session to report to + Called when field updates. """ - self.oobhandler = oobhandler - self.fieldname = fieldname - self.sessid = sessid - - def update(self, new_value, *args, **kwargs): - "Called by cache when updating the tracked entitiy" - # use oobhandler to relay data + # we must never relay objects across to the Portal, only + # text. try: - # we must never relay objects across the amp, only text data. + # it may be an object new_value = new_value.key except AttributeError: + # ... or not new_value = to_str(new_value, force_string=True) - kwargs[self.fieldname] = new_value - # this is a wrapper call for sending oob data back to session - self.oobhandler.msg(self.sessid, "report", *args, **kwargs) + # return as an OOB call of type "report" + session.msg(oob=("report", {fieldname:new_value})) -class ReportAttributeTracker(TrackerBase): +class ReportAttributeTracker(FieldTracker): """ Tracker that passively sends data to a stored sessid whenever - the Attribute updates. Since the field here is always "db_key", - we instead store the name of the attribute to return. + the Attribute updates. Since the Attribute's field is always + db_value, we return the attribute's name instead. """ - def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs): + def at_field_update(self, session, fieldname, new_value): """ - attrname - name of attribute to track - sessid - sessid of session to report to + Called when field updates. """ - self.oobhandler = oobhandler - self.attrname = attrname - self.sessid = sessid - - def update(self, new_value, *args, **kwargs): - "Called by cache when attribute's db_value field updates" - kwargs[self.attrname] = new_value - # this is a wrapper call for sending oob data back to session - self.oobhandler.msg(self.sessid, "report", *args, **kwargs) + # we must never relay objects across to the Portal, only + # text. + try: + # it may be an object + new_value = new_value.key + except AttributeError: + # ... or not + new_value = to_str(new_value, force_string=True) + session.msg(oob=("report", {obj.db_key: new_value}) @@ -222,46 +193,39 @@ class OOBTickerHandler(TickerHandler): # Main OOB Handler -class OOBHandler(object): - """ - The OOBHandler maintains all dynamic on-object oob hooks. It will store the - creation instructions and and re-apply them at a server reload (but - not after a server shutdown) - """ - def __init__(self): - """ - Initialize handler - """ - self.sessionhandler = SESSIONS - self.oob_tracker_storage = {} - self.tickerhandler = OOBTickerHandler("oob_ticker_storage") +class OOBHandler(TickerHandler): - def save(self): + class AtTick(object): """ - Save the command_storage as a serialized string into a temporary - ServerConf field + A wrapper object with a hook to call at regular intervals """ - 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)) - self.tickerhandler.save() + global SESSIONS, _OOB_FUNCS - def restore(self): + def at_tick(self, oobhandler, cmdname, sessid, *args, **kwargs): + "Called at regular intervals. Calls the oob function" + session = SESSIONS.session_from_sessid(sessid): + cmd = _OOB_FUNCS.get(cmdname, None) + try: + cmd(oobhandler, session, *args, **kwargs) + except Exception: + logger.log_trace() + + def __init__(self, *args, **kwargs): + self.save_name = "oob_ticker_storage" + super(OOBHandler, self).__init__(*args, **kwargs) + + def set_repeat(self, obj, sessid, oobfunc, interval=20, *args, **kwargs): """ - Restore the command_storage from database and re-initialize the handler from storage.. This is - only triggered after a server reload, not after a shutdown-restart + Set an oob function to be repeatedly called. + + Args: + obj (Object) - the object registering the repeat + sessid (int) - session id of the session registering + oobfunc (str) - oob function name to call every interval seconds + interval (int) - interval to call oobfunc, in seconds + *args, **kwargs - are used as arguments to the oobfunc """ - # load stored command instructions and use them to re-initialize handler - tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage") - if tracker_storage: - self.oob_tracker_storage = dbunserialize(tracker_storage) - for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values(): - #print "restoring tracking:",obj, sessid, fieldname, trackerclass - self._track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs) - # make sure to purge the storage - ServerConfig.objects.conf(key="oob_tracker_storage", delete=True) - self.tickerhandler.restore() + def _track(self, obj, sessid, propname, trackerclass, *args, **kwargs): """ @@ -272,7 +236,7 @@ class OOBHandler(object): named as propname, this will be used as the property name when assigning the OOB to obj, otherwise tracker_key is used as the property name. """ - if not "_trackerhandler" in _GA(obj, "__dict__"): + if not hasattr(obj, "_trackerhandler"): # assign trackerhandler to object _SA(obj, "_trackerhandler", TrackerHandler(obj)) # initialize object diff --git a/evennia/utils/idmapper/base.py b/evennia/utils/idmapper/base.py index fff36f1e9..01fa2bd47 100755 --- a/evennia/utils/idmapper/base.py +++ b/evennia/utils/idmapper/base.py @@ -27,35 +27,6 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ -def at_field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs): - """ - Called at the beginning of the field save operation. The save method - must be called with the update_fields keyword in order to be most efficient. - This method should NOT save; rather it is the save() that triggers this - function. Its main purpose is to allow to plug-in a save handler and oob - handlers. - """ - if raw: - return - if update_fields: - # this is a list of strings at this point. We want field objects - update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields) - else: - # meta.fields are already field objects; get them all - update_fields = _GA(_GA(instance, "_meta"), "fields") - for field in update_fields: - fieldname = field.name - handlername = "_at_%s_postsave" % fieldname - handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None - if callable(handler): - handler() - trackerhandler = _GA(instance, "_trackerhandler") if "_trackerhandler" in _GA(instance, '__dict__') else None - if trackerhandler: - trackerhandler.update(fieldname, _GA(instance, fieldname)) - -# connect the signal to catch field changes -post_save.connect(at_post_field_save, dispatch_uid="at_post_field_save") - # References to db-updated objects are stored here so the # main process can be informed to re-cache itself. @@ -362,6 +333,25 @@ class SharedMemoryModel(Model): super(SharedMemoryModel, cls).save(*args, **kwargs) callFromThread(_save_callback, self, *args, **kwargs) + # update field-update hooks and eventual OOB watchers + if "update_fields" in kwargs: + # get field objects from their names + update_fields = (self._meta.get_field_by_name(field)[0] for field in kwargs["update_fields"]) + else: + # meta.fields are already field objects; get them all + update_fields = self._meta.fields + for field in update_fields: + fieldname = field.name + # if a hook is defined it must be named exactly on this form + hookname = "_at_%s_postsave" % fieldname + if hasattr(self, hookname) and callable(_GA(self, hookname)): + _GA(self, hookname)() + # if a trackerhandler is set on this object, update it with the + # fieldname and the new value + trackername = "_oob_at_%s_postsave" % fieldname + if hasattr(self, trackername): + _GA(self, trackername).trigger_update(fieldname, _GA(self, fieldname)) + class WeakSharedMemoryModelBase(SharedMemoryModelBase): """