MonitorHandler implemented as a replacement for the OOBHandler monitor functionality, as per #924.

This commit is contained in:
Griatch 2016-03-05 19:51:09 +01:00
parent 198a348d73
commit 8090d92d85
5 changed files with 506 additions and 408 deletions

View file

@ -1,422 +1,515 @@
""" """
Monitors - handle input-commands from the client. Monitors - catch changes to model fields and Attributes.
The INPUT_HANDLER singleton from this module is meant as a resource The MONITOR_HANDLER singleton from this module offers the following
for input functions to use. The handler is never executed functionality:
automatically unless called from an input function. The handler offers
the following features:
- Field-monitor - track a object's specific database field and perform - Field-monitor - track a object's specific database field and perform
an action whenever that field *changes* for whatever reason. an action whenever that field *changes* for whatever reason.
- Attribute-monitor track an object's specific Attribute and perform - Attribute-monitor tracks an object's specific Attribute and perform
an action whenever that Attribute *changes* for whatever reason. an action whenever that Attribute *changes* for whatever reason.
""" """
from builtins import object from builtins import object
from collections import defaultdict from collections import defaultdict
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.scripts.tickerhandler import TickerHandler from evennia.utils.dbserialize import dbserialize, dbunserialize
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
from evennia.utils import logger from evennia.utils import logger
_SA = object.__setattr__ _SA = object.__setattr__
_GA = object.__getattribute__ _GA = object.__getattribute__
_DA = object.__delattr__ _DA = object.__delattr__
_SESSIONS = None
# set at the bottom of this module class MonitorHandler(object):
_OOB_FUNCS = None
_OOB_ERROR = None
#
# TrackerHandler is assigned to objects that should notify themselves to
# the OOB system when some property changes. This is never assigned manually
# but automatically through the OOBHandler.
#
class OOBFieldMonitor(object):
""" """
This object should be stored on the This is a resource singleton that allows for registering
tracked object as "_oob_at_<fieldname>_postsave". callbacks for when a field or Attribute is updated (saved).
the update() method w ill be called by the
save mechanism, which in turn will call the
user-customizable func()
""" """
def __init__(self, obj): def __init__(self):
""" """
This initializes the monitor with the object it sits on. Initialize the handler.
"""
self.savekey = "_monitorhandler_save"
self.monitors = defaultdict(lambda:defaultdict(dict))
def at_update(self, obj, fieldname):
"""
Called by the field as it saves.
"""
to_delete = []
if obj in self.monitors and fieldname in self.monitors[obj]:
for (callback, kwargs) in self.monitors[obj][fieldname].iteritems():
kwargs.update({"obj": obj, "fieldname": fieldname})
try:
callback( **kwargs)
except Exception:
to_delete.append((obj, fieldname, callback))
logger.log_trace("Monitor callback was removed.")
# we need to do the cleanup after loop has finished
for (obj, fieldname, callback) in to_delete:
del self.monitors[obj][fieldname][callback]
def add(self, obj, fieldname, callback, **kwargs):
"""
Add monitoring to a given field or Attribute. A field must
be specified with the full db_* name or it will be assumed
to be an Attribute (so `db_key`, not just `key`).
Args: Args:
obj (Object): object handler is defined on. obj (Typeclassed Entity): The entity on which to monitor a
field or Attribute.
fieldname (str): Name of field (db_*) or Attribute to monitor.
callback (callable): A callable on the form `callable(obj,
fieldname, **kwargs), where kwargs holds keys fieldname
and obj.
uid (hashable): A unique id to identify this particular monitor.
It is used together with obj to
persistent (bool): If this monitor should survive a server
reboot or not (it will always survive a reload).
""" """
self.obj = obj if not fieldname.startswith("db_") or not hasattr(obj, fieldname):
self.subscribers = defaultdict(list) # an Attribute - we track it's db_value field
obj = obj.attributes.get(fieldname, return_obj=True)
if not obj:
return
fieldname = "db_value"
def __call__(self, fieldname): # we try to serialize this data to test it's valid. Otherwise we won't accept it.
"""
Called by the save() mechanism when the given field has
updated.
Args:
fieldname (str): The field to monitor
"""
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
for sessid, oobtuples in self.subscribers.items():
# oobtuples is a list [(oobfuncname, args, kwargs), ...],
# a potential list of oob commands to call when this
# field changes.
sessid = _SESSIONS.get(sessid)
if sessid:
for (oobfuncname, args, kwargs) in oobtuples:
INPUT_HANDLER.execute_cmd(sessid, oobfuncname, fieldname, self.obj, *args, **kwargs)
def add(self, session, oobfuncname, *args, **kwargs):
"""
Add a specific tracking callback to monitor
Args:
session (int): Session.
oobfuncname (str): oob command to call when field updates
args,kwargs (any): arguments to pass to oob commjand
Notes:
Each sessid may have a list of (oobfuncname, args, kwargs)
tuples, all of which will be executed when the
field updates.
"""
self.subscribers[session.sessid].append((oobfuncname, args, kwargs))
def remove(self, session, oobfuncname=None):
"""
Remove a subscribing session from the monitor
Args:
sessid(int): Session id
oobfuncname (str, optional): Only delete this cmdname.
If not given, delete all.
"""
if oobfuncname:
self.subscribers[session.sessid] = [item for item in self.subscribers[session.sessid]
if item[0] != oobfuncname]
else:
self.subscribers.pop(session.sessid, None)
class OOBAtRepeater(object):
"""
This object is created and used by the `OOBHandler.repeat` method.
It will be assigned to a target object as a custom variable, e.g.:
`obj._oob_ECHO_every_20s_for_sessid_1 = AtRepater()`
It will be called every interval seconds by the OOBHandler,
triggering whatever OOB function it is set to use.
"""
def __call__(self, *args, **kwargs):
"Called at regular intervals. Calls the oob function"
INPUT_HANDLER.execute_cmd(kwargs["_sessid"], kwargs["_oobfuncname"], *args, **kwargs)
# Main OOB Handler
class OOBHandler(TickerHandler):
"""
The OOBHandler manages all server-side OOB functionality
"""
def __init__(self, *args, **kwargs):
"""
Setup the tickerhandler wrapper.
"""
super(OOBHandler, self).__init__(*args, **kwargs)
self.save_name = "oob_ticker_storage"
self.oob_save_name = "oob_monitor_storage"
self.oob_monitor_storage = {}
def _get_repeater_hook_name(self, oobfuncname, interval, sessid):
"""
Get the unique repeater call hook name for this object
Args:
oobfuncname (str): OOB function to retrieve
interval (int): Repeat interval
sessid (int): The Session id.
Returns:
hook_name (str): The repeater hook, when created, is a
dynamically assigned function that gets assigned to a
variable with a name created by combining the arguments.
"""
return "_oob_%s_every_%ss_for_sessid_%s" % (oobfuncname, interval, sessid)
def _get_fieldmonitor_name(self, fieldname):
"""
Get the fieldmonitor name.
Args:
fieldname (str): The field monitored.
Returns:
fieldmonitor_name (str): A dynamic function name
created from the argument.
"""
return "_oob_at_%s_postsave" % fieldname
def _add_monitor(self, obj, sessid, fieldname, oobfuncname, *args, **kwargs):
"""
Helper method. Creates a fieldmonitor and store it on the
object. This tracker will be updated whenever the given field
changes.
Args:
obj (Object): The object on which to store the monitor.
sessid (int): The Session id associated with the monitor.
fieldname (str): The field to monitor
oobfuncname (str): The OOB callback function to trigger when
field `fieldname` changes.
args, kwargs (any): Arguments to pass on to the callback.
"""
fieldmonitorname = self._get_fieldmonitor_name(fieldname)
if not hasattr(obj, fieldmonitorname):
# assign a new fieldmonitor to the object
_SA(obj, fieldmonitorname, OOBFieldMonitor(obj))
# register the session with the monitor
_GA(obj, fieldmonitorname).add(sessid, oobfuncname, *args, **kwargs)
# store calling arguments as a pickle for retrieval at reload
storekey = (pack_dbobj(obj), sessid, fieldname, oobfuncname)
stored = (args, kwargs)
self.oob_monitor_storage[storekey] = stored
def _remove_monitor(self, obj, sessid, fieldname, oobfuncname=None):
"""
Helper method. Removes the OOB from obj.
Args:
obj (Object): The object from which to remove the monitor.
sessid (int): The Session id associated with the monitor.
fieldname (str): The monitored field from which to remove the monitor.
oobfuncname (str): The oob callback function.
"""
fieldmonitorname = self._get_fieldmonitor_name(fieldname)
try: try:
_GA(obj, fieldmonitorname).remove(sessid, oobfuncname=oobfuncname) dbserialize((obj, fieldname, callback, kwargs))
if not _GA(obj, fieldmonitorname).subscribers: except Exception:
_DA(obj, fieldmonitorname) err = "Invalid monitor definition (skipped since it could not be serialized):\n" \
except AttributeError: " (%s, %s, %s, %s)" % (obj, fieldname, callback, kwargs)
pass logger.log_trace(err)
# remove the pickle from storage else:
store_key = (pack_dbobj(obj), sessid, fieldname, oobfuncname) self.monitors[obj][fieldname][callback] = kwargs
self.oob_monitor_storage.pop(store_key, None)
def remove(self, obj, fieldname, callback):
"""
Remove a monitor.
"""
callback_dict = self.monitors[obj][fieldname]
if callback in callback_dict:
del callback_dict[callback]
def save(self): def save(self):
""" """
Handles saving of the OOBHandler data when the server reloads. Store our monitors to the database. This is called
Called from the Server process. by the server process.
Since dbserialize can't handle defaultdicts, we convert to an
intermediary save format ((obj,fieldname, callback, kwargs), ...)
""" """
# save ourselves as a tickerhandler savedata = []
super(OOBHandler, self).save() for obj in self.monitors:
# handle the extra oob monitor store for fieldname in self.monitors[obj]:
if self.ticker_storage: for callback in self.monitors[obj][fieldname]:
ServerConfig.objects.conf(key=self.oob_save_name, savedata.append((obj, fieldname, callback, self.monitors[obj][fieldname][callback]))
value=dbserialize(self.oob_monitor_storage)) savedata = dbserialize(savedata)
else: ServerConfig.objects.conf(key=self.savekey,
# make sure we have nothing lingering in the database value=savedata)
ServerConfig.objects.conf(key=self.oob_save_name, delete=True)
def restore(self): def restore(self):
""" """
Called when the handler recovers after a Server reload. Called Restore our monitors after a reload. This is called
by the Server process as part of the reload upstart. Here we by the server process.
overload the tickerhandler's restore method completely to make
sure we correctly re-apply and re-initialize the correct
monitor and repeater objecth on all saved objects.
""" """
# load the oob monitors and initialize them savedata = ServerConfig.objects.conf(key=self.savekey)
oob_storage = ServerConfig.objects.conf(key=self.oob_save_name) if savedata:
if oob_storage: for (obj, fieldname, callback, kwargs) in dbunserialize(savedata):
self.oob_storage = dbunserialize(oob_storage) self.monitors[obj][fieldname][callback] = kwargs
for store_key, (args, kwargs) in self.oob_storage.items(): ServerConfig.objects.conf(key=self.savekey, delete=True)
# re-create the monitors
obj, sessid, fieldname, oobfuncname = store_key
obj = unpack_dbobj(obj)
self._add_monitor(obj, sessid, fieldname, oobfuncname, *args, **kwargs)
# handle the tickers (same as in TickerHandler except we call
# the add_repeater method which makes sure to add the hooks before
# starting the tickerpool)
ticker_storage = ServerConfig.objects.conf(key=self.save_name)
if ticker_storage:
self.ticker_storage = dbunserialize(ticker_storage)
for store_key, (args, kwargs) in self.ticker_storage.items():
obj, interval, idstring = store_key
obj = unpack_dbobj(obj)
# we saved these in add_repeater before, can now retrieve them
sessid = kwargs["_sessid"]
oobfuncname = kwargs["_oobfuncname"]
self.add_repeater(obj, sessid, oobfuncname, interval, *args, **kwargs)
def add_repeater(self, obj, session, oobfuncname, interval=20, *args, **kwargs): ##
""" ## TrackerHandler is assigned to objects that should notify themselves to
Set an oob function to be repeatedly called. ## the OOB system when some property changes. This is never assigned manually
## but automatically through the OOBHandler.
Args: ##
obj (Object); The object on which to register the repeat. #
session (Session): Session of the session registering. #class _FieldMonitor(object):
oobfuncname (str): Oob function name to call every interval seconds. # """
interval (int, optional): Interval to call oobfunc, in seconds. # This object will be stored on the tracked object as
# "_oob_at_<fieldname>_postsave". the update() method will be
Notes: # called by the save mechanism, which in turn will call the
*args, **kwargs are used as extra arguments to the oobfunc. # user-customizable func().
""" #
sessid = session # """
hook = OOBAtRepeater() # def __init__(self, obj, fieldname):
hookname = self._get_repeater_hook_name(oobfuncname, interval, sessid) # """
_SA(obj, hookname, hook) # This initializes the monitor with the object it sits on.
# we store these in kwargs so that tickerhandler saves them with the rest #
kwargs.update({"_sessid":sessid, "_oobfuncname":oobfuncname}) # Args:
super(OOBHandler, self).add(obj, int(interval), oobfuncname, hookname, *args, **kwargs) # obj (Object): Object handler is defined on.
# fieldname (str): The name of the field to monitor
def remove_repeater(self, obj, session, oobfuncname, interval=20): #
""" # """
Remove the repeatedly calling oob function # self.obj = obj
# self.fieldname = fieldname
Args: # self.subscribers = defaultdict(list)
obj (Object): The object on which the repeater sits #
sessid (Session): Session that registered the repeater # def __call__(self):
oobfuncname (str): Name of oob function to call at repeat # """
interval (int, optional): Number of seconds between repeats # Called by the save() mechanism when the given field has
# updated (it's already saved at this point).
""" #
sessid = session.sessid # Args:
super(OOBHandler, self).remove(obj, interval, idstring=oobfuncname) # fieldname (str): The field to monitor
hookname = self._get_repeater_hook_name(oobfuncname, interval, sessid) #
try: # """
_DA(obj, hookname) # global _SESSIONS
except AttributeError: # if not _SESSIONS:
pass # from evennia.server.sessionhandler import SESSIONS as _SESSIONS
#
def add_field_monitor(self, obj, session, field_name, oobfuncname, *args, **kwargs): # for sessid, functuples in self.subscribers.items():
""" # # oobtuples is a list [(oobfuncname, args, kwargs), ...],
Add a monitor tracking a database field # # a potential list of oob commands to call when this
# # field changes.
Args: # session = _SESSIONS.get(sessid)
obj (Object): The object who'se field is to be monitored. # if session:
session (Session): Session monitoring. # for (args, kwargs) in functuples:
field_name (str): Name of database field to monitor. The db_* can optionally # self.at_update(session, *args, **kwargs)
be skipped (it will be automatically appended if missing). #
oobfuncname (str): OOB function to call when field changes. # def add(self, session, *args, **kwargs):
# """
Notes: # Add a specific tracking callback to monitor
When the field updates the given oobfunction will be called as #
# Args:
`oobfuncname(session, fieldname, obj, *args, **kwargs)` # session (int): Session.
# oobfuncname (str): oob command to call when field updates
where `fieldname` is the name of the monitored field and # args,kwargs (any): arguments to pass to oob commjand
`obj` is the object on which the field sits. From this you #
can also easily get the new field value if you want. # Notes:
# Each sessid may have a list of (oobfuncname, args, kwargs)
""" # tuples, all of which will be executed when the
sessid = session.sessid # field updates.
# all database field names starts with db_* #
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name # """
self._add_monitor(obj, sessid, field_name, oobfuncname, *args, **kwargs) # self.subscribers[session.sessid].append((args, kwargs))
#
def remove_field_monitor(self, obj, session, field_name, oobfuncname=None): # def remove(self, session):
""" # """
Un-tracks a database field # Remove a subscribing session from the monitor
#
Args: # Args:
obj (Object): Entity with the monitored field. # sessid(int): Session id
session (Session): Session that monitors. #
field_name (str): database field monitored (the db_* can optionally be # """
skipped (it will be auto-appended if missing). # self.subscribers.pop(session.sessid, None)
oobfuncname (str, optional): OOB command to call on that field. #
# def at_update(self, session, *args, **kwargs):
Notes: # """
When the Attributes db_value updates the given oobfunction # This should be overloaded for the respective functionality.
will be called as # """
# pass
`oobfuncname(session, fieldname, obj, *args, **kwargs)` #
#
where `fieldname` is the name of the monitored field and #class _Repeater(object):
`obj` is the object on which the field sits. From this you # """
can also easily get the new field value if you want. # This object is created and used by the `OOBHandler.repeat` method.
""" # It will be assigned to a target object as a custom variable, e.g.:
sessid = session.sessid #
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name # `obj._oob_ECHO_every_20s_for_sessid_1 = AtRepater()`
self._remove_monitor(obj, sessid, field_name, oobfuncname=oobfuncname) #
# It will be called every interval seconds by the OOBHandler,
def add_attribute_monitor(self, obj, session, attr_name, oobfuncname, *args, **kwargs): # triggering whatever OOB function it is set to use.
""" #
Monitor the changes of an Attribute on an object. Will trigger when # """
the Attribute's `db_value` field updates. #
# def __call__(self, *args, **kwargs):
Args: # "Called at regular intervals. Calls the oob function"
obj (Object): Object with the Attribute to monitor. # INPUT_HANDLER.execute_cmd(kwargs["_sessid"], kwargs["_oobfuncname"], *args, **kwargs)
session (Session): Session monitoring Session. #
attr_name (str): Name (key) of Attribute to monitor. ## Main OOB Handler
oobfuncname (str): OOB function to call when Attribute updates. #
#class MonitorHandler(TickerHandler):
""" # """
sessid = session.sessid # The MonitorHandler manages the monitoring of changes on Objects,
# get the attribute object if we can # as requested by various agents (normally inputfuncs).
attrobj = obj.attributes.get(attr_name, return_obj=True) # """
if attrobj: #
self._add_monitor(attrobj, sessid, "db_value", oobfuncname) # def __init__(self, *args, **kwargs):
# """
def remove_attribute_monitor(self, obj, session, attr_name, oobfuncname): # Setup the tickerhandler wrapper.
""" # """
Deactivate tracking for a given object's Attribute # super(OOBHandler, self).__init__(*args, **kwargs)
# self.save_name = "oob_ticker_storage"
Args: # self.oob_save_name = "oob_monitor_storage"
obj (Object): Object monitored. # self.oob_monitor_storage = {}
session (Session): Session monitoring. #
attr_name (str): Name of Attribute monitored. # def _get_repeater_hook_name(self, oobfuncname, interval, sessid):
oobfuncname (str): OOB function name called when Attribute updates. # """
# Get the unique repeater call hook name for this object
""" #
sessid = session.sessid # Args:
attrobj = obj.attributes.get(attr_name, return_obj=True) # oobfuncname (str): OOB function to retrieve
if attrobj: # interval (int): Repeat interval
self._remove_monitor(attrobj, sessid, "db_value", oobfuncname) # sessid (int): The Session id.
#
def get_all_monitors(self, session): # Returns:
""" # hook_name (str): The repeater hook, when created, is a
Get the names of all variables this session is tracking. # dynamically assigned function that gets assigned to a
# variable with a name created by combining the arguments.
Args: #
session (Session): Session monitoring. # """
Returns: # return "_oob_%s_every_%ss_for_sessid_%s" % (oobfuncname, interval, sessid)
stored monitors (tuple): A list of tuples #
`(obj, fieldname, args, kwargs)` representing all # def _get_fieldmonitor_name(self, fieldname):
the monitoring the Session with the given sessid is doing. # """
# Get the fieldmonitor name.
""" #
sessid = session.sessid # Args:
# [(obj, fieldname, args, kwargs), ...] # fieldname (str): The field monitored.
return [(unpack_dbobj(key[0]), key[2], stored[0], stored[1]) #
for key, stored in self.oob_monitor_storage.items() if key[1] == sessid] # Returns:
# fieldmonitor_name (str): A dynamic function name
# created from the argument.
#
# """
# return "_oob_at_%s_postsave" % fieldname
#
# def _add_monitor(self, obj, sessid, fieldname, oobfuncname, *args, **kwargs):
# """
# Helper method. Creates a fieldmonitor and store it on the
# object. This tracker will be updated whenever the given field
# changes.
#
# Args:
# obj (Object): The object on which to store the monitor.
# sessid (int): The Session id associated with the monitor.
# fieldname (str): The field to monitor
# oobfuncname (str): The OOB callback function to trigger when
# field `fieldname` changes.
# args, kwargs (any): Arguments to pass on to the callback.
#
# """
# fieldmonitorname = self._get_fieldmonitor_name(fieldname)
# if not hasattr(obj, fieldmonitorname):
# # assign a new fieldmonitor to the object
# _SA(obj, fieldmonitorname, FieldMonitor(obj))
# # register the session with the monitor
# _GA(obj, fieldmonitorname).add(sessid, oobfuncname, *args, **kwargs)
#
# # store calling arguments as a pickle for retrieval at reload
# storekey = (pack_dbobj(obj), sessid, fieldname, oobfuncname)
# stored = (args, kwargs)
# self.oob_monitor_storage[storekey] = stored
#
# def _remove_monitor(self, obj, sessid, fieldname, oobfuncname=None):
# """
# Helper method. Removes the OOB from obj.
#
# Args:
# obj (Object): The object from which to remove the monitor.
# sessid (int): The Session id associated with the monitor.
# fieldname (str): The monitored field from which to remove the monitor.
# oobfuncname (str): The oob callback function.
#
# """
# fieldmonitorname = self._get_fieldmonitor_name(fieldname)
# try:
# _GA(obj, fieldmonitorname).remove(sessid, oobfuncname=oobfuncname)
# if not _GA(obj, fieldmonitorname).subscribers:
# _DA(obj, fieldmonitorname)
# except AttributeError:
# pass
# # remove the pickle from storage
# store_key = (pack_dbobj(obj), sessid, fieldname, oobfuncname)
# self.oob_monitor_storage.pop(store_key, None)
#
# def save(self):
# """
# Handles saving of the OOBHandler data when the server reloads.
# Called from the Server process.
#
# """
# # save ourselves as a tickerhandler
# super(OOBHandler, self).save()
# # handle the extra oob monitor store
# if self.ticker_storage:
# ServerConfig.objects.conf(key=self.oob_save_name,
# value=dbserialize(self.oob_monitor_storage))
# else:
# # make sure we have nothing lingering in the database
# ServerConfig.objects.conf(key=self.oob_save_name, delete=True)
#
# def restore(self):
# """
# Called when the handler recovers after a Server reload. Called
# by the Server process as part of the reload upstart. Here we
# overload the tickerhandler's restore method completely to make
# sure we correctly re-apply and re-initialize the correct
# monitor and repeater objecth on all saved objects.
#
# """
# # load the oob monitors and initialize them
# oob_storage = ServerConfig.objects.conf(key=self.oob_save_name)
# if oob_storage:
# self.oob_storage = dbunserialize(oob_storage)
# for store_key, (args, kwargs) in self.oob_storage.items():
# # re-create the monitors
# obj, sessid, fieldname, oobfuncname = store_key
# obj = unpack_dbobj(obj)
# self._add_monitor(obj, sessid, fieldname, oobfuncname, *args, **kwargs)
# # handle the tickers (same as in TickerHandler except we call
# # the add_repeater method which makes sure to add the hooks before
# # starting the tickerpool)
# ticker_storage = ServerConfig.objects.conf(key=self.save_name)
# if ticker_storage:
# self.ticker_storage = dbunserialize(ticker_storage)
# for store_key, (args, kwargs) in self.ticker_storage.items():
# obj, interval, idstring = store_key
# obj = unpack_dbobj(obj)
# # we saved these in add_repeater before, can now retrieve them
# sessid = kwargs["_sessid"]
# oobfuncname = kwargs["_oobfuncname"]
# self.add_repeater(obj, sessid, oobfuncname, interval, *args, **kwargs)
#
# def add_repeater(self, obj, session, oobfuncname, interval=20, *args, **kwargs):
# """
# Set an oob function to be repeatedly called.
#
# Args:
# obj (Object); The object on which to register the repeat.
# session (Session): Session of the session registering.
# oobfuncname (str): Oob function name to call every interval seconds.
# interval (int, optional): Interval to call oobfunc, in seconds.
#
# Notes:
# *args, **kwargs are used as extra arguments to the oobfunc.
# """
# sessid = session
# hook = OOBAtRepeater()
# hookname = self._get_repeater_hook_name(oobfuncname, interval, sessid)
# _SA(obj, hookname, hook)
# # we store these in kwargs so that tickerhandler saves them with the rest
# kwargs.update({"_sessid":sessid, "_oobfuncname":oobfuncname})
# super(OOBHandler, self).add(obj, int(interval), oobfuncname, hookname, *args, **kwargs)
#
# def remove_repeater(self, obj, session, oobfuncname, interval=20):
# """
# Remove the repeatedly calling oob function
#
# Args:
# obj (Object): The object on which the repeater sits
# sessid (Session): Session that registered the repeater
# oobfuncname (str): Name of oob function to call at repeat
# interval (int, optional): Number of seconds between repeats
#
# """
# sessid = session.sessid
# super(OOBHandler, self).remove(obj, interval, idstring=oobfuncname)
# hookname = self._get_repeater_hook_name(oobfuncname, interval, sessid)
# try:
# _DA(obj, hookname)
# except AttributeError:
# pass
#
# def add_field_monitor(self, obj, session, field_name, oobfuncname, *args, **kwargs):
# """
# Add a monitor tracking a database field
#
# Args:
# obj (Object): The object who'se field is to be monitored.
# session (Session): Session monitoring.
# field_name (str): Name of database field to monitor. The db_* can optionally
# be skipped (it will be automatically appended if missing).
# oobfuncname (str): OOB function to call when field changes.
#
# Notes:
# When the field updates the given oobfunction will be called as
#
# `oobfuncname(session, fieldname, obj, *args, **kwargs)`
#
# where `fieldname` is the name of the monitored field and
# `obj` is the object on which the field sits. From this you
# can also easily get the new field value if you want.
#
# """
# sessid = session.sessid
# # all database field names starts with db_*
# field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
# self._add_monitor(obj, sessid, field_name, oobfuncname, *args, **kwargs)
#
# def remove_field_monitor(self, obj, session, field_name, oobfuncname=None):
# """
# Un-tracks a database field
#
# Args:
# obj (Object): Entity with the monitored field.
# session (Session): Session that monitors.
# field_name (str): database field monitored (the db_* can optionally be
# skipped (it will be auto-appended if missing).
# oobfuncname (str, optional): OOB command to call on that field.
#
# Notes:
# When the Attributes db_value updates the given oobfunction
# will be called as
#
# `oobfuncname(session, fieldname, obj, *args, **kwargs)`
#
# where `fieldname` is the name of the monitored field and
# `obj` is the object on which the field sits. From this you
# can also easily get the new field value if you want.
# """
# sessid = session.sessid
# field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
# self._remove_monitor(obj, sessid, field_name, oobfuncname=oobfuncname)
#
# def add_attribute_monitor(self, obj, session, attr_name, oobfuncname, *args, **kwargs):
# """
# Monitor the changes of an Attribute on an object. Will trigger when
# the Attribute's `db_value` field updates.
#
# Args:
# obj (Object): Object with the Attribute to monitor.
# session (Session): Session monitoring Session.
# attr_name (str): Name (key) of Attribute to monitor.
# oobfuncname (str): OOB function to call when Attribute updates.
#
# """
# sessid = session.sessid
# # get the attribute object if we can
# attrobj = obj.attributes.get(attr_name, return_obj=True)
# if attrobj:
# self._add_monitor(attrobj, sessid, "db_value", oobfuncname)
#
# def remove_attribute_monitor(self, obj, session, attr_name, oobfuncname):
# """
# Deactivate tracking for a given object's Attribute
#
# Args:
# obj (Object): Object monitored.
# session (Session): Session monitoring.
# attr_name (str): Name of Attribute monitored.
# oobfuncname (str): OOB function name called when Attribute updates.
#
# """
# sessid = session.sessid
# attrobj = obj.attributes.get(attr_name, return_obj=True)
# if attrobj:
# self._remove_monitor(attrobj, sessid, "db_value", oobfuncname)
#
# def get_all_monitors(self, session):
# """
# Get the names of all variables this session is tracking.
#
# Args:
# session (Session): Session monitoring.
# Returns:
# stored monitors (tuple): A list of tuples
# `(obj, fieldname, args, kwargs)` representing all
# the monitoring the Session with the given sessid is doing.
#
# """
# sessid = session.sessid
# # [(obj, fieldname, args, kwargs), ...]
# return [(unpack_dbobj(key[0]), key[2], stored[0], stored[1])
# for key, stored in self.oob_monitor_storage.items() if key[1] == sessid]
# access object # access object
MONITOR_HANDLER = OOBHandler() MONITOR_HANDLER = MonitorHandler()

View file

@ -62,7 +62,7 @@ from django.core.exceptions import ObjectDoesNotExist
from evennia.scripts.scripts import ExtendedLoopingCall from evennia.scripts.scripts import ExtendedLoopingCall
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.utils.logger import log_trace, log_err from evennia.utils.logger import log_trace, log_err
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj
_GA = object.__getattribute__ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
@ -349,7 +349,6 @@ class TickerHandler(object):
self.ticker_storage = dbunserialize(ticker_storage) self.ticker_storage = dbunserialize(ticker_storage)
for store_key, (args, kwargs) in self.ticker_storage.items(): for store_key, (args, kwargs) in self.ticker_storage.items():
obj, interval, idstring = store_key obj, interval, idstring = store_key
obj = unpack_dbobj(obj)
_, store_key = self._store_key(obj, interval, idstring) _, store_key = self._store_key(obj, interval, idstring)
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)

View file

@ -50,18 +50,3 @@ class ServerConfigManager(models.Manager):
return default return default
return conf[0].value return conf[0].value
def get_mysql_db_version(self):
"""
This is a helper method for specifically getting the version
string of a MySQL database.
Returns:
mysql_version (str): The currently used mysql database
version.
"""
from django.db import connection
conn = connection.cursor()
conn.execute("SELECT VERSION()")
version = conn.fetchone()
return version and str(version[0]) or ""

View file

@ -28,7 +28,6 @@ except ImportError:
from pickle import dumps, loads from pickle import dumps, loads
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from evennia.server.models import ServerConfig
from evennia.utils.utils import to_str, uses_database from evennia.utils.utils import to_str, uses_database
from evennia.utils import logger from evennia.utils import logger
@ -36,6 +35,22 @@ __all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle")
PICKLE_PROTOCOL = 2 PICKLE_PROTOCOL = 2
def _get_mysql_db_version(self):
"""
This is a helper method for specifically getting the version
string of a MySQL database.
Returns:
mysql_version (str): The currently used mysql database
version.
"""
from django.db import connection
conn = connection.cursor()
conn.execute("SELECT VERSION()")
version = conn.fetchone()
return version and str(version[0]) or ""
# initialization and helpers # initialization and helpers
_GA = object.__getattribute__ _GA = object.__getattribute__
@ -43,7 +58,7 @@ _SA = object.__setattr__
_FROM_MODEL_MAP = None _FROM_MODEL_MAP = None
_TO_MODEL_MAP = None _TO_MODEL_MAP = None
_IS_PACKED_DBOBJ = lambda o: type(o) == tuple and len(o) == 4 and o[0] == '__packed_dbobj__' _IS_PACKED_DBOBJ = lambda o: type(o) == tuple and len(o) == 4 and o[0] == '__packed_dbobj__'
if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4': if uses_database("mysql") and _get_mysql_db_version() < '5.6.4':
# mysql <5.6.4 don't support millisecond precision # mysql <5.6.4 don't support millisecond precision
_DATESTRING = "%Y:%m:%d-%H:%M:%S:000000" _DATESTRING = "%Y:%m:%d-%H:%M:%S:000000"
else: else:
@ -416,4 +431,4 @@ def dbserialize(data):
def dbunserialize(data, db_obj=None): def dbunserialize(data, db_obj=None):
"Un-serialize in one step. See from_pickle for help db_obj." "Un-serialize in one step. See from_pickle for help db_obj."
return do_unpickle(from_pickle(data, db_obj=db_obj)) return from_pickle(do_unpickle(data), db_obj=db_obj)

View file

@ -27,7 +27,7 @@ AUTO_FLUSH_MIN_INTERVAL = 60.0 * 5 # at least 5 mins between cache flushes
_GA = object.__getattribute__ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
_DA = object.__delattr__ _DA = object.__delattr__
_MONITOR_HANDLER = None
# References to db-updated objects are stored here so the # References to db-updated objects are stored here so the
# main process can be informed to re-cache itself. # main process can be informed to re-cache itself.
@ -361,6 +361,9 @@ class SharedMemoryModel(with_metaclass(SharedMemoryModelBase, Model)):
self._oob_at_<fieldname>_postsave()) self._oob_at_<fieldname>_postsave())
""" """
global _MONITOR_HANDLER
if not _MONITOR_HANDLER:
from evennia.scripts.monitorhandler import MONITOR_HANDLER as _MONITORHANDLER
if _IS_SUBPROCESS: if _IS_SUBPROCESS:
# we keep a store of objects modified in subprocesses so # we keep a store of objects modified in subprocesses so
@ -390,15 +393,18 @@ class SharedMemoryModel(with_metaclass(SharedMemoryModelBase, Model)):
update_fields = self._meta.fields update_fields = self._meta.fields
for field in update_fields: for field in update_fields:
fieldname = field.name fieldname = field.name
# trigger eventual monitors
_MONITORHANDLER.at_update(self, fieldname)
# if a hook is defined it must be named exactly on this form # if a hook is defined it must be named exactly on this form
hookname = "at_%s_postsave" % fieldname hookname = "at_%s_postsave" % fieldname
if hasattr(self, hookname) and callable(_GA(self, hookname)): if hasattr(self, hookname) and callable(_GA(self, hookname)):
_GA(self, hookname)(new) _GA(self, hookname)(new)
# if a trackerhandler is set on this object, update it with the
# fieldname and the new value # # if a trackerhandler is set on this object, update it with the
fieldtracker = "_oob_at_%s_postsave" % fieldname # # fieldname and the new value
if hasattr(self, fieldtracker): # fieldtracker = "_oob_at_%s_postsave" % fieldname
_GA(self, fieldtracker)(fieldname) # if hasattr(self, fieldtracker):
# _GA(self, fieldtracker)(fieldname)
class WeakSharedMemoryModelBase(SharedMemoryModelBase): class WeakSharedMemoryModelBase(SharedMemoryModelBase):