Skip to content

Commit 4084c8e

Browse files
Merge pull request #1937 from Bastian-Krause/bst/conftest-fix-threaded-pexpect
tests/conftest: fix concurrent access to pexpect.spawn
2 parents 64723a5 + 66be067 commit 4084c8e

1 file changed

Lines changed: 36 additions & 2 deletions

File tree

tests/conftest.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from signal import SIGTERM
44
import sys
55
import threading
6+
import time
67

78
import pytest
89
import 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+
6184
class 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

Comments
 (0)