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
|
(http://tintin.sourceforge.net/msdp/), but is independent of the
|
||||||
actual transfer protocol (webclient, MSDP, GMCP etc).
|
actual transfer protocol (webclient, MSDP, GMCP etc).
|
||||||
|
|
||||||
This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
|
This module is pointed to by settings.OOB_PLUGIN_MODULES. It must
|
||||||
(not classes) defined globally in this module will be made available
|
contain a global dictionary CMD_MAP which is a dictionary that maps
|
||||||
to the oob mechanism.
|
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:
|
oob functions have the following call signature:
|
||||||
|
|
||||||
function(oobhandler, session, *args, **kwargs)
|
function(oobhandler, session, *args, **kwargs)
|
||||||
|
|
||||||
where oobhandler is a back-reference to the central OOB_HANDLER
|
here, oobhandler always holds a back-reference to the central oob
|
||||||
instance and session is the active session to get return data.
|
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
|
A function called with OOB_ERROR will retrieve error strings if it is
|
||||||
like "LIST" which would otherwise collide with Python builtins).
|
|
||||||
|
|
||||||
A function named OOB_ERROR will retrieve error strings if it is
|
|
||||||
defined. It will get the error message as its 3rd argument.
|
defined. It will get the error message as its 3rd argument.
|
||||||
|
|
||||||
Data is usually returned via
|
oob_error(oobhandler, session, error, *args, **kwargs)
|
||||||
session.msg(oob=(cmdname, (args,), {kwargs}))
|
|
||||||
Note that args, kwargs must be iterable/dict, non-iterables will
|
This allows for customizing error handling.
|
||||||
be interpreted as a new command name.
|
|
||||||
|
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__
|
_SA = object.__setattr__
|
||||||
_NA_SEND = lambda o: "N/A"
|
_NA_SEND = lambda o: "N/A"
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
#------------------------------------------------------------
|
||||||
# All OOB commands must be on the form
|
# All OOB commands must be on the form
|
||||||
# cmdname(oobhandler, session, *args, **kwargs)
|
# 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
|
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
|
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,)))
|
session.msg(oob=("err", ("ERROR " + errmsg,)))
|
||||||
|
|
||||||
|
def oob_echo(oobhandler, session, *args, **kwargs):
|
||||||
def ECHO(oobhandler, session, *args, **kwargs):
|
|
||||||
"Test/debug function, simply returning the args and kwargs"
|
"Test/debug function, simply returning the args and kwargs"
|
||||||
session.msg(oob=("echo", args, kwargs))
|
session.msg(oob=("echo", args, kwargs))
|
||||||
|
|
||||||
##OOB{"SEND":"CHARACTER_NAME"}
|
# MSDP standard commands
|
||||||
def SEND(oobhandler, session, *args, **kwargs):
|
|
||||||
|
##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
|
This function directly returns the value of the given variable to the
|
||||||
session.
|
session.
|
||||||
|
|
@ -71,7 +88,7 @@ def SEND(oobhandler, session, *args, **kwargs):
|
||||||
session.msg(oob=("err", ("You must log in first.",)))
|
session.msg(oob=("err", ("You must log in first.",)))
|
||||||
|
|
||||||
##OOB{"REPORT":"TEST"}
|
##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.
|
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"}
|
##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.
|
This removes tracking for the given data given in *args.
|
||||||
"""
|
"""
|
||||||
|
|
@ -118,7 +135,7 @@ def UNREPORT(oobhandler, session, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
##OOB{"LIST":"COMMANDS"}
|
##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
|
List available properties. Mode is the type of information
|
||||||
desired:
|
desired:
|
||||||
|
|
@ -172,7 +189,7 @@ def _repeat_callback(oobhandler, session, *args, **kwargs):
|
||||||
session.msg(oob=("repeat", ("Repeat!",)))
|
session.msg(oob=("repeat", ("Repeat!",)))
|
||||||
|
|
||||||
##OOB{"REPEAT":10}
|
##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
|
Test command for the repeat functionality. Note that the args/kwargs
|
||||||
must not be db objects (or anything else non-picklable), rather use
|
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}
|
##OOB{"UNREPEAT":10}
|
||||||
def UNREPEAT(oobhandler, session, interval):
|
def oob_unrepeat(oobhandler, session, interval):
|
||||||
"""
|
"""
|
||||||
Disable repeating callback
|
Disable repeating callback
|
||||||
"""
|
"""
|
||||||
|
|
@ -219,3 +236,16 @@ OOB_REPORTABLE = {
|
||||||
"ROOM_NAME": "db_location",
|
"ROOM_NAME": "db_location",
|
||||||
"TEST" : "test"
|
"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)
|
return [sess for sess in self.get_sessions(include_unloggedin=True)
|
||||||
if hasattr(sess, 'suid') and sess.suid == suid]
|
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):
|
def data_in(self, session, text="", **kwargs):
|
||||||
"""
|
"""
|
||||||
Called by portal sessions for relaying data coming
|
Called by portal sessions for relaying data coming
|
||||||
|
|
@ -189,20 +254,17 @@ class PortalSessionHandler(SessionHandler):
|
||||||
msg=text,
|
msg=text,
|
||||||
data=kwargs)
|
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):
|
def data_out(self, sessid, text=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called by server for having the portal relay messages and data
|
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)
|
session = self.sessions.get(sessid, None)
|
||||||
if session:
|
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)
|
session.data_out(text=text, **kwargs)
|
||||||
|
|
||||||
PORTAL_SESSIONS = PortalSessionHandler()
|
PORTAL_SESSIONS = PortalSessionHandler()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ sessions etc.
|
||||||
import re
|
import re
|
||||||
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO
|
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO
|
||||||
from evennia.server.session import Session
|
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.mccp import Mccp, mccp_compress, MCCP
|
||||||
from evennia.server.portal.mxp import Mxp, mxp_parse
|
from evennia.server.portal.mxp import Mxp, mxp_parse
|
||||||
from evennia.utils import utils, ansi, logger
|
from evennia.utils import utils, ansi, logger
|
||||||
|
|
@ -35,7 +35,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
client_address = self.transport.client
|
client_address = self.transport.client
|
||||||
# this number is counted down for every handshake that completes.
|
# this number is counted down for every handshake that completes.
|
||||||
# when it reaches 0 the portal/server syncs their data
|
# 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)
|
self.init_session("telnet", client_address, self.factory.sessionhandler)
|
||||||
|
|
||||||
# negotiate client size
|
# negotiate client size
|
||||||
|
|
@ -47,8 +47,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
self.mccp = Mccp(self)
|
self.mccp = Mccp(self)
|
||||||
# negotiate mssp (crawler communication)
|
# negotiate mssp (crawler communication)
|
||||||
self.mssp = mssp.Mssp(self)
|
self.mssp = mssp.Mssp(self)
|
||||||
# msdp
|
# oob communication (MSDP, GMCP)
|
||||||
self.msdp = msdp.Msdp(self)
|
self.oob = telnet_oob.TelnetOOB(self)
|
||||||
# mxp support
|
# mxp support
|
||||||
self.mxp = Mxp(self)
|
self.mxp = Mxp(self)
|
||||||
# keepalive watches for dead links
|
# keepalive watches for dead links
|
||||||
|
|
@ -211,7 +211,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
through the telnet connection.
|
through the telnet connection.
|
||||||
|
|
||||||
valid telnet kwargs:
|
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
|
xterm256=True/False - enforce xterm256 setting. If not
|
||||||
given, ttype result is used. If
|
given, ttype result is used. If
|
||||||
client does not suport xterm256, the
|
client does not suport xterm256, the
|
||||||
|
|
@ -237,13 +237,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
self.sendLine(str(e))
|
self.sendLine(str(e))
|
||||||
return
|
return
|
||||||
if "oob" in kwargs:
|
if "oob" in kwargs:
|
||||||
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
|
# oob is a list of [(cmdname, arg, kwarg), ...]
|
||||||
if "MSDP" in self.protocol_flags:
|
if "OOB" in self.protocol_flags:
|
||||||
for cmdname, args, kwargs in oobstruct:
|
for cmdname, args, kwargs in kwargs["oob"]:
|
||||||
#print "cmdname, args, kwargs:", cmdname, args, kwargs
|
self.oob.data_out(cmdname, *args, **kwargs)
|
||||||
msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs)
|
|
||||||
#print "msdp_string:", msdp_string
|
|
||||||
self.msdp.data_out(msdp_string)
|
|
||||||
|
|
||||||
# parse **kwargs, falling back to ttype if nothing is given explicitly
|
# parse **kwargs, falling back to ttype if nothing is given explicitly
|
||||||
ttype = self.protocol_flags.get('TTYPE', {})
|
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
|
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||||
_GA = object.__getattribute__
|
_GA = object.__getattribute__
|
||||||
_ObjectDB = None
|
_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
|
INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
|
||||||
|
|
||||||
# i18n
|
# i18n
|
||||||
|
|
@ -193,10 +189,9 @@ class ServerSession(Session):
|
||||||
Send User->Evennia. This will in effect
|
Send User->Evennia. This will in effect
|
||||||
execute a command string on the server.
|
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
|
#explicitly check for None since text can be an empty string, which is
|
||||||
#also valid
|
#also valid
|
||||||
|
|
@ -218,16 +213,6 @@ class ServerSession(Session):
|
||||||
categories=("inputline", "channels"), include_player=False)
|
categories=("inputline", "channels"), include_player=False)
|
||||||
cmdhandler(self, text, callertype="session", sessid=self.sessid)
|
cmdhandler(self, text, callertype="session", sessid=self.sessid)
|
||||||
self.update_session_counters()
|
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
|
execute_cmd = data_in # alias
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ _PlayerDB = None
|
||||||
_ServerSession = None
|
_ServerSession = None
|
||||||
_ServerConfig = None
|
_ServerConfig = None
|
||||||
_ScriptDB = None
|
_ScriptDB = None
|
||||||
|
_OOB_HANDLER = None
|
||||||
|
|
||||||
|
|
||||||
# AMP signals
|
# AMP signals
|
||||||
|
|
@ -102,63 +103,6 @@ class SessionHandler(object):
|
||||||
"""
|
"""
|
||||||
return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items())
|
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
|
# Server-SessionHandler class
|
||||||
|
|
@ -482,11 +426,22 @@ class ServerSessionHandler(SessionHandler):
|
||||||
|
|
||||||
def data_in(self, sessid, text="", **kwargs):
|
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)
|
session = self.sessions.get(sessid, None)
|
||||||
if session:
|
if session:
|
||||||
text = text and to_unicode(strip_control_sequences(text), encoding=session.encoding)
|
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)
|
session.data_in(text=text, **kwargs)
|
||||||
|
|
||||||
SESSIONS = ServerSessionHandler()
|
SESSIONS = ServerSessionHandler()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import os, threading, gc, time
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
from twisted.internet.reactor import callFromThread
|
from twisted.internet.reactor import callFromThread
|
||||||
from django.core.exceptions import ObjectDoesNotExist, FieldError
|
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.base import Model, ModelBase
|
||||||
from django.db.models.signals import post_save, pre_delete, post_syncdb
|
from django.db.models.signals import post_save, pre_delete, post_syncdb
|
||||||
from evennia.utils import logger
|
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
|
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__
|
||||||
|
|
||||||
|
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
|
# 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.
|
||||||
PROC_MODIFIED_COUNT = 0
|
PROC_MODIFIED_COUNT = 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue