434 lines
17 KiB
Python
434 lines
17 KiB
Python
"""
|
|
OOBHandler - Out Of Band Handler
|
|
|
|
The OOBHandler.execute_cmd is called by the sessionhandler when it detects
|
|
an OOB instruction (exactly how this looked depends on the protocol; at this
|
|
point all oob calls should look the same)
|
|
|
|
The handler pieces of functionality:
|
|
|
|
function execution - the oob protocol can execute a function directly on
|
|
the server. The available functions must be defined
|
|
as global functions in settings.OOB_PLUGIN_MODULES.
|
|
repeat func execution - the oob protocol can request a given function be
|
|
executed repeatedly at a regular interval. This
|
|
uses an internal script pool.
|
|
tracking - the oob protocol can request Evennia to track changes to
|
|
fields on objects, as well as changes in Attributes. This is
|
|
done by dynamically adding tracker-objects on entities. The
|
|
behaviour of those objects can be customized by adding new
|
|
tracker classes in settings.OOB_PLUGIN_MODULES.
|
|
|
|
What goes into the OOB_PLUGIN_MODULES is a (list of) modules that contains
|
|
the working server-side code available to the OOB system: oob functions and
|
|
tracker classes.
|
|
|
|
oob functions have the following call signature:
|
|
function(caller, session, *args, **kwargs)
|
|
|
|
oob trackers should inherit from the OOBTracker class (in this
|
|
module) and implement a minimum of the same functionality.
|
|
|
|
If a function named "oob_error" is given, this will be called with error
|
|
messages.
|
|
|
|
"""
|
|
|
|
from inspect import isfunction
|
|
from collections import defaultdict
|
|
from django.conf import settings
|
|
from evennia.server.models import ServerConfig
|
|
from evennia.server.sessionhandler import SESSIONS
|
|
from evennia.scripts.tickerhandler import TickerHandler
|
|
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
|
|
from evennia.utils import logger
|
|
from evennia.utils.utils import all_from_module, make_iter
|
|
|
|
_SA = object.__setattr__
|
|
_GA = object.__getattribute__
|
|
_DA = object.__delattr__
|
|
|
|
# load resources from plugin module
|
|
_OOB_FUNCS = {}
|
|
for mod in make_iter(settings.OOB_PLUGIN_MODULES):
|
|
_OOB_FUNCS.update(mod.CMD_MAP)
|
|
|
|
# get the command to receive eventual error strings
|
|
_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
|
|
if not _OOB_ERROR:
|
|
# create default oob error message function
|
|
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
|
|
"""
|
|
Fallback error handler. This will be used if no custom
|
|
oob_error is defined and just echoes the error back to the
|
|
session.
|
|
"""
|
|
session.msg(oob=("err", ("ERROR ", errmsg)))
|
|
_OOB_ERROR = oob_error
|
|
|
|
|
|
#
|
|
# 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
|
|
tracked object as "_oob_at_<fieldname>_update".
|
|
the update() method will be called by the
|
|
save mechanism, which in turn will call the
|
|
user-customizable func()
|
|
"""
|
|
def __init__(self):
|
|
"""
|
|
This initializes the monitor with the object it sits on.
|
|
"""
|
|
self.subscribers = defaultdict(list)
|
|
|
|
def __call__(self, obj, fieldname):
|
|
"""
|
|
Called by the save() mechanism when the given
|
|
field has updated.
|
|
"""
|
|
for sessid, (oobfuncname, args, kwargs) in self.subscribers.items():
|
|
OOB_HANDLER.execute_cmd(sessid, oobfuncname, fieldname, obj, *args, **kwargs)
|
|
|
|
def add(self, sessid, oobfuncname, *args, **kwargs):
|
|
"""
|
|
Add a specific tracking callback to monitor
|
|
|
|
Args:
|
|
sessid (int): Session id
|
|
oobfuncname (str): oob command to call when field updates
|
|
args,kwargs: 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[sessid].append((oobfuncname, args, kwargs))
|
|
|
|
def remove(self, sessid, oobfuncname=None):
|
|
"""
|
|
Remove a subscribing session from the monitor
|
|
|
|
Args:
|
|
sessid(int): Session id
|
|
Keyword Args:
|
|
oobfuncname (str, optional): Only delete this cmdname.
|
|
If not given, delete all.
|
|
|
|
"""
|
|
if oobfuncname:
|
|
self.subscribers[sessid] = [item for item in self.subscribers[sessid]
|
|
if item[0] != oobfuncname]
|
|
else:
|
|
self.subscribers.pop(sessid, None)
|
|
|
|
|
|
class OOBAtRepeat(object):
|
|
"""
|
|
This object should be stored on a target object, named
|
|
as the hook to call repeatedly, e.g.
|
|
|
|
_oob_listen_every_20s_for_sessid_1 = AtRepat()
|
|
"""
|
|
|
|
def __call__(self, sessid, oobfuncname, *args, **kwargs):
|
|
"Called at regular intervals. Calls the oob function"
|
|
OOB_HANDLER.execute_cmd(sessid, oobfuncname, *args, **kwargs)
|
|
|
|
|
|
# Main OOB Handler
|
|
|
|
class OOBHandler(TickerHandler):
|
|
"""
|
|
The OOBHandler manages all server-side OOB functionality
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.save_name = "oob_ticker_storage"
|
|
self.oob_save_name = "oob_monitor_storage"
|
|
self.oob_monitor_storage = {}
|
|
super(OOBHandler, self).__init__(*args, **kwargs)
|
|
|
|
def _get_repeat_hook_name(self, oobfuncname, interval, sessid):
|
|
"Return the unique repeat call hook name for this object"
|
|
return "_oob_%s_every_%ss_for_sessid_%s" % (oobfuncname, interval, sessid)
|
|
|
|
def _get_fieldmonitor_name(self, fieldname):
|
|
"Return the fieldmonitor name"
|
|
return "_oob_at_%s_postsave" % fieldname
|
|
|
|
def _add_monitor(self, obj, sessid, fieldname, oobfuncname, *args, **kwargs):
|
|
"""
|
|
Create a fieldmonitor and store it on the object. This tracker
|
|
will be updated whenever the given field changes.
|
|
"""
|
|
fieldmonitorname = self._get_fieldtracker_name(fieldname)
|
|
if not hasattr(obj, fieldmonitorname):
|
|
# assign a new fieldmonitor to the object
|
|
_SA(obj, fieldmonitorname, OOBFieldMonitor())
|
|
# 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):
|
|
"""
|
|
Remove the OOB from obj. If oob implements an
|
|
at_delete hook, this will be called with args, kwargs
|
|
"""
|
|
fieldmonitorname = self._get_fieldtracker_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 repeat objects 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_repeat 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_repeat before, can now retrieve them
|
|
sessid = kwargs["sessid"]
|
|
oobfuncname = kwargs["oobfuncname"]
|
|
self.add_repeat(obj, sessid, oobfuncname, interval, *args, **kwargs)
|
|
|
|
def add_repeat(self, obj, sessid, oobfuncname, interval=20, *args, **kwargs):
|
|
"""
|
|
Set an oob function to be repeatedly called.
|
|
|
|
Args:
|
|
obj (Object) - the object on which to register the repeat
|
|
sessid (int) - session id 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.
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.sessid
|
|
|
|
hook = OOBAtRepeat()
|
|
hookname = self._get_repeat_hook_name(oobfuncname, interval, sessid)
|
|
_SA(obj, hookname, hook)
|
|
kwargs.update({"sessid":sessid, "oobfuncname":oobfuncname})
|
|
# we store these in kwargs so that tickerhandler saves them with the rest
|
|
kwargs["sessid"] = sessid
|
|
kwargs["oobfuncbame"] = oobfuncname
|
|
self.add(obj, interval, idstring=oobfuncname, hook_key=hookname, *args, **kwargs)
|
|
|
|
def remove_repeat(self, obj, sessid, oobfuncname, interval=20):
|
|
"""
|
|
Remove the repeatedly calling oob function
|
|
|
|
Args:
|
|
obj (Object): The object on which the repeater sits
|
|
sessid (int): Session id of the Session that registered the repeat
|
|
oobfuncname (str): Name of oob function to call at repeat
|
|
interval (int, optional): Number of seconds between repeats
|
|
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.sessid
|
|
self.remove(obj, interval, idstring=oobfuncname)
|
|
hookname = self._get_repeat_hook_name(oobfuncname, interval, sessid)
|
|
try:
|
|
_DA(obj, hookname)
|
|
except AttributeError:
|
|
pass
|
|
|
|
def add_field_monitor(self, obj, sessid, field_name, oobfuncname, *args, **kwargs):
|
|
"""
|
|
Add a monitor tracking a database field
|
|
|
|
Args:
|
|
obj (Object): The object who'se field is to be monitored
|
|
sessid (int): Session if of the 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(oobhandler, 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.
|
|
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.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, field_name, oobfuncname=None)
|
|
|
|
def remove_field_monitor(self, obj, sessid, field_name, oobfuncname=None):
|
|
"""
|
|
Un-tracks a database field
|
|
|
|
Args:
|
|
obj (Object): Entity with the monitored field
|
|
sessid (int): Session id of 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
|
|
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.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, sessid, attr_name, oobfuncname):
|
|
"""
|
|
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.
|
|
sessid (int): Session id of monitoring Session.
|
|
attr_name (str): Name (key) of Attribute to monitor.
|
|
oobfuncname (str): OOB function to call when Attribute updates.
|
|
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.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, sessid, attr_name, oobfuncname):
|
|
"""
|
|
Deactivate tracking for a given object's Attribute
|
|
|
|
Args:
|
|
obj (Object): Object monitored.
|
|
sessid (int): Session id of monitoring Session.
|
|
attr_name (str): Name of Attribute monitored.
|
|
oobfuncname (str): OOB function name called when Attribute updates.
|
|
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.sessid
|
|
attrobj = obj.attributes.get(attr_name, return_obj=True)
|
|
if attrobj:
|
|
self._remove_monitor(attrobj, sessid, "db_value", attr_name, oobfuncname)
|
|
|
|
def get_all_monitors(self, sessid):
|
|
"""
|
|
Get the names of all variables this session is tracking.
|
|
|
|
Args:
|
|
sessid (id): Session id of monitoring Session
|
|
Returns:
|
|
stored monitors (tuple): A list of tuples
|
|
`(obj, fieldname, args, kwargs)` representing all
|
|
the monitoring the Session with the given sessid is doing.
|
|
"""
|
|
# check so we didn't get a session instead of a sessid
|
|
if not isinstance(sessid, int):
|
|
sessid = sessid.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 method - called from session.msg()
|
|
|
|
def execute_cmd(self, session, oobfuncname, *args, **kwargs):
|
|
"""
|
|
Execute an oob command
|
|
|
|
Args:
|
|
session (Session or int): Session or Session.sessid calling
|
|
the oob command
|
|
oobfuncname (str): The name of the oob command (case sensitive)
|
|
|
|
Notes:
|
|
If the oobfuncname is a valid oob function, the `*args` and
|
|
`**kwargs` are passed into the oob command.
|
|
|
|
"""
|
|
if isinstance(session, int):
|
|
# a sessid. Convert to a session
|
|
session = SESSIONS.session_from_sessid(self.sessid)
|
|
if not session:
|
|
errmsg = "OOB Error: execute_cmd(%s,%s,%s,%s) - no valid session" % \
|
|
(session, oobfuncname, args, kwargs)
|
|
raise RuntimeError(errmsg)
|
|
|
|
# don't catch this, wrong oobfuncname should be reported
|
|
oobfunc = _OOB_FUNCS[oobfuncname]
|
|
|
|
# we found an oob command. Execute it.
|
|
try:
|
|
#print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
|
|
oobfunc(self, session, *args, **kwargs)
|
|
except Exception, err:
|
|
errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (oobfuncname, args, kwargs, err)
|
|
if _OOB_ERROR:
|
|
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
|
|
logger.log_trace(errmsg)
|
|
raise Exception(errmsg)
|
|
|
|
|
|
# access object
|
|
OOB_HANDLER = OOBHandler()
|