33from signal import SIGTERM
44import sys
55import threading
6+ import time
67
78import pytest
89import pexpect
@@ -58,12 +59,42 @@ def __getattr__(self, name):
5859 return getattr (self .__wrapped , name )
5960
6061
62+ class _LockedSpawn :
63+ """
64+ pexpect/ptyprocess is not thread-safe.
65+ Serialize access to pexpect.spawn through a lock, so the reader thread and the main thread
66+ never call into pexpect (and thus os.waitpid()) concurrently.
67+ """
68+
69+ def __init__ (self , spawn ):
70+ self ._spawn = spawn
71+ self ._lock = threading .Lock ()
72+
73+ def __getattr__ (self , name ):
74+ with self ._lock :
75+ attr = getattr (self ._spawn , name )
76+ if callable (attr ):
77+ def locked (* args , ** kwargs ):
78+ with self ._lock :
79+ return attr (* args , ** kwargs )
80+ return locked
81+ return attr
82+
83+
6184class LabgridComponent :
6285 def __init__ (self , cwd ):
6386 self .cwd = str (cwd )
6487 self .spawn = None
6588 self .reader = None
6689
90+ @property
91+ def spawn (self ):
92+ return self ._spawn
93+
94+ @spawn .setter
95+ def spawn (self , spawn ):
96+ self ._spawn = _LockedSpawn (spawn ) if spawn is not None else None
97+
6798 def stop (self ):
6899 logging .info ("stopping %s pid=%s" , self .__class__ .__name__ , self .spawn .pid )
69100
@@ -83,16 +114,19 @@ def keep_reading(spawn):
83114 "The output from background processes must be read to avoid blocking them."
84115 while spawn .isalive ():
85116 try :
86- data = spawn .read_nonblocking (size = 1024 , timeout = 0.1 )
117+ data = spawn .read_nonblocking (size = 1024 , timeout = 0 )
87118 if not data :
88119 return
89120 except pexpect .TIMEOUT :
90- continue
121+ pass
91122 except pexpect .EOF :
92123 return
93124 except OSError :
94125 return
95126
127+ # give external LabgridComponent.spawn users a chance to acquire the lock
128+ time .sleep (0.001 )
129+
96130 def start_reader (self ):
97131 self .reader = threading .Thread (
98132 target = LabgridComponent .keep_reading ,
0 commit comments