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:
Griatch 2015-02-11 20:58:37 +01:00
parent f1b6a4e212
commit eda15ccc45
9 changed files with 416 additions and 555 deletions

View file

@ -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)

View file

@ -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}

View file

@ -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))

View file

@ -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()

View file

@ -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', {})

View 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)

View file

@ -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

View file

@ -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()

View file

@ -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