diff --git a/evennia/contrib/slow_exit.py b/evennia/contrib/slow_exit.py index 107d274b8c..3b060f7471 100644 --- a/evennia/contrib/slow_exit.py +++ b/evennia/contrib/slow_exit.py @@ -36,6 +36,7 @@ TickerHandler might be better. """ from evennia import DefaultExit, utils, Command +from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER MOVE_DELAY = {"stroll": 6, "walk": 4, "run": 2, "sprint": 1} @@ -70,11 +71,11 @@ class SlowExit(DefaultExit): traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed)) # create a delayed movement - deferred = utils.delay(move_delay, move_callback) + task_id = utils.delay(move_delay, move_callback) # we store the deferred on the character, this will allow us # to abort the movement. We must use an ndb here since # deferreds cannot be pickled. - traversing_object.ndb.currently_moving = deferred + traversing_object.ndb.currently_moving = _TASK_HANDLER.get_deferred(task_id) # diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index a4e50a29f7..7b69d07cc9 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1152,13 +1152,7 @@ from evennia.contrib import slow_exit slow_exit.MOVE_DELAY = {"stroll": 0, "walk": 0, "run": 0, "sprint": 0} -def _cancellable_mockdelay(time, callback, *args, **kwargs): - callback(*args, **kwargs) - return Mock() - - class TestSlowExit(CommandTest): - @patch("evennia.utils.delay", _cancellable_mockdelay) def test_exit(self): exi = create_object( slow_exit.SlowExit, key="slowexit", location=self.room1, destination=self.room2 diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index ca4147a715..759d1f1dc0 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -24,6 +24,7 @@ import random from evennia import DefaultObject, DefaultExit, Command, CmdSet from evennia.utils import search, delay, dedent from evennia.prototypes.spawner import spawn +from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER # ------------------------------------------------------------- # @@ -389,7 +390,8 @@ class LightSource(TutorialObject): # start the burn timer. When it runs out, self._burnout # will be called. We store the deferred so it can be # killed in unittesting. - self.deferred = delay(60 * 3, self._burnout) + task_id = delay(60 * 3, self._burnout) + self.deferred = _TASK_HANDLER.get_deferred(task_id) return True @@ -687,7 +689,8 @@ class CrumblingWall(TutorialObject, DefaultExit): self.db.exit_open = True # start a 45 second timer before closing again. We store the deferred so it can be # killed in unittesting. - self.deferred = delay(45, self.reset) + task_id = delay(45, self.reset) + self.deferred = _TASK_HANDLER.get_deferred(task_id) return True def _translate_position(self, root, ipos): diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 3273e27e8a..b865a74185 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -44,7 +44,6 @@ class TaskHandler(object): Dev notes: deferLater creates an instance of IDelayedCall using reactor.callLater. deferLater uses the cancel method on the IDelayedCall instance to create - the defer instance it returns. """ @@ -79,16 +78,18 @@ class TaskHandler(object): continue callback = getattr(obj, method) - self.tasks[task_id] = (date, callback, args, kwargs) + self.tasks[task_id] = date, callback, args, kwargs, True, None if to_save: self.save() def save(self): """Save the tasks in ServerConfig.""" - for task_id, (date, callback, args, kwargs) in self.tasks.items(): + for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): if task_id in self.to_save: continue + if not persistent: + continue if getattr(callback, "__self__", None): # `callback` is an instance method @@ -127,8 +128,8 @@ class TaskHandler(object): any (any): additional keyword arguments to send to the callback Returns: - twisted.internet.defer.Deferred instance of the deferred task task_id (int), the task's id intended for use with this class. + False, if the task has completed before addition finishes. Notes: This method has two return types. @@ -144,19 +145,23 @@ class TaskHandler(object): As those memory references will no longer acurately point to the variable desired. """ + # set the completion time + # Only used on persistent tasks after a restart + now = datetime.now() + delta = timedelta(seconds=timedelay) + comp_time = now + delta + # get an open task id + used_ids = list(self.tasks.keys()) + task_id = 1 + while task_id in used_ids: + task_id += 1 + + # record the task to the tasks dictionary persistent = kwargs.get("persistent", False) if persistent: del kwargs["persistent"] - now = datetime.now() - delta = timedelta(seconds=timedelay) - - # Choose a free task_id safe_args = [] safe_kwargs = {} - used_ids = list(self.tasks.keys()) - task_id = 1 - while task_id in used_ids: - task_id += 1 # Check that args and kwargs contain picklable information for arg in args: @@ -183,17 +188,28 @@ class TaskHandler(object): else: safe_kwargs[key] = value - self.tasks[task_id] = (now + delta, callback, safe_args, safe_kwargs) + self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, True, None) self.save() - callback = self.do_task - args = [task_id] - kwargs = {} - deferLater(self.clock, timedelay, callback, *args, **kwargs) - return task_id + else: # this is a non-persitent task + self.tasks[task_id] = (comp_time, callback, args, kwargs, True, None) + # defer the task + callback = self.do_task + args = [task_id] + kwargs = {} d = deferLater(self.clock, timedelay, callback, *args, **kwargs) d.addErrback(handle_error) - return d + + # some tasks may complete before the deferal can be added + if task_id in self.tasks: + task = self.tasks.get(task_id) + task = list(task) + task[4] = persistent + task[5] = d + self.tasks[task_id] = task + else: # the task already completed + return False + return task_id def remove(self, task_id): """Remove a persistent task without executing it. @@ -206,7 +222,8 @@ class TaskHandler(object): in the TaskHandler. """ - del self.tasks[task_id] + if task_id in self.tasks: + del self.tasks[task_id] if task_id in self.to_save: del self.to_save[task_id] @@ -222,13 +239,30 @@ class TaskHandler(object): This will also remove it from the list of current tasks. """ - date, callback, args, kwargs = self.tasks.pop(task_id) + date, callback, args, kwargs, persistent, d = self.tasks.pop(task_id) + if task_id in self.to_save: del self.to_save[task_id] self.save() callback(*args, **kwargs) + def get_deferred(self, task_id): + """ + Return the instance of the deferred the task id is using. + + Args: + task_id (int): a valid task ID. + + Returns: + An instance of a deferral or False if there is no task with the id. + None is returned if there is no deferral affiliated with this id. + """ + if task_id in self.tasks: + return self.tasks[task_id][5] + else: + return False + def create_delays(self): """Create the delayed tasks for the persistent tasks. @@ -237,9 +271,14 @@ class TaskHandler(object): """ now = datetime.now() - for task_id, (date, callbac, args, kwargs) in self.tasks.items(): + for task_id, (date, callbac, args, kwargs, _, _) in self.tasks.items(): + self.tasks[task_id] = date, callbac, args, kwargs, True, None seconds = max(0, (date - now).total_seconds()) - deferLater(self.clock, seconds, self.do_task, task_id) + d = deferLater(self.clock, seconds, self.do_task, task_id) + d.addErrback(handle_error) + # some tasks may complete before the deferal can be added + if self.tasks.get(task_id, False): + self.tasks[task_id] = date, callbac, args, kwargs, True, d # Create the soft singleton diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 821e96aa24..42facf7e65 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -31,6 +31,7 @@ from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP from evennia.server.portal.mxp import Mxp, mxp_parse from evennia.utils import ansi from evennia.utils.utils import to_bytes +from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _RE_N = re.compile(r"\|n$") _RE_LEND = re.compile(br"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) @@ -127,8 +128,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): from evennia.utils.utils import delay - # timeout the handshakes in case the client doesn't reply at all - self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True) + task_id = delay(2, callback=self.handshake_done, timeout=True) + self._handshake_delay = _TASK_HANDLER.get_deferred(task_id) # TCP/IP keepalive watches for dead links self.transport.setTcpKeepAlive(1) diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index bebea149ce..ae4d49e90f 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -323,24 +323,31 @@ class TestDelay(EvenniaTest): def test_delay(self): # get a reference of TASK_HANDLER + timedelay = 5 global _TASK_HANDLER if _TASK_HANDLER is None: from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _TASK_HANDLER.clock = task.Clock() self.char1.ndb.dummy_var = False # test a persistent deferral, that completes after delay time - task_id = utils.delay(1, dummy_func, self.char1.dbref, persistent=True) - _TASK_HANDLER.clock.advance(1) # make time pass + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non persisten deferral, that completes after delay time. - deferal_inst = utils.delay(1, dummy_func, self.char1.dbref) - _TASK_HANDLER.clock.advance(1) # make time pass + utils.delay(timedelay, dummy_func, self.char1.dbref) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False + # test a non persisten deferral, with a short timedelay + utils.delay(.1, dummy_func, self.char1.dbref) + _TASK_HANDLER.clock.advance(.1) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test canceling a deferral. - deferal_inst = utils.delay(1, dummy_func, self.char1.dbref) + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + deferal_inst = _TASK_HANDLER.get_deferred(task_id) deferal_inst.cancel() - _TASK_HANDLER.clock.advance(1) # make time pass + _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) self.char1.ndb.dummy_var = False