diff --git a/scapy/automaton.py b/scapy/automaton.py index f26370b102f..f29e4d7857d 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -11,6 +11,7 @@ - add documentation for ioevent, as_supersocket... """ +import copy import ctypes import itertools import logging @@ -1308,6 +1309,23 @@ def _do_control(self, ready, *args, **kargs): k.update(kargs) self.parse_args(*a, **k) + # The timers are registered on the class by the @ATMT.timeout and + # @ATMT.timer decorators, but their runtime state (_time, _expired, + # _just_expired) is per-run bookkeeping. Left shared on the class, + # concurrently running instances of the same automaton (Automaton.spawn, + # ioevents used as supersockets) race for the one-shot expiry flags: + # whichever control thread decrements a timer first consumes the + # expiration, and the other instances never observe the timeout and + # block forever in select(). Copy the timers per started instance, + # after parse_args() so pre-start reconfiguration through + # timer_by_name() (which mutates the class-level timers) is honored. + self.timeout = {} # type: Dict[str, _TimerList] + for state_name, timers in type(self).timeout.items(): + timer_list = _TimerList() + for t in timers: + timer_list.add_timer(copy.copy(t)) + self.timeout[state_name] = timer_list + # Start the automaton self.state = self.initial_states[0](self) self.send_sock = self.sock or self.send_sock_class(**self.socket_kargs) diff --git a/test/scapy/automaton.uts b/test/scapy/automaton.uts index 6aec9b0bbac..d71c2820de3 100644 --- a/test/scapy/automaton.uts +++ b/test/scapy/automaton.uts @@ -386,6 +386,42 @@ assert sm.timer_by_name("goto_end")._timeout == 1 assert sm.count1 == 10 assert sm.count2 == 6 +~ timers are per-instance when several instances run concurrently + +import time + + +class TimerRaceTest(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + self.fired = False + + @ATMT.timeout(BEGIN, 0.2) + def goto_end(self): + self.fired = True + raise self.END() + + @ATMT.state(final=1) + def END(self): + pass + + +a = TimerRaceTest(ll=lambda: None, recvsock=lambda: None) +b = TimerRaceTest(ll=lambda: None, recvsock=lambda: None) +assert a.timer_by_name("goto_end") is not None +assert b.timer_by_name("goto_end") is not None + +a.runbg() +b.runbg() +deadline = time.time() + 10 +while time.time() < deadline and (a.isrunning() or b.isrunning()): + time.sleep(0.05) +assert not a.isrunning(), "instance a never observed its timeout" +assert not b.isrunning(), "instance b never observed its timeout" +assert a.fired and b.fired +a.stop() +b.stop() + ~ reconfigure timers sm = TimerTest(ll=lambda: None, recvsock=lambda: None)