Skip to content

Commit ae77274

Browse files
committed
mitogen: _first_stage: Add timeout handling
Do not wait/block forever for data to be read. Add a test for this. The test can be run using the following command: PYTHONPATH=$(pwd)/tests:$PYTHONPATH python -m unittest -v tests.first_stage_test Signed-off-by: Marc Hartmayer <[email protected]>
1 parent aa7d95b commit ae77274

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

mitogen/parent.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,9 @@ class Connection(object):
13781378
#: user.
13791379
exception = None
13801380

1381+
#: First stage timeout in seconds,
1382+
_first_stage_timeout = 10
1383+
13811384
#: Extra text appended to :class:`EofError` if that exception is raised on
13821385
#: a failed connection attempt. May be used in subclasses to hint at common
13831386
#: problems with a particular connection method.
@@ -1418,6 +1421,9 @@ def __repr__(self):
14181421
# C: the decompressed core source.
14191422
# n: size of the compressed core source to be read
14201423
# V: data chunk
1424+
# rl: list of FDs ready for reading
1425+
# t: timeout value in seconds
1426+
# _: throw away variable
14211427

14221428
# Final os.close(STDOUT_FILENO) to avoid --py-debug build corrupting stream with
14231429
# "[1234 refs]" during exit.
@@ -1439,8 +1445,16 @@ def _first_stage():
14391445
os.environ['ARGV0']=sys.executable
14401446
os.execl(sys.executable,sys.executable+'(mitogen:%s)'%sys.argv[2])
14411447
os.write(1,'MITO000\n'.encode())
1442-
n=int(sys.argv[3]);C=''.encode();V='V'
1443-
while n>len(C) and V:select.select([0],[],[]);V=os.read(0,n-len(C));C+=V
1448+
n=int(sys.argv[3])
1449+
t=float(sys.argv[4])
1450+
C=''.encode()
1451+
V='V'
1452+
while n>len(C) and V:
1453+
rl,_,_=select.select([0],[],[],t)
1454+
if not rl:
1455+
raise Exception("TimeoutError")
1456+
V=os.read(0,n-len(C))
1457+
C+=V
14441458
C=zlib.decompress(C)
14451459
f=os.fdopen(W,'wb',0)
14461460
f.write(C)
@@ -1486,6 +1500,7 @@ def get_boot_command(self):
14861500
encoded.decode(),
14871501
self.options.remote_name,
14881502
str(len(self.get_preamble())),
1503+
str(self._first_stage_timeout),
14891504
]
14901505

14911506
def get_econtext_config(self):

tests/first_stage_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import subprocess
2+
import sys
23

34
import mitogen.core
45
import mitogen.parent
@@ -50,3 +51,36 @@ def test_valid_syntax(self):
5051
)
5152
finally:
5253
fp.close()
54+
55+
def test_timeout_error(self):
56+
options = mitogen.parent.Options(max_message_size=123)
57+
conn = mitogen.parent.Connection(options, self.router)
58+
conn.context = mitogen.core.Context(None, 123)
59+
# We do not want to wait the default of 10s, change it to 0.1s
60+
conn._first_stage_timeout = 0.1
61+
args = conn.get_boot_command()
62+
63+
# The boot command should write an ECO marker to stdout, read the
64+
# preamble from stdin, then execute it.
65+
66+
# This test attaches closes stdin to create a specific failure
67+
# 1. Fork child tries to read from STDIN, but fails as it is closed
68+
# 2. Fork child raises TimeoutError
69+
# 3. Fork child's file descriptors (write pipes) are closed by the OS
70+
# 4. Fork parent does `dup(<read pipe>, <stdin>)` and `exec(<python>)`
71+
# 5. Python reads `b''` (i.e. EOF) from stdin (a closed pipe)
72+
# 6. Python runs `''` (a valid script) and exits with success
73+
74+
proc = mitogen.parent.popen(args=args,
75+
stdout=subprocess.PIPE,
76+
stderr=subprocess.PIPE,
77+
preexec_fn=sys.stdin.close
78+
)
79+
stdout, stderr = proc.communicate()
80+
self.assertEqual(0, proc.returncode)
81+
self.assertEqual(stdout,
82+
mitogen.parent.BootstrapProtocol.EC0_MARKER+b('\n'))
83+
self.assertIn(
84+
b("TimeoutError"),
85+
stderr,
86+
)

0 commit comments

Comments
 (0)