mirror of
https://github.com/evennia/evennia.git
synced 2026-03-24 16:56:32 +01:00
Added support for GMCP out-of-band messaging, as a backup handler to MSDP. Starting to rework the oob system to be more straightforward to follow and understand.
This commit is contained in:
parent
f1b6a4e212
commit
eda15ccc45
9 changed files with 416 additions and 555 deletions
|
|
@ -1,193 +0,0 @@
|
|||
"""
|
||||
Central caching module.
|
||||
|
||||
"""
|
||||
|
||||
from sys import getsizeof
|
||||
import os
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.utils.utils import uses_database, to_str, get_evennia_pids
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_SA = object.__setattr__
|
||||
_DA = object.__delattr__
|
||||
|
||||
_IS_SUBPROCESS = os.getpid() in get_evennia_pids()
|
||||
_IS_MAIN_THREAD = threading.currentThread().getName() == "MainThread"
|
||||
|
||||
#
|
||||
# Set up the cache stores
|
||||
#
|
||||
|
||||
_ATTR_CACHE = {}
|
||||
_PROP_CACHE = defaultdict(dict)
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Cache key hash generation
|
||||
#------------------------------------------------------------
|
||||
|
||||
if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4':
|
||||
# mysql <5.6.4 don't support millisecond precision
|
||||
_DATESTRING = "%Y:%m:%d-%H:%M:%S:000000"
|
||||
else:
|
||||
_DATESTRING = "%Y:%m:%d-%H:%M:%S:%f"
|
||||
|
||||
|
||||
def hashid(obj, suffix=""):
|
||||
"""
|
||||
Returns a per-class unique hash that combines the object's
|
||||
class name with its idnum and creation time. This makes this id unique also
|
||||
between different typeclassed entities such as scripts and
|
||||
objects (which may still have the same id).
|
||||
"""
|
||||
if not obj:
|
||||
return obj
|
||||
try:
|
||||
hid = _GA(obj, "_hashid")
|
||||
except AttributeError:
|
||||
try:
|
||||
date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id")
|
||||
except AttributeError:
|
||||
try:
|
||||
# maybe a typeclass, try to go to dbobj
|
||||
obj = _GA(obj, "dbobj")
|
||||
date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id")
|
||||
except AttributeError:
|
||||
# this happens if hashing something like ndb. We have to
|
||||
# rely on memory adressing in this case.
|
||||
date, idnum = "InMemory", id(obj)
|
||||
if not idnum or not date:
|
||||
# this will happen if setting properties on an object which
|
||||
# is not yet saved
|
||||
return None
|
||||
# we have to remove the class-name's space, for eventual use
|
||||
# of memcached
|
||||
hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum)
|
||||
hid = hid.replace(" ", "")
|
||||
# we cache the object part of the hashid to avoid too many
|
||||
# object lookups
|
||||
_SA(obj, "_hashid", hid)
|
||||
# build the complete hashid
|
||||
hid = "%s%s" % (hid, suffix)
|
||||
return to_str(hid)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Cache callback handlers
|
||||
#------------------------------------------------------------
|
||||
|
||||
# callback to field pre_save signal (connected in evennia.server.server)
|
||||
#def field_pre_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_presave" % fieldname
|
||||
# handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
|
||||
# if callable(handler):
|
||||
# handler()
|
||||
|
||||
|
||||
def 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))
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Attribute lookup cache
|
||||
#------------------------------------------------------------
|
||||
|
||||
def get_attr_cache(obj):
|
||||
"Retrieve lookup cache"
|
||||
hid = hashid(obj)
|
||||
return _ATTR_CACHE.get(hid, None)
|
||||
|
||||
|
||||
def set_attr_cache(obj, store):
|
||||
"Set lookup cache"
|
||||
global _ATTR_CACHE
|
||||
hid = hashid(obj)
|
||||
_ATTR_CACHE[hid] = store
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Property cache - this is a generic cache for properties stored on models.
|
||||
#------------------------------------------------------------
|
||||
|
||||
# access methods
|
||||
|
||||
def get_prop_cache(obj, propname):
|
||||
"retrieve data from cache"
|
||||
hid = hashid(obj, "-%s" % propname)
|
||||
return _PROP_CACHE[hid].get(propname, None) if hid else None
|
||||
|
||||
|
||||
def set_prop_cache(obj, propname, propvalue):
|
||||
"Set property cache"
|
||||
hid = hashid(obj, "-%s" % propname)
|
||||
if hid:
|
||||
_PROP_CACHE[hid][propname] = propvalue
|
||||
|
||||
|
||||
def del_prop_cache(obj, propname):
|
||||
"Delete element from property cache"
|
||||
hid = hashid(obj, "-%s" % propname)
|
||||
if hid:
|
||||
if propname in _PROP_CACHE[hid]:
|
||||
del _PROP_CACHE[hid][propname]
|
||||
|
||||
|
||||
def flush_prop_cache():
|
||||
"Clear property cache"
|
||||
global _PROP_CACHE
|
||||
_PROP_CACHE = defaultdict(dict)
|
||||
|
||||
|
||||
def get_cache_sizes():
|
||||
"""
|
||||
Get cache sizes, expressed in number of objects and memory size in MB
|
||||
"""
|
||||
global _ATTR_CACHE, _PROP_CACHE
|
||||
attr_n = len(_ATTR_CACHE)
|
||||
attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0
|
||||
prop_n = sum(len(dic) for dic in _PROP_CACHE.values())
|
||||
prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0
|
||||
return (attr_n, attr_mb), (prop_n, prop_mb)
|
||||
|
||||
|
||||
|
|
@ -5,26 +5,40 @@ This module implements commands as defined by the MSDP standard
|
|||
(http://tintin.sourceforge.net/msdp/), but is independent of the
|
||||
actual transfer protocol (webclient, MSDP, GMCP etc).
|
||||
|
||||
This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
|
||||
(not classes) defined globally in this module will be made available
|
||||
to the oob mechanism.
|
||||
This module is pointed to by settings.OOB_PLUGIN_MODULES. It must
|
||||
contain a global dictionary CMD_MAP which is a dictionary that maps
|
||||
the call available in the OOB call to a function in this module.
|
||||
|
||||
For example, if the OOB strings received looks like this:
|
||||
|
||||
MDSP.LISTEN [desc, key] # GMCP (wrapping to MSDP)
|
||||
LISTEN ARRAY VAL desc VAL key # MSDP
|
||||
|
||||
and CMD_MAP = {"LISTEN", listen} then this would result in a call to a
|
||||
function "listen" in this module, with the arguments *("desc", "key").
|
||||
|
||||
oob functions have the following call signature:
|
||||
|
||||
function(oobhandler, session, *args, **kwargs)
|
||||
|
||||
where oobhandler is a back-reference to the central OOB_HANDLER
|
||||
instance and session is the active session to get return data.
|
||||
here, oobhandler always holds a back-reference to the central oob
|
||||
handler, session is the active session and *args, **kwargs are what
|
||||
is sent from the oob call.
|
||||
|
||||
The function names are not case-sensitive (this allows for names
|
||||
like "LIST" which would otherwise collide with Python builtins).
|
||||
|
||||
A function named OOB_ERROR will retrieve error strings if it is
|
||||
A function called with OOB_ERROR will retrieve error strings if it is
|
||||
defined. It will get the error message as its 3rd argument.
|
||||
|
||||
Data is usually returned via
|
||||
session.msg(oob=(cmdname, (args,), {kwargs}))
|
||||
Note that args, kwargs must be iterable/dict, non-iterables will
|
||||
be interpreted as a new command name.
|
||||
oob_error(oobhandler, session, error, *args, **kwargs)
|
||||
|
||||
This allows for customizing error handling.
|
||||
|
||||
Data is usually returned to the user via a return OOB call:
|
||||
|
||||
session.msg(oob=(oobcmdname, (args,), {kwargs}))
|
||||
|
||||
oobcmdnames (like "MSDP.LISTEN" / "LISTEN" above) are case-sensitive. Note that args,
|
||||
kwargs must be iterable. Non-iterables will be interpreted as a new
|
||||
command name (you can send multiple oob commands with one msg() call))
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -33,12 +47,14 @@ _GA = object.__getattribute__
|
|||
_SA = object.__setattr__
|
||||
_NA_SEND = lambda o: "N/A"
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# All OOB commands must be on the form
|
||||
# cmdname(oobhandler, session, *args, **kwargs)
|
||||
#------------------------------------------------------------
|
||||
|
||||
def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs):
|
||||
|
||||
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
|
||||
"""
|
||||
A function with this name is special and is called by the oobhandler when an error
|
||||
occurs already at the execution stage (such as the oob function
|
||||
|
|
@ -46,13 +62,14 @@ def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs):
|
|||
"""
|
||||
session.msg(oob=("err", ("ERROR " + errmsg,)))
|
||||
|
||||
|
||||
def ECHO(oobhandler, session, *args, **kwargs):
|
||||
def oob_echo(oobhandler, session, *args, **kwargs):
|
||||
"Test/debug function, simply returning the args and kwargs"
|
||||
session.msg(oob=("echo", args, kwargs))
|
||||
|
||||
##OOB{"SEND":"CHARACTER_NAME"}
|
||||
def SEND(oobhandler, session, *args, **kwargs):
|
||||
# MSDP standard commands
|
||||
|
||||
##OOB{"SEND":"CHARACTER_NAME"} - from webclient
|
||||
def oob_send(oobhandler, session, *args, **kwargs):
|
||||
"""
|
||||
This function directly returns the value of the given variable to the
|
||||
session.
|
||||
|
|
@ -71,7 +88,7 @@ def SEND(oobhandler, session, *args, **kwargs):
|
|||
session.msg(oob=("err", ("You must log in first.",)))
|
||||
|
||||
##OOB{"REPORT":"TEST"}
|
||||
def REPORT(oobhandler, session, *args, **kwargs):
|
||||
def oob_report(oobhandler, session, *args, **kwargs):
|
||||
"""
|
||||
This creates a tracker instance to track the data given in *args.
|
||||
|
||||
|
|
@ -99,7 +116,7 @@ def REPORT(oobhandler, session, *args, **kwargs):
|
|||
|
||||
|
||||
##OOB{"UNREPORT": "TEST"}
|
||||
def UNREPORT(oobhandler, session, *args, **kwargs):
|
||||
def oob_unreport(oobhandler, session, *args, **kwargs):
|
||||
"""
|
||||
This removes tracking for the given data given in *args.
|
||||
"""
|
||||
|
|
@ -118,7 +135,7 @@ def UNREPORT(oobhandler, session, *args, **kwargs):
|
|||
|
||||
|
||||
##OOB{"LIST":"COMMANDS"}
|
||||
def LIST(oobhandler, session, mode, *args, **kwargs):
|
||||
def oob_list(oobhandler, session, mode, *args, **kwargs):
|
||||
"""
|
||||
List available properties. Mode is the type of information
|
||||
desired:
|
||||
|
|
@ -172,7 +189,7 @@ def _repeat_callback(oobhandler, session, *args, **kwargs):
|
|||
session.msg(oob=("repeat", ("Repeat!",)))
|
||||
|
||||
##OOB{"REPEAT":10}
|
||||
def REPEAT(oobhandler, session, interval, *args, **kwargs):
|
||||
def oob_repeat(oobhandler, session, interval, *args, **kwargs):
|
||||
"""
|
||||
Test command for the repeat functionality. Note that the args/kwargs
|
||||
must not be db objects (or anything else non-picklable), rather use
|
||||
|
|
@ -184,7 +201,7 @@ def REPEAT(oobhandler, session, interval, *args, **kwargs):
|
|||
|
||||
|
||||
##OOB{"UNREPEAT":10}
|
||||
def UNREPEAT(oobhandler, session, interval):
|
||||
def oob_unrepeat(oobhandler, session, interval):
|
||||
"""
|
||||
Disable repeating callback
|
||||
"""
|
||||
|
|
@ -219,3 +236,16 @@ OOB_REPORTABLE = {
|
|||
"ROOM_NAME": "db_location",
|
||||
"TEST" : "test"
|
||||
}
|
||||
|
||||
|
||||
# this maps the commands to the names available to use from
|
||||
# the oob call
|
||||
CMD_MAP = {"OOB_ERROR": oob_error, # will get error messages
|
||||
"SEND": oob_send,
|
||||
"ECHO": oob_echo,
|
||||
"REPORT": oob_report,
|
||||
"UNREPORT": oob_unreport,
|
||||
"LIST": oob_list,
|
||||
"REPEAT": oob_repeat,
|
||||
"UNREPEAT": oob_unrepeat}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,243 +0,0 @@
|
|||
"""
|
||||
|
||||
MSDP (Mud Server Data Protocol)
|
||||
|
||||
This implements the MSDP protocol as per
|
||||
http://tintin.sourceforge.net/msdp/. MSDP manages out-of-band
|
||||
communication between the client and server, for updating health bars
|
||||
etc.
|
||||
|
||||
"""
|
||||
import re
|
||||
from evennia.utils.utils import to_str
|
||||
|
||||
# MSDP-relevant telnet cmd/opt-codes
|
||||
MSDP = chr(69)
|
||||
MSDP_VAR = chr(1)
|
||||
MSDP_VAL = chr(2)
|
||||
MSDP_TABLE_OPEN = chr(3)
|
||||
MSDP_TABLE_CLOSE = chr(4)
|
||||
MSDP_ARRAY_OPEN = chr(5)
|
||||
MSDP_ARRAY_CLOSE = chr(6)
|
||||
|
||||
IAC = chr(255)
|
||||
SB = chr(250)
|
||||
SE = chr(240)
|
||||
|
||||
force_str = lambda inp: to_str(inp, force_string=True)
|
||||
|
||||
# pre-compiled regexes
|
||||
# returns 2-tuple
|
||||
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
|
||||
MSDP_ARRAY_OPEN,
|
||||
MSDP_ARRAY_CLOSE))
|
||||
# returns 2-tuple (may be nested)
|
||||
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
|
||||
MSDP_TABLE_OPEN,
|
||||
MSDP_TABLE_CLOSE))
|
||||
regex_var = re.compile(MSDP_VAR)
|
||||
regex_val = re.compile(MSDP_VAL)
|
||||
|
||||
|
||||
# Msdp object handler
|
||||
|
||||
class Msdp(object):
|
||||
"""
|
||||
Implements the MSDP protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
Initiates by storing the protocol
|
||||
on itself and trying to determine
|
||||
if the client supports MSDP.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['MSDP'] = False
|
||||
self.protocol.negotiationMap[MSDP] = self.msdp_to_evennia
|
||||
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
|
||||
self.msdp_reported = {}
|
||||
|
||||
def no_msdp(self, option):
|
||||
"No msdp supported or wanted"
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_msdp(self, option):
|
||||
"""
|
||||
Called when client confirms that it can do MSDP.
|
||||
"""
|
||||
self.protocol.protocol_flags['MSDP'] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def evennia_to_msdp(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
handle return data from cmdname by converting it to
|
||||
a proper msdp structure. data can either be a single value (will be
|
||||
converted to a string), a list (will be converted to an MSDP_ARRAY),
|
||||
or a dictionary (will be converted to MSDP_TABLE).
|
||||
|
||||
OBS - there is no actual use of arrays and tables in the MSDP
|
||||
specification or default commands -- are returns are implemented
|
||||
as simple lists or named lists (our name for them here, these
|
||||
un-bounded structures are not named in the specification). So for
|
||||
now, this routine will not explicitly create arrays nor tables,
|
||||
although there are helper methods ready should it be needed in
|
||||
the future.
|
||||
"""
|
||||
|
||||
def make_table(name, **kwargs):
|
||||
"build a table that may be nested with other tables or arrays."
|
||||
string = MSDP_VAR + force_str(name) + MSDP_VAL + MSDP_TABLE_OPEN
|
||||
for key, val in kwargs.items():
|
||||
if isinstance(val, dict):
|
||||
string += make_table(string, key, **val)
|
||||
elif hasattr(val, '__iter__'):
|
||||
string += make_array(string, key, *val)
|
||||
else:
|
||||
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
|
||||
string += MSDP_TABLE_CLOSE
|
||||
return string
|
||||
|
||||
def make_array(name, *args):
|
||||
"build a array. Arrays may not nest tables by definition."
|
||||
string = MSDP_VAR + force_str(name) + MSDP_ARRAY_OPEN
|
||||
string += MSDP_VAL.join(force_str(arg) for arg in args)
|
||||
string += MSDP_ARRAY_CLOSE
|
||||
return string
|
||||
|
||||
def make_list(name, *args):
|
||||
"build a simple list - an array without start/end markers"
|
||||
string = MSDP_VAR + force_str(name)
|
||||
string += MSDP_VAL.join(force_str(arg) for arg in args)
|
||||
return string
|
||||
|
||||
def make_named_list(name, **kwargs):
|
||||
"build a named list - a table without start/end markers"
|
||||
string = MSDP_VAR + force_str(name)
|
||||
for key, val in kwargs.items():
|
||||
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
|
||||
return string
|
||||
|
||||
# Default MSDP commands
|
||||
|
||||
print "MSDP outgoing:", cmdname, args, kwargs
|
||||
|
||||
cupper = cmdname.upper()
|
||||
if cupper == "LIST":
|
||||
if args:
|
||||
args = list(args)
|
||||
mode = args.pop(0).upper()
|
||||
self.data_out(make_array(mode, *args))
|
||||
elif cupper == "REPORT":
|
||||
self.data_out(make_list("REPORT", *args))
|
||||
elif cupper == "UNREPORT":
|
||||
self.data_out(make_list("UNREPORT", *args))
|
||||
elif cupper == "RESET":
|
||||
self.data_out(make_list("RESET", *args))
|
||||
elif cupper == "SEND":
|
||||
self.data_out(make_named_list("SEND", **kwargs))
|
||||
else:
|
||||
# return list or named lists.
|
||||
msdp_string = ""
|
||||
if args:
|
||||
msdp_string += make_list(cupper, *args)
|
||||
if kwargs:
|
||||
msdp_string += make_named_list(cupper, **kwargs)
|
||||
self.data_out(msdp_string)
|
||||
|
||||
def msdp_to_evennia(self, data):
|
||||
"""
|
||||
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 OOB_FUNCS dictionary. command names are case-insensitive.
|
||||
|
||||
varname, var --> mapped to function varname(var)
|
||||
arrayname, array --> mapped to function arrayname(*array)
|
||||
tablename, table --> mapped to function tablename(**table)
|
||||
|
||||
Note: Combinations of args/kwargs to one function is not supported
|
||||
in this implementation (it complicates the code for limited
|
||||
gain - arrayname(*array) is usually as complex as anyone should
|
||||
ever need to go anyway (I hope!).
|
||||
|
||||
"""
|
||||
tables = {}
|
||||
arrays = {}
|
||||
variables = {}
|
||||
|
||||
if hasattr(data, "__iter__"):
|
||||
data = "".join(data)
|
||||
|
||||
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
|
||||
|
||||
for key, table in regex_table.findall(data):
|
||||
tables[key] = {}
|
||||
for varval in regex_var.split(table):
|
||||
parts = regex_val.split(varval)
|
||||
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
|
||||
for key, array in regex_array.findall(data):
|
||||
arrays[key] = []
|
||||
for val in regex_val.split(array):
|
||||
arrays[key].append(val)
|
||||
arrays[key] = tuple(arrays[key])
|
||||
for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))):
|
||||
# get remaining varvals after cleaning away tables/arrays
|
||||
parts = regex_val.split(varval)
|
||||
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
|
||||
|
||||
#print "MSDP: table, array, variables:", tables, arrays, variables
|
||||
|
||||
# all variables sent through msdp to Evennia are considered commands
|
||||
# with arguments. There are three forms of commands possible
|
||||
# through msdp:
|
||||
#
|
||||
# VARNAME VAR -> varname(var)
|
||||
# ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val)
|
||||
# TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE ->
|
||||
# tablename(varname=val, varname=val)
|
||||
#
|
||||
|
||||
# default MSDP functions
|
||||
if "LIST" in variables:
|
||||
self.data_in("list", *variables.pop("LIST"))
|
||||
if "REPORT" in variables:
|
||||
self.data_in("report", *variables.pop("REPORT"))
|
||||
if "REPORT" in arrays:
|
||||
self.data_in("report", *(arrays.pop("REPORT")))
|
||||
if "UNREPORT" in variables:
|
||||
self.data_in("unreport", *(arrays.pop("UNREPORT")))
|
||||
if "RESET" in variables:
|
||||
self.data_in("reset", *variables.pop("RESET"))
|
||||
if "RESET" in arrays:
|
||||
self.data_in("reset", *(arrays.pop("RESET")))
|
||||
if "SEND" in variables:
|
||||
self.data_in("send", *variables.pop("SEND"))
|
||||
if "SEND" in arrays:
|
||||
self.data_in("send", *(arrays.pop("SEND")))
|
||||
|
||||
# if there are anything left consider it a call to a custom function
|
||||
|
||||
for varname, var in variables.items():
|
||||
# a simple function + argument
|
||||
self.data_in(varname, (var,))
|
||||
for arrayname, array in arrays.items():
|
||||
# we assume the array are multiple arguments to the function
|
||||
self.data_in(arrayname, *array)
|
||||
for tablename, table in tables.items():
|
||||
# we assume tables are keyword arguments to the function
|
||||
self.data_in(tablename, **table)
|
||||
|
||||
def data_out(self, msdp_string):
|
||||
"""
|
||||
Return a msdp-valid subnegotiation across the protocol.
|
||||
"""
|
||||
#print "msdp data_out (without IAC SE):", msdp_string
|
||||
self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE)
|
||||
|
||||
def data_in(self, funcname, *args, **kwargs):
|
||||
"""
|
||||
Send oob data to Evennia
|
||||
"""
|
||||
#print "msdp data_in:", funcname, args, kwargs
|
||||
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))
|
||||
|
|
@ -179,6 +179,71 @@ class PortalSessionHandler(SessionHandler):
|
|||
return [sess for sess in self.get_sessions(include_unloggedin=True)
|
||||
if hasattr(sess, 'suid') and sess.suid == suid]
|
||||
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connection sessions
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.data_out(message)
|
||||
|
||||
def oobstruct_parser(self, oobstruct):
|
||||
"""
|
||||
Helper method for each session to use to parse oob structures
|
||||
(The 'oob' kwarg of the msg() method).
|
||||
|
||||
Allowed input oob structures are:
|
||||
cmdname
|
||||
((cmdname,), (cmdname,))
|
||||
(cmdname,(arg, ))
|
||||
(cmdname,(arg1,arg2))
|
||||
(cmdname,{key:val,key2:val2})
|
||||
(cmdname, (args,), {kwargs})
|
||||
((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,)))
|
||||
outputs an ordered structure on the form
|
||||
((cmdname, (args,), {kwargs}), ...), where the two last
|
||||
parts of each tuple may be empty
|
||||
"""
|
||||
def _parse(oobstruct):
|
||||
slen = len(oobstruct)
|
||||
if not oobstruct:
|
||||
return tuple(None, (), {})
|
||||
elif not hasattr(oobstruct, "__iter__"):
|
||||
# a singular command name, without arguments or kwargs
|
||||
return (oobstruct, (), {})
|
||||
# regardless of number of args/kwargs, the first element must be
|
||||
# the function name. We will not catch this error if not, but
|
||||
# allow it to propagate.
|
||||
if slen == 1:
|
||||
return (oobstruct[0], (), {})
|
||||
elif slen == 2:
|
||||
if isinstance(oobstruct[1], dict):
|
||||
# cmdname, {kwargs}
|
||||
return (oobstruct[0], (), dict(oobstruct[1]))
|
||||
elif isinstance(oobstruct[1], (tuple, list)):
|
||||
# cmdname, (args,)
|
||||
return (oobstruct[0], list(oobstruct[1]), {})
|
||||
else:
|
||||
# cmdname, cmdname
|
||||
return ((oobstruct[0], (), {}), (oobstruct[1].lower(), (), {}))
|
||||
else:
|
||||
# cmdname, (args,), {kwargs}
|
||||
return (oobstruct[0], list(oobstruct[1]), dict(oobstruct[2]))
|
||||
|
||||
if hasattr(oobstruct, "__iter__"):
|
||||
# differentiate between (cmdname, cmdname),
|
||||
# (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}),
|
||||
# (cmdname,(args),{kwargs}), ...)
|
||||
|
||||
if oobstruct and isinstance(oobstruct[0], basestring):
|
||||
return (list(_parse(oobstruct)),)
|
||||
else:
|
||||
out = []
|
||||
for oobpart in oobstruct:
|
||||
out.append(_parse(oobpart))
|
||||
return (list(out),)
|
||||
return (_parse(oobstruct),)
|
||||
|
||||
|
||||
def data_in(self, session, text="", **kwargs):
|
||||
"""
|
||||
Called by portal sessions for relaying data coming
|
||||
|
|
@ -189,20 +254,17 @@ class PortalSessionHandler(SessionHandler):
|
|||
msg=text,
|
||||
data=kwargs)
|
||||
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connection sessions
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.data_out(message)
|
||||
|
||||
def data_out(self, sessid, text=None, **kwargs):
|
||||
"""
|
||||
Called by server for having the portal relay messages and data
|
||||
to the correct session protocol.
|
||||
to the correct session protocol. We also convert oob input to
|
||||
a generic form here.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
# convert oob to the generic format
|
||||
if "oob" in kwargs:
|
||||
kwargs["oob"] = self.oobstruct_parser(kwargs["oob"])
|
||||
session.data_out(text=text, **kwargs)
|
||||
|
||||
PORTAL_SESSIONS = PortalSessionHandler()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ sessions etc.
|
|||
import re
|
||||
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO
|
||||
from evennia.server.session import Session
|
||||
from evennia.server.portal import ttype, mssp, msdp, naws
|
||||
from evennia.server.portal import ttype, mssp, telnet_oob, naws
|
||||
from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP
|
||||
from evennia.server.portal.mxp import Mxp, mxp_parse
|
||||
from evennia.utils import utils, ansi, logger
|
||||
|
|
@ -35,7 +35,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
client_address = self.transport.client
|
||||
# this number is counted down for every handshake that completes.
|
||||
# when it reaches 0 the portal/server syncs their data
|
||||
self.handshakes = 6 # naws, ttype, mccp, mssp, msdp, mxp
|
||||
self.handshakes = 6 # naws, ttype, mccp, mssp, oob, mxp
|
||||
self.init_session("telnet", client_address, self.factory.sessionhandler)
|
||||
|
||||
# negotiate client size
|
||||
|
|
@ -47,8 +47,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
self.mccp = Mccp(self)
|
||||
# negotiate mssp (crawler communication)
|
||||
self.mssp = mssp.Mssp(self)
|
||||
# msdp
|
||||
self.msdp = msdp.Msdp(self)
|
||||
# oob communication (MSDP, GMCP)
|
||||
self.oob = telnet_oob.TelnetOOB(self)
|
||||
# mxp support
|
||||
self.mxp = Mxp(self)
|
||||
# keepalive watches for dead links
|
||||
|
|
@ -211,7 +211,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
through the telnet connection.
|
||||
|
||||
valid telnet kwargs:
|
||||
oob=<string> - supply an Out-of-Band instruction.
|
||||
oob=[(cmdname,args,kwargs), ...] - supply an Out-of-Band instruction.
|
||||
xterm256=True/False - enforce xterm256 setting. If not
|
||||
given, ttype result is used. If
|
||||
client does not suport xterm256, the
|
||||
|
|
@ -237,13 +237,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
self.sendLine(str(e))
|
||||
return
|
||||
if "oob" in kwargs:
|
||||
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
|
||||
if "MSDP" in self.protocol_flags:
|
||||
for cmdname, args, kwargs in oobstruct:
|
||||
#print "cmdname, args, kwargs:", cmdname, args, kwargs
|
||||
msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs)
|
||||
#print "msdp_string:", msdp_string
|
||||
self.msdp.data_out(msdp_string)
|
||||
# oob is a list of [(cmdname, arg, kwarg), ...]
|
||||
if "OOB" in self.protocol_flags:
|
||||
for cmdname, args, kwargs in kwargs["oob"]:
|
||||
self.oob.data_out(cmdname, *args, **kwargs)
|
||||
|
||||
# parse **kwargs, falling back to ttype if nothing is given explicitly
|
||||
ttype = self.protocol_flags.get('TTYPE', {})
|
||||
|
|
|
|||
238
evennia/server/portal/telnet_oob.py
Normal file
238
evennia/server/portal/telnet_oob.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
"""
|
||||
|
||||
Telnet OOB (Out of band communication)
|
||||
|
||||
This implements the following telnet oob protocols:
|
||||
MSDP (Mud Server Data Protocol)
|
||||
GMCP (Generic Mud Communication Protocol)
|
||||
|
||||
This implements the MSDP protocol as per
|
||||
http://tintin.sourceforge.net/msdp/ and the GMCP protocol as per
|
||||
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
|
||||
|
||||
Following the lead of KaVir's protocol snippet, we first check if
|
||||
client supports MSDP and if not, we fallback to GMCP with a MSDP
|
||||
header where applicable.
|
||||
|
||||
OOB manages out-of-band
|
||||
communication between the client and server, for updating health bars
|
||||
etc. See also GMCP which is another standard doing the same thing.
|
||||
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
from evennia.utils.utils import to_str
|
||||
|
||||
# MSDP-relevant telnet cmd/opt-codes
|
||||
MSDP = chr(69)
|
||||
MSDP_VAR = chr(1)
|
||||
MSDP_VAL = chr(2)
|
||||
MSDP_TABLE_OPEN = chr(3)
|
||||
MSDP_TABLE_CLOSE = chr(4)
|
||||
MSDP_ARRAY_OPEN = chr(5)
|
||||
MSDP_ARRAY_CLOSE = chr(6)
|
||||
|
||||
GMCP = chr(200)
|
||||
|
||||
IAC = chr(255)
|
||||
SB = chr(250)
|
||||
SE = chr(240)
|
||||
|
||||
force_str = lambda inp: to_str(inp, force_string=True)
|
||||
|
||||
# pre-compiled regexes
|
||||
# returns 2-tuple
|
||||
msdp_regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
|
||||
MSDP_ARRAY_OPEN,
|
||||
MSDP_ARRAY_CLOSE))
|
||||
# returns 2-tuple (may be nested)
|
||||
msdp_regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
|
||||
MSDP_TABLE_OPEN,
|
||||
MSDP_TABLE_CLOSE))
|
||||
msdp_regex_var = re.compile(MSDP_VAR)
|
||||
msdp_regex_val = re.compile(MSDP_VAL)
|
||||
|
||||
# Msdp object handler
|
||||
|
||||
class Telnet_OOB(object):
|
||||
"""
|
||||
Implements the MSDP and GMCP protocols.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
Initiates by storing the protocol
|
||||
on itself and trying to determine
|
||||
if the client supports MSDP.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['OOB'] = False
|
||||
self.MSDP = False
|
||||
self.GMCP = False
|
||||
# detect MSDP first
|
||||
self.protocol.negotiationMap[MSDP] = self.data_in
|
||||
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
|
||||
self.oob_reported = {}
|
||||
|
||||
def no_msdp(self, option):
|
||||
"No msdp supported or wanted"
|
||||
# no msdp, check GMCP
|
||||
self.protocol.negotiationMap[GMCP] = self.data_in
|
||||
self.protocol.will(GMCP).addCallbacks(self.do_oob, self.no_oob)
|
||||
|
||||
def do_msdp(self, option):
|
||||
"MSDP supported by client"
|
||||
self.MSDP = True
|
||||
self.protocol.protocol_flags['OOB'] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def no_gmcp(self, option):
|
||||
"Neither MSDP nor GMCP supported"
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_gmcp(self, option):
|
||||
"""
|
||||
Called when client confirms that it can do MSDP or GMCP.
|
||||
"""
|
||||
self.GMCP = True
|
||||
self.protocol.protocol_flags['OOB'] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
# encoders
|
||||
|
||||
def encode_msdp(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
handle return data from cmdname by converting it to
|
||||
a proper msdp structure. These are the combinations we
|
||||
support:
|
||||
|
||||
cmdname string -> cmdname string
|
||||
cmdname *args -> cmdname MSDP_ARRAY
|
||||
cmdname **kwargs -> cmdname MSDP_TABLE
|
||||
|
||||
OBS - whereas there are also definitions for making arrays and tables in
|
||||
the specification, these are not actually used in the default
|
||||
msdp commands -- returns are implemented as simple lists or
|
||||
named lists (our name for them here, these un-bounded
|
||||
structures are not named in the specification). So for now,
|
||||
this routine will not explicitly create arrays nor tables,
|
||||
although there are helper methods ready should it be needed in
|
||||
the future.
|
||||
"""
|
||||
msdp_string = ""
|
||||
if args:
|
||||
if len(args) == 1:
|
||||
msdp_string = "%s %s" % (cmdname.upper(), args[0])
|
||||
else:
|
||||
msdp_string = "%s%s%s%s" % (MSDP_VAR, cmdname.upper(), "".join(
|
||||
"%s%s" % (MSDP_VAL, val) for val in args))
|
||||
elif kwargs:
|
||||
msdp_string = "%s%s%s" % (MSDP_VAR. cmdname.upper(), "".join(
|
||||
["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) for key, val in kwargs.items()]))
|
||||
return msdp_string
|
||||
|
||||
def encode_gmcp(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
Gmcp messages are on one of the following outgoing forms:
|
||||
|
||||
cmdname string -> cmdname string
|
||||
cmdname *args -> cmdname [arg, arg, arg, ...]
|
||||
cmdname **kwargs -> cmdname {key:arg, key:arg, ...}
|
||||
|
||||
cmdname is generally recommended to be a string on the form
|
||||
Module.Submodule.Function
|
||||
"""
|
||||
if args:
|
||||
gmcp_string = "%s %s" % (cmdname, json.dumps(args))
|
||||
elif kwargs:
|
||||
gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs))
|
||||
return gmcp_string
|
||||
|
||||
def decode_msdp(self, data):
|
||||
"""
|
||||
Decodes incoming MSDP data
|
||||
|
||||
cmdname, var --> cmdname arg
|
||||
cmdname, array --> cmdname *args
|
||||
cmdname, table --> cmdname **kwargs
|
||||
|
||||
"""
|
||||
tables = {}
|
||||
arrays = {}
|
||||
variables = {}
|
||||
|
||||
if hasattr(data, "__iter__"):
|
||||
data = "".join(data)
|
||||
|
||||
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
|
||||
|
||||
# decode
|
||||
for key, table in msdp_regex_table.findall(data):
|
||||
tables[key] = {}
|
||||
for varval in msdp_regex_var.split(table):
|
||||
parts = msdp_regex_val.split(varval)
|
||||
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
|
||||
for key, array in msdp_regex_array.findall(data):
|
||||
arrays[key] = []
|
||||
for val in msdp_regex_val.split(array):
|
||||
arrays[key].append(val)
|
||||
arrays[key] = tuple(arrays[key])
|
||||
for varval in msdp_regex_var.split(msdp_regex_array.sub("", msdp_regex_table.sub("", data))):
|
||||
# get remaining varvals after cleaning away tables/arrays
|
||||
parts = msdp_regex_val.split(varval)
|
||||
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
|
||||
|
||||
# send to the sessionhandler
|
||||
if data:
|
||||
for varname, var in variables.items():
|
||||
# a simple function + argument
|
||||
self.protocol.data_in(oob=(varname, var, {}))
|
||||
for arrayname, array in arrays.items():
|
||||
# we assume the array are multiple arguments to the function
|
||||
self.protocol.data_in(oob=(arrayname, array, {}))
|
||||
for tablename, table in tables.items():
|
||||
# we assume tables are keyword arguments to the function
|
||||
self.protocol.data_in(oob=(tablename, (), table))
|
||||
|
||||
def decode_gmcp(self, data):
|
||||
"""
|
||||
Decodes incoming GMCP data on the form 'varname <structure>'
|
||||
|
||||
cmdname string -> cmdname arg
|
||||
cmdname [arg, arg,...] -> cmdname *args
|
||||
cmdname {key:arg, key:arg, ...} -> cmdname **kwargs
|
||||
|
||||
"""
|
||||
if data:
|
||||
splits = data.split(" ", 1)
|
||||
cmdname = splits[0]
|
||||
if len(splits) < 2:
|
||||
self.protocol.data_in(oob=(cmdname, (), {}))
|
||||
else:
|
||||
struct = json.loads(splits[1])
|
||||
self.protocol.data_in(oob=(cmdname,
|
||||
struct if isinstance(struct, list) else (),
|
||||
struct if isinstance(struct, dict) else {}))
|
||||
|
||||
# access methods
|
||||
|
||||
def data_out(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
Return a msdp-valid subnegotiation across the protocol.
|
||||
"""
|
||||
if self.MSDP:
|
||||
encoded_oob = force_str(self.encode_msdp(cmdname, *args, **kwargs))
|
||||
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
|
||||
else:
|
||||
encoded_oob = force_str(self.encode_gmcp(cmdname, *args, **kwargs))
|
||||
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
|
||||
|
||||
def data_in(self, data):
|
||||
"""
|
||||
Send oob data to Evennia. The self.decode_* methods send to
|
||||
protocol.data_in() themselves.
|
||||
"""
|
||||
if self.MSDP:
|
||||
self.decode_msdp(data)
|
||||
else:
|
||||
self.decode_gmcp(data)
|
||||
|
|
@ -22,11 +22,7 @@ from evennia.server.session import Session
|
|||
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||
_GA = object.__getattribute__
|
||||
_ObjectDB = None
|
||||
_OOB_HANDLER = None
|
||||
|
||||
# load optional out-of-band function module (this acts as a verification)
|
||||
OOB_PLUGIN_MODULES = [utils.mod_import(mod)
|
||||
for mod in make_iter(settings.OOB_PLUGIN_MODULES) if mod]
|
||||
INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
|
||||
|
||||
# i18n
|
||||
|
|
@ -193,10 +189,9 @@ class ServerSession(Session):
|
|||
Send User->Evennia. This will in effect
|
||||
execute a command string on the server.
|
||||
|
||||
Especially handled keywords:
|
||||
Note that oob data is already sent to the
|
||||
oobhandler at this point.
|
||||
|
||||
oob - this should hold a dictionary of oob command calls from
|
||||
the oob-supporting protocol.
|
||||
"""
|
||||
#explicitly check for None since text can be an empty string, which is
|
||||
#also valid
|
||||
|
|
@ -218,16 +213,6 @@ class ServerSession(Session):
|
|||
categories=("inputline", "channels"), include_player=False)
|
||||
cmdhandler(self, text, callertype="session", sessid=self.sessid)
|
||||
self.update_session_counters()
|
||||
if "oob" in kwargs:
|
||||
# handle oob instructions
|
||||
global _OOB_HANDLER
|
||||
if not _OOB_HANDLER:
|
||||
from evennia.server.oobhandler import OOB_HANDLER as _OOB_HANDLER
|
||||
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None))
|
||||
#print "session.data_in: oobstruct:",oobstruct
|
||||
for (funcname, args, kwargs) in oobstruct:
|
||||
if funcname:
|
||||
_OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs)
|
||||
|
||||
execute_cmd = data_in # alias
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ _PlayerDB = None
|
|||
_ServerSession = None
|
||||
_ServerConfig = None
|
||||
_ScriptDB = None
|
||||
_OOB_HANDLER = None
|
||||
|
||||
|
||||
# AMP signals
|
||||
|
|
@ -102,63 +103,6 @@ class SessionHandler(object):
|
|||
"""
|
||||
return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items())
|
||||
|
||||
def oobstruct_parser(self, oobstruct):
|
||||
"""
|
||||
Helper method for each session to use to parse oob structures
|
||||
(The 'oob' kwarg of the msg() method).
|
||||
|
||||
Allowed input oob structures are:
|
||||
cmdname
|
||||
((cmdname,), (cmdname,))
|
||||
(cmdname,(arg, ))
|
||||
(cmdname,(arg1,arg2))
|
||||
(cmdname,{key:val,key2:val2})
|
||||
(cmdname, (args,), {kwargs})
|
||||
((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,)))
|
||||
outputs an ordered structure on the form
|
||||
((cmdname, (args,), {kwargs}), ...), where the two last
|
||||
parts of each tuple may be empty
|
||||
"""
|
||||
def _parse(oobstruct):
|
||||
slen = len(oobstruct)
|
||||
if not oobstruct:
|
||||
return tuple(None, (), {})
|
||||
elif not hasattr(oobstruct, "__iter__"):
|
||||
# a singular command name, without arguments or kwargs
|
||||
return (oobstruct.lower(), (), {})
|
||||
# regardless of number of args/kwargs, the first element must be
|
||||
# the function name. We will not catch this error if not, but
|
||||
# allow it to propagate.
|
||||
if slen == 1:
|
||||
return (oobstruct[0].lower(), (), {})
|
||||
elif slen == 2:
|
||||
if isinstance(oobstruct[1], dict):
|
||||
# cmdname, {kwargs}
|
||||
return (oobstruct[0].lower(), (), dict(oobstruct[1]))
|
||||
elif isinstance(oobstruct[1], (tuple, list)):
|
||||
# cmdname, (args,)
|
||||
return (oobstruct[0].lower(), list(oobstruct[1]), {})
|
||||
else:
|
||||
# cmdname, cmdname
|
||||
return ((oobstruct[0].lower(), (), {}), (oobstruct[1].lower(), (), {}))
|
||||
else:
|
||||
# cmdname, (args,), {kwargs}
|
||||
return (oobstruct[0].lower(), list(oobstruct[1]), dict(oobstruct[2]))
|
||||
|
||||
if hasattr(oobstruct, "__iter__"):
|
||||
# differentiate between (cmdname, cmdname),
|
||||
# (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}),
|
||||
# (cmdname,(args),{kwargs}), ...)
|
||||
|
||||
if oobstruct and isinstance(oobstruct[0], basestring):
|
||||
return (list(_parse(oobstruct)),)
|
||||
else:
|
||||
out = []
|
||||
for oobpart in oobstruct:
|
||||
out.append(_parse(oobpart))
|
||||
return (list(out),)
|
||||
return (_parse(oobstruct),)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Server-SessionHandler class
|
||||
|
|
@ -482,11 +426,22 @@ class ServerSessionHandler(SessionHandler):
|
|||
|
||||
def data_in(self, sessid, text="", **kwargs):
|
||||
"""
|
||||
Data Portal -> Server
|
||||
Data Portal -> Server.
|
||||
We also intercept OOB communication here.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
text = text and to_unicode(strip_control_sequences(text), encoding=session.encoding)
|
||||
if "oob" in kwargs:
|
||||
# incoming data is always on the form (cmdname, args, kwargs)
|
||||
global _OOB_HANDLER
|
||||
if not _OOB_HANDLER:
|
||||
from evennia.server.oobhandler import OOB_HANDLER as _OOB_HANDLER
|
||||
funcname, args, kwargs = kwargs.pop("oob")
|
||||
if funcname:
|
||||
_OOB_HANDLER.execute_cmd(session, funcname, *args, **kwargs)
|
||||
|
||||
# pass the rest off to the session
|
||||
session.data_in(text=text, **kwargs)
|
||||
|
||||
SESSIONS = ServerSessionHandler()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import os, threading, gc, time
|
|||
from weakref import WeakValueDictionary
|
||||
from twisted.internet.reactor import callFromThread
|
||||
from django.core.exceptions import ObjectDoesNotExist, FieldError
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.base import Model, ModelBase
|
||||
from django.db.models.signals import post_save, pre_delete, post_syncdb
|
||||
from evennia.utils import logger
|
||||
|
|
@ -22,11 +23,40 @@ from manager import SharedMemoryManager
|
|||
|
||||
AUTO_FLUSH_MIN_INTERVAL = 60.0 * 5 # at least 5 mins between cache flushes
|
||||
|
||||
|
||||
_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.
|
||||
PROC_MODIFIED_COUNT = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue