Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dfd5f34

Browse files
authored
Fix bpo-30589: improve Process.exitcode with forkserver (#1989)
* Fix bpo-30589: improve Process.exitcode with forkserver When the child is killed, Process.exitcode should return -signum, not 255. * Add Misc/NEWS
1 parent ced36a9 commit dfd5f34

5 files changed

Lines changed: 138 additions & 49 deletions

File tree

Lib/multiprocessing/forkserver.py

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import struct
77
import sys
88
import threading
9+
import warnings
910

1011
from . import connection
1112
from . import process
@@ -22,7 +23,7 @@
2223
#
2324

2425
MAXFDS_TO_SEND = 256
25-
UNSIGNED_STRUCT = struct.Struct('Q') # large enough for pid_t
26+
SIGNED_STRUCT = struct.Struct('q') # large enough for pid_t
2627

2728
#
2829
# Forkserver class
@@ -148,21 +149,33 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None):
148149

149150
util._close_stdin()
150151

151-
# ignoring SIGCHLD means no need to reap zombie processes;
152+
sig_r, sig_w = os.pipe()
153+
os.set_blocking(sig_w, False)
154+
155+
def sigchld_handler(*_unused):
156+
try:
157+
os.write(sig_w, b'.')
158+
except BlockingIOError:
159+
pass
160+
152161
# letting SIGINT through avoids KeyboardInterrupt tracebacks
153162
handlers = {
154-
signal.SIGCHLD: signal.SIG_IGN,
163+
signal.SIGCHLD: sigchld_handler,
155164
signal.SIGINT: signal.SIG_DFL,
156165
}
157166
old_handlers = {sig: signal.signal(sig, val)
158167
for (sig, val) in handlers.items()}
159168

169+
# map child pids to client fds
170+
pid_to_fd = {}
171+
160172
with socket.socket(socket.AF_UNIX, fileno=listener_fd) as listener, \
161173
selectors.DefaultSelector() as selector:
162174
_forkserver._forkserver_address = listener.getsockname()
163175

164176
selector.register(listener, selectors.EVENT_READ)
165177
selector.register(alive_r, selectors.EVENT_READ)
178+
selector.register(sig_r, selectors.EVENT_READ)
166179

167180
while True:
168181
try:
@@ -176,62 +189,100 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None):
176189
assert os.read(alive_r, 1) == b''
177190
raise SystemExit
178191

179-
assert listener in rfds
180-
with listener.accept()[0] as s:
181-
code = 1
182-
if os.fork() == 0:
192+
if sig_r in rfds:
193+
# Got SIGCHLD
194+
os.read(sig_r, 65536) # exhaust
195+
while True:
196+
# Scan for child processes
183197
try:
184-
_serve_one(s, listener, alive_r, old_handlers)
185-
except Exception:
186-
sys.excepthook(*sys.exc_info())
187-
sys.stderr.flush()
188-
finally:
189-
os._exit(code)
198+
pid, sts = os.waitpid(-1, os.WNOHANG)
199+
except ChildProcessError:
200+
break
201+
if pid == 0:
202+
break
203+
child_w = pid_to_fd.pop(pid, None)
204+
if child_w is not None:
205+
if os.WIFSIGNALED(sts):
206+
returncode = -os.WTERMSIG(sts)
207+
else:
208+
assert os.WIFEXITED(sts)
209+
returncode = os.WEXITSTATUS(sts)
210+
# Write the exit code to the pipe
211+
write_signed(child_w, returncode)
212+
os.close(child_w)
213+
else:
214+
# This shouldn't happen really
215+
warnings.warn('forkserver: waitpid returned '
216+
'unexpected pid %d' % pid)
217+
218+
if listener in rfds:
219+
# Incoming fork request
220+
with listener.accept()[0] as s:
221+
# Receive fds from client
222+
fds = reduction.recvfds(s, MAXFDS_TO_SEND + 1)
223+
assert len(fds) <= MAXFDS_TO_SEND
224+
child_r, child_w, *fds = fds
225+
s.close()
226+
pid = os.fork()
227+
if pid == 0:
228+
# Child
229+
code = 1
230+
try:
231+
listener.close()
232+
code = _serve_one(child_r, fds,
233+
(alive_r, child_w, sig_r, sig_w),
234+
old_handlers)
235+
except Exception:
236+
sys.excepthook(*sys.exc_info())
237+
sys.stderr.flush()
238+
finally:
239+
os._exit(code)
240+
else:
241+
# Send pid to client processes
242+
write_signed(child_w, pid)
243+
pid_to_fd[pid] = child_w
244+
os.close(child_r)
245+
for fd in fds:
246+
os.close(fd)
190247

191248
except OSError as e:
192249
if e.errno != errno.ECONNABORTED:
193250
raise
194251

195-
def _serve_one(s, listener, alive_r, handlers):
252+
253+
def _serve_one(child_r, fds, unused_fds, handlers):
196254
# close unnecessary stuff and reset signal handlers
197-
listener.close()
198-
os.close(alive_r)
199255
for sig, val in handlers.items():
200256
signal.signal(sig, val)
257+
for fd in unused_fds:
258+
os.close(fd)
201259

202-
# receive fds from parent process
203-
fds = reduction.recvfds(s, MAXFDS_TO_SEND + 1)
204-
s.close()
205-
assert len(fds) <= MAXFDS_TO_SEND
206-
(child_r, child_w, _forkserver._forkserver_alive_fd,
207-
stfd, *_forkserver._inherited_fds) = fds
208-
semaphore_tracker._semaphore_tracker._fd = stfd
209-
210-
# send pid to client processes
211-
write_unsigned(child_w, os.getpid())
260+
(_forkserver._forkserver_alive_fd,
261+
semaphore_tracker._semaphore_tracker._fd,
262+
*_forkserver._inherited_fds) = fds
212263

213-
# run process object received over pipe
264+
# Run process object received over pipe
214265
code = spawn._main(child_r)
215266

216-
# write the exit code to the pipe
217-
write_unsigned(child_w, code)
267+
return code
268+
218269

219270
#
220-
# Read and write unsigned numbers
271+
# Read and write signed numbers
221272
#
222273

223-
def read_unsigned(fd):
274+
def read_signed(fd):
224275
data = b''
225-
length = UNSIGNED_STRUCT.size
276+
length = SIGNED_STRUCT.size
226277
while len(data) < length:
227278
s = os.read(fd, length - len(data))
228279
if not s:
229280
raise EOFError('unexpected EOF')
230281
data += s
231-
return UNSIGNED_STRUCT.unpack(data)[0]
282+
return SIGNED_STRUCT.unpack(data)[0]
232283

233-
def write_unsigned(fd, n):
234-
msg = UNSIGNED_STRUCT.pack(n)
284+
def write_signed(fd, n):
285+
msg = SIGNED_STRUCT.pack(n)
235286
while msg:
236287
nbytes = os.write(fd, msg)
237288
if nbytes == 0:

Lib/multiprocessing/popen_fork.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,12 @@ def duplicate_for_child(self, fd):
2424

2525
def poll(self, flag=os.WNOHANG):
2626
if self.returncode is None:
27-
while True:
28-
try:
29-
pid, sts = os.waitpid(self.pid, flag)
30-
except OSError as e:
31-
# Child process not yet created. See #1731717
32-
# e.errno == errno.ECHILD == 10
33-
return None
34-
else:
35-
break
27+
try:
28+
pid, sts = os.waitpid(self.pid, flag)
29+
except OSError as e:
30+
# Child process not yet created. See #1731717
31+
# e.errno == errno.ECHILD == 10
32+
return None
3633
if pid == self.pid:
3734
if os.WIFSIGNALED(sts):
3835
self.returncode = -os.WTERMSIG(sts)

Lib/multiprocessing/popen_forkserver.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def _launch(self, process_obj):
5252
util.Finalize(self, os.close, (self.sentinel,))
5353
with open(w, 'wb', closefd=True) as f:
5454
f.write(buf.getbuffer())
55-
self.pid = forkserver.read_unsigned(self.sentinel)
55+
self.pid = forkserver.read_signed(self.sentinel)
5656

5757
def poll(self, flag=os.WNOHANG):
5858
if self.returncode is None:
@@ -61,8 +61,10 @@ def poll(self, flag=os.WNOHANG):
6161
if not wait([self.sentinel], timeout):
6262
return None
6363
try:
64-
self.returncode = forkserver.read_unsigned(self.sentinel)
64+
self.returncode = forkserver.read_signed(self.sentinel)
6565
except (OSError, EOFError):
66-
# The process ended abnormally perhaps because of a signal
66+
# This should not happen usually, but perhaps the forkserver
67+
# process itself got killed
6768
self.returncode = 255
69+
6870
return self.returncode

Lib/test/_test_multiprocessing.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ def test_process(self):
274274
def _test_terminate(cls):
275275
time.sleep(100)
276276

277+
@classmethod
278+
def _test_sleep(cls, delay):
279+
time.sleep(delay)
280+
277281
def test_terminate(self):
278282
if self.TYPE == 'threads':
279283
self.skipTest('test not appropriate for {}'.format(self.TYPE))
@@ -323,8 +327,9 @@ def handler(*args):
323327

324328
p.join()
325329

326-
# XXX sometimes get p.exitcode == 0 on Windows ...
327-
#self.assertEqual(p.exitcode, -signal.SIGTERM)
330+
# sometimes get p.exitcode == 0 on Windows ...
331+
if os.name != 'nt':
332+
self.assertEqual(p.exitcode, -signal.SIGTERM)
328333

329334
def test_cpu_count(self):
330335
try:
@@ -398,6 +403,36 @@ def test_sentinel(self):
398403
p.join()
399404
self.assertTrue(wait_for_handle(sentinel, timeout=1))
400405

406+
def test_many_processes(self):
407+
if self.TYPE == 'threads':
408+
self.skipTest('test not appropriate for {}'.format(self.TYPE))
409+
410+
sm = multiprocessing.get_start_method()
411+
N = 5 if sm == 'spawn' else 100
412+
413+
# Try to overwhelm the forkserver loop with events
414+
procs = [self.Process(target=self._test_sleep, args=(0.01,))
415+
for i in range(N)]
416+
for p in procs:
417+
p.start()
418+
for p in procs:
419+
p.join(timeout=10)
420+
for p in procs:
421+
self.assertEqual(p.exitcode, 0)
422+
423+
procs = [self.Process(target=self._test_terminate)
424+
for i in range(N)]
425+
for p in procs:
426+
p.start()
427+
time.sleep(0.001) # let the children start...
428+
for p in procs:
429+
p.terminate()
430+
for p in procs:
431+
p.join(timeout=10)
432+
if os.name != 'nt':
433+
for p in procs:
434+
self.assertEqual(p.exitcode, -signal.SIGTERM)
435+
401436
#
402437
#
403438
#

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ Extension Modules
362362
Library
363363
-------
364364

365+
- bpo-30589: Fix multiprocessing.Process.exitcode to return the opposite
366+
of the signal number when the process is killed by a signal (instead
367+
of 255) when using the "forkserver" method.
368+
365369
- bpo-28994: The traceback no longer displayed for SystemExit raised in
366370
a callback registered by atexit.
367371

0 commit comments

Comments
 (0)