Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions scapy/automaton.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- add documentation for ioevent, as_supersocket...
"""

import copy
import ctypes
import itertools
import logging
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions test/scapy/automaton.uts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading