From 1bcd9abc6bf3186555dddb93854fabb2a5729d4b Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 13 Oct 2014 08:41:22 +0200 Subject: [PATCH 01/11] Changed order of Script save. This avoids a race condition where Scripts that die very quickly tries to delete themselves before having had time to save first. Resolves #597. --- src/utils/create.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/create.py b/src/utils/create.py index 7414697a4e..6a2ac42a88 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -305,11 +305,15 @@ def create_script(typeclass, key=None, obj=None, player=None, locks=None, if persistent is not None: new_script.persistent = persistent + # must do this before starting the script since some + # scripts may otherwise run for a very short time and + # try to delete itself before we have a time to save it. + new_db_script.save() + # a new created script should usually be started. if autostart: new_script.start() - new_db_script.save() return new_script #alias script = create_script From fb7ac49253d40260f01fdd084ee9340304fd0f04 Mon Sep 17 00:00:00 2001 From: Simon Vermeersch Date: Tue, 14 Oct 2014 18:27:58 +0200 Subject: [PATCH 02/11] Escape <, > and & when MXP is enabled. --- src/server/portal/mxp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/portal/mxp.py b/src/server/portal/mxp.py index 94eb3ac7f0..05d9958a05 100644 --- a/src/server/portal/mxp.py +++ b/src/server/portal/mxp.py @@ -28,6 +28,10 @@ def mxp_parse(text): """ Replaces links to the correct format for MXP. """ + text = text.replace("&", "&") \ + .replace("<", "<") \ + .replace(">", ">") + text = LINKS_SUB.sub(MXP_SEND, text) return text From 8a9e1a9b4bffa1a36bfcf2bffbebcabba128666b Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 17 Oct 2014 18:28:53 +0200 Subject: [PATCH 03/11] Fixed arg_regex inheritance across child commands. Resolves #601. --- src/commands/command.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/command.py b/src/commands/command.py index 04bd773283..bec477b9bb 100644 --- a/src/commands/command.py +++ b/src/commands/command.py @@ -62,8 +62,6 @@ def _init_command(mcs, **kwargs): if hasattr(mcs, 'arg_regex') and isinstance(mcs.arg_regex, basestring): mcs.arg_regex = re.compile(r"%s" % mcs.arg_regex, re.I) - else: - mcs.arg_regex = None if not hasattr(mcs, "auto_help"): mcs.auto_help = True if not hasattr(mcs, 'is_exit'): @@ -140,14 +138,16 @@ class Command(object): locks = "" # used by the help system to group commands in lists. help_category = "general" - # This allows to turn off auto-help entry creation for individual commands. auto_help = True + # optimization for quickly separating exit-commands from normal commands + is_exit = False + # define the command not only by key but by the regex form of its arguments + arg_regex = None + # auto-set (by Evennia on command instantiation) are: # obj - which object this command is defined on - # sessid - which session-id (if any) is responsible for - # triggering this command - # + # sessid - which session-id (if any) is responsible for triggering this command def __init__(self, **kwargs): """the lockhandler works the same as for objects. From 182488a713603f7207fff0b566e5f7421a28326f Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 17 Oct 2014 19:32:33 +0200 Subject: [PATCH 04/11] Updates to menu_node to make sure separate, new cmdsets are initialized in different tree instances, as per #596. --- contrib/menu_login.py | 3 ++- contrib/menusystem.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contrib/menu_login.py b/contrib/menu_login.py index f7d92e899b..5947beb003 100644 --- a/contrib/menu_login.py +++ b/contrib/menu_login.py @@ -346,7 +346,7 @@ node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP, class UnloggedInCmdSet(CmdSet): "Cmdset for the unloggedin state" - key = "UnloggedinState" + key = "DefaultUnloggedin" priority = 0 def at_cmdset_creation(self): @@ -361,6 +361,7 @@ class CmdUnloggedinLook(Command): to the menu's own look command.. """ key = CMD_LOGINSTART + aliases = ["look", "l"] locks = "cmd:all()" def func(self): diff --git a/contrib/menusystem.py b/contrib/menusystem.py index 35b2a41990..dbab4e1f9e 100644 --- a/contrib/menusystem.py +++ b/contrib/menusystem.py @@ -150,7 +150,7 @@ class MenuTree(object): tree as needed. For safety, being in a menu will not survive a server reboot. - A menutree have two special node keys given by 'startnode' and + A menutree has two special node keys given by 'startnode' and 'endnode' arguments. The startnode is where the user will start upon first entering the menu. The endnode need not actually exist, the moment it is linked to and that link is used, the menu @@ -188,7 +188,6 @@ class MenuTree(object): Add a menu node object to the tree. Each node itself keeps track of which nodes it is connected to. """ - menunode.init(self) self.tree[menunode.key] = menunode def goto(self, key): @@ -206,6 +205,8 @@ class MenuTree(object): # not exiting, look for a valid code. node = self.tree.get(key, None) if node: + # initialize - this creates new cmdset + node.init(self) if node.code: # Execute eventual code active on this # node. self.caller is available at this point. @@ -266,7 +267,7 @@ class MenuNode(object): code block, as well as ev. nodefaultcmds - if true, don't offer the default help and look commands in the node - separator - this string will be put on the line between menu nodes5B. + separator - this string will be put on the line between menu nodes. """ self.key = key self.cmdset = None From 36629a8bdb3bb9c4e69df228b5293c4e255e3afe Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 19 Oct 2014 19:11:56 +0200 Subject: [PATCH 05/11] Fixed a race condition when the telnet protocol synced with the server with a delay (such as when the connecting client didn't respond to all protocol request tokens. This could lead to the sync overwriting already updated session flags, notably the puppet id (puid). Resolves #583. --- src/server/portal/portalsessionhandler.py | 8 ++++++++ src/server/serversession.py | 6 ------ src/server/sessionhandler.py | 5 +++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index 9ff5d122d7..0d9b1e3b08 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -64,6 +64,14 @@ class PortalSessionHandler(SessionHandler): # only use if session already has sessid (i.e. has already connected) sessdata = session.get_sync_data() if self.portal.amp_protocol: + # we remove sessdata that could already have changed on the + # server level + sessdata = dict((key, val) for key, val in sessdata.items() if key in ("protocol_key", + "address", + "suid", + "conn_time", + "protocol_flags", + "server_data",)) self.portal.amp_protocol.call_remote_ServerAdmin(session.sessid, operation=PCONNSYNC, data=sessdata) diff --git a/src/server/serversession.py b/src/server/serversession.py index 3498d74ffc..825ef2c574 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -234,12 +234,6 @@ class ServerSession(Session): Send Evennia -> User """ text = text if text else "" - #if text is None: - # text = "" - #else: - # text = to_unicode(text) - # text = to_str(text, self.encoding) - self.sessionhandler.data_out(self, text=text, **kwargs) def __eq__(self, other): diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index d4498054f1..b4b17eff02 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -220,6 +220,11 @@ class ServerSessionHandler(SessionHandler): sessid = portalsessiondata.get("sessid") session = self.sessions.get(sessid) if session: + # since some of the session properties may have had + # a chance to change already before the portal gets here + # the portal doesn't send all sessiondata here, but only + # ones which should only be changed from portal (like + # protocol_flags etc) session.load_sync_data(portalsessiondata) def portal_disconnect(self, sessid): From f6b9b7e1a99408e0fcd41d7585febb1d8c4c8d29 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 19 Oct 2014 19:40:42 +0200 Subject: [PATCH 06/11] Implemented a idstring for the tickerhandler, as per #554. --- src/scripts/tickerhandler.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/scripts/tickerhandler.py b/src/scripts/tickerhandler.py index 9dd7c33e56..b3b7e61e2f 100644 --- a/src/scripts/tickerhandler.py +++ b/src/scripts/tickerhandler.py @@ -195,7 +195,7 @@ class TickerHandler(object): self.save_name = save_name self.ticker_pool = self.ticker_pool_class() - def _store_key(self, obj, interval): + def _store_key(self, obj, interval, idstring=""): """ Tries to create a store_key for the object. Returns a tuple (isdb, store_key) where isdb @@ -224,7 +224,7 @@ class TickerHandler(object): objkey = id(obj) isdb = False # return sidb and store_key - return isdb, (objkey, interval) + return isdb, (objkey, interval, idstring) def save(self): """ @@ -237,7 +237,7 @@ class TickerHandler(object): start_delays = dict((interval, ticker.task.next_call_time()) for interval, ticker in self.ticker_pool.tickers.items()) # update the timers for the tickers - for (obj, interval), (args, kwargs) in self.ticker_storage.items(): + for (obj, interval, idstring), (args, kwargs) in self.ticker_storage.items(): kwargs["_start_delay"] = start_delays.get(interval, None) ServerConfig.objects.conf(key=self.save_name, @@ -254,30 +254,34 @@ class TickerHandler(object): if ticker_storage: self.ticker_storage = dbunserialize(ticker_storage) #print "restore:", self.ticker_storage - for (obj, interval), (args, kwargs) in self.ticker_storage.items(): + for store_key, (args, kwargs) in self.ticker_storage.items(): + if len(store_key) == 2: + # old form of store_key + store_key = (store_key[0], store_key[1], "") + obj, interval, idstring = store_key obj = unpack_dbobj(obj) - _, store_key = self._store_key(obj, interval) + _, store_key = self._store_key(obj, interval, idstring) self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) - def add(self, obj, interval, *args, **kwargs): + def add(self, obj, interval, idstring="", *args, **kwargs): """ Add object to tickerhandler. The object must have an at_tick method. This will be called every interval seconds until the object is unsubscribed from the ticker. """ - isdb, store_key = self._store_key(obj, interval) + isdb, store_key = self._store_key(obj, interval, idstring) if isdb: self.ticker_storage[store_key] = (args, kwargs) self.save() self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) - def remove(self, obj, interval=None): + def remove(self, obj, interval=None, idstring=""): """ Remove object from ticker, or only this object ticking at a given interval. """ if interval: - isdb, store_key = self._store_key(obj, interval) + isdb, store_key = self._store_key(obj, interval, idstring) if isdb: self.ticker_storage.pop(store_key, None) self.save() @@ -287,7 +291,7 @@ class TickerHandler(object): intervals = self.ticker_pool.tickers.keys() should_save = False for interval in intervals: - isdb, store_key = self._store_key(obj, interval) + isdb, store_key = self._store_key(obj, interval, idstring) if isdb: self.ticker_storage.pop(store_key, None) should_save = True From dd937cdcd3054a255a6dd02d7d8658fd0ffd0f53 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Oct 2014 06:07:15 +0200 Subject: [PATCH 07/11] Made new tickerhandler store_key more ubiquitous, in case of not stopping server before pulling latest. --- src/scripts/tickerhandler.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scripts/tickerhandler.py b/src/scripts/tickerhandler.py index b3b7e61e2f..cf3421a9ba 100644 --- a/src/scripts/tickerhandler.py +++ b/src/scripts/tickerhandler.py @@ -237,9 +237,11 @@ class TickerHandler(object): start_delays = dict((interval, ticker.task.next_call_time()) for interval, ticker in self.ticker_pool.tickers.items()) # update the timers for the tickers - for (obj, interval, idstring), (args, kwargs) in self.ticker_storage.items(): + #for (obj, interval, idstring), (args, kwargs) in self.ticker_storage.items(): + for store_key, (args, kwargs) in self.ticker_storage.items(): + interval = store_key[1] + # this is a mutable, so it's updated in-place in ticker_storage kwargs["_start_delay"] = start_delays.get(interval, None) - ServerConfig.objects.conf(key=self.save_name, value=dbserialize(self.ticker_storage)) else: @@ -256,7 +258,7 @@ class TickerHandler(object): #print "restore:", self.ticker_storage for store_key, (args, kwargs) in self.ticker_storage.items(): if len(store_key) == 2: - # old form of store_key + # old form of store_key - update it store_key = (store_key[0], store_key[1], "") obj, interval, idstring = store_key obj = unpack_dbobj(obj) From a5b4ddd1e960392eb10057703804751f8e727ae5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Oct 2014 19:55:06 +0200 Subject: [PATCH 08/11] Adding minor doc update. --- src/utils/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.py b/src/utils/utils.py index 7ea45d35f1..cef6cad9cd 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -778,7 +778,8 @@ def mod_import(module): def all_from_module(module): """ - Return all global-level variables from a module as a dict + Return all global-level variables from a module as a dict. + Ignores modules and variable names starting with an underscore. """ mod = mod_import(module) if not mod: From 3eb347a07675eb96dfb73da8fb89e4029da15354 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Oct 2014 22:14:58 +0200 Subject: [PATCH 09/11] Fixes a side effect of the #583 solution only showing up in certain connection timing combinations related to which protocols were supported in a given client. The portal<->server handhshake exchanged the session info an extra round depending on the internal timing of the connection. Also fixed a handshake bug in the MXP initialization. --- src/server/portal/mxp.py | 1 + src/server/portal/portalsessionhandler.py | 5 +++-- src/server/session.py | 2 +- src/server/sessionhandler.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/server/portal/mxp.py b/src/server/portal/mxp.py index 05d9958a05..56463ce473 100644 --- a/src/server/portal/mxp.py +++ b/src/server/portal/mxp.py @@ -51,6 +51,7 @@ class Mxp(object): Client does not support MXP. """ self.protocol.protocol_flags["MXP"] = False + self.protocol.handshake_done() def do_mxp(self, option): """ diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index 0d9b1e3b08..290dd04b4d 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -64,10 +64,11 @@ class PortalSessionHandler(SessionHandler): # only use if session already has sessid (i.e. has already connected) sessdata = session.get_sync_data() if self.portal.amp_protocol: - # we remove sessdata that could already have changed on the - # server level + # we only send sessdata that should not have changed + # at the server level at this point sessdata = dict((key, val) for key, val in sessdata.items() if key in ("protocol_key", "address", + "sessid", "suid", "conn_time", "protocol_flags", diff --git a/src/server/session.py b/src/server/session.py index a0e8c81ca4..2b2d499854 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -95,7 +95,7 @@ class Session(object): and loads it into the correct properties of the session. """ for propname, value in sessdata.items(): - self.__dict__[propname] = value + setattr(self, propname, value) def at_sync(self): """ diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index b4b17eff02..8d12482161 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -344,7 +344,7 @@ class ServerSessionHandler(SessionHandler): session.logged_in = True # sync the portal to the session - sessdata = session.get_sync_data() + sessdata = {"logged_in": True} if not testmode: self.server.amp_protocol.call_remote_PortalAdmin(session.sessid, operation=SLOGIN, From b8dfce9cefbc06db1cfc532d14abddcd0a45feed Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Oct 2014 22:38:06 +0200 Subject: [PATCH 10/11] Made also the prompt parsed for MXP tags and added html parsing for the prompt in the webclient. See #599. --- src/server/portal/telnet.py | 2 ++ src/server/portal/websocket_client.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index 436f34afa5..f02a0e3d7b 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -258,6 +258,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): if prompt: # Send prompt separately prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + "{n", strip_ansi=nomarkup, xterm256=xterm256) + if mxp: + prompt = mxp_parse(prompt) prompt = prompt.replace(IAC, IAC + IAC).replace('\n', '\r\n') prompt += IAC + GA self.transport.write(mccp_compress(self, prompt)) diff --git a/src/server/portal/websocket_client.py b/src/server/portal/websocket_client.py index d17702f471..1b5fe8656b 100644 --- a/src/server/portal/websocket_client.py +++ b/src/server/portal/websocket_client.py @@ -122,10 +122,10 @@ class WebSocketClient(Protocol, Session): oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob")) #print "oob data_out:", "OOB" + json.dumps(oobstruct) self.sendLine("OOB" + json.dumps(oobstruct)) - if "prompt" in kwargs: - self.sendLine("PROMPT" + kwargs["prompt"]) raw = kwargs.get("raw", False) nomarkup = kwargs.get("nomarkup", False) + if "prompt" in kwargs: + self.sendLine("PROMPT" + parse_html(kwargs["prompt"], strip_ansi=nomarkup)) if raw: self.sendLine(text) else: From 263092b4c939e8f08a3eef56b3c725a17b4323f9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 20 Oct 2014 23:40:01 +0200 Subject: [PATCH 11/11] Added TCP keepalive to telnet and webclient, to make sure to (eventually) catch dead connections. Should help #498, but should test on demo before closing. --- src/server/portal/telnet.py | 2 ++ src/server/portal/websocket_client.py | 2 ++ src/server/sessionhandler.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index f02a0e3d7b..1118dd9e2c 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -50,6 +50,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.msdp = msdp.Msdp(self) # mxp support self.mxp = Mxp(self) + # keepalive watches for dead links + self.transport.setTcpKeepAlive(1) # add this new connection to sessionhandler so # the Server becomes aware of it. self.sessionhandler.connect(self) diff --git a/src/server/portal/websocket_client.py b/src/server/portal/websocket_client.py index 1b5fe8656b..f8b14f6c5d 100644 --- a/src/server/portal/websocket_client.py +++ b/src/server/portal/websocket_client.py @@ -46,6 +46,8 @@ class WebSocketClient(Protocol, Session): """ client_address = self.transport.client self.init_session("websocket", client_address, self.factory.sessionhandler) + # watch for dead links + self.transport.setTcpKeepAlive(1) self.sessionhandler.connect(self) def disconnect(self, reason=None): diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 8d12482161..b47579b78b 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -412,7 +412,7 @@ class ServerSessionHandler(SessionHandler): def validate_sessions(self): """ Check all currently connected sessions (logged in and not) - and see if any are dead. + and see if any are dead or idle """ tcurr = time.time() reason = _("Idle timeout exceeded, disconnecting.")