Skip to content

Commit b50c371

Browse files
committed
Fix subprocess.Popen.wait to raise ProcessLookupError for external waits
1 parent 8665769 commit b50c371

File tree

4 files changed

+37
-5
lines changed

4 files changed

+37
-5
lines changed

Lib/subprocess.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,11 +2020,17 @@ def _try_wait(self, wait_flags):
20202020
try:
20212021
(pid, sts) = os.waitpid(self.pid, wait_flags)
20222022
except ChildProcessError:
2023-
# This happens if SIGCLD is set to be ignored or waiting
2023+
# This happens if SIGCHLD is set to be ignored or waiting
20242024
# for child processes has otherwise been disabled for our
2025-
# process. This child is dead, we can't get the status.
2026-
pid = self.pid
2027-
sts = 0
2025+
# process. This child is dead, we can't get the status.
2026+
if signal.getsignal(signal.SIGCHLD) == signal.SIG_IGN:
2027+
# SIGCHLD was ignored; for compatibility return a zero
2028+
# status for this child.
2029+
pid = self.pid
2030+
sts = 0
2031+
return (pid, sts)
2032+
2033+
raise ProcessLookupError(f'Process {self.pid} is already waited on externally')
20282034
return (pid, sts)
20292035

20302036

Lib/test/test_asyncio/test_subprocess.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,11 @@ async def kill_running():
607607

608608
# kill the process (but asyncio is not notified immediately)
609609
proc.kill()
610-
proc.wait()
610+
try:
611+
proc.wait()
612+
except ProcessLookupError:
613+
# Process already exited, which is expected
614+
pass
611615

612616
proc.kill = mock.Mock()
613617
proc_returncode = proc.poll()

Lib/test/test_subprocess.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3361,6 +3361,25 @@ def test_stopped(self):
33613361

33623362
self.assertEqual(returncode, -3)
33633363

3364+
def test_wait_after_external_wait(self):
3365+
"""Test behavior when process is already waited on externally; issue96863."""
3366+
# Create a process with non-zero exit code
3367+
proc = subprocess.Popen([sys.executable, '-c', 'import sys; sys.exit(1)'])
3368+
3369+
# Wait for the process externally using os.wait()
3370+
pid, status = os.wait()
3371+
self.assertEqual(pid, proc.pid)
3372+
self.assertEqual(os.WEXITSTATUS(status), 1)
3373+
3374+
# Now calling proc.wait() should raise ProcessLookupError
3375+
# instead of returning 0 (the old buggy behavior)
3376+
with self.assertRaises(ProcessLookupError) as cm:
3377+
proc.wait()
3378+
3379+
# Verify the exception contains proper information
3380+
exc = cm.exception
3381+
self.assertIn('%d is already waited on externally' % proc.pid, str(exc))
3382+
33643383
def test_send_signal_race(self):
33653384
# bpo-38630: send_signal() must poll the process exit status to reduce
33663385
# the risk of sending the signal to the wrong process.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :meth:`subprocess.Popen.wait` returning zero always when process already
2+
waited on externally. Now raises :exc:`ProcessLookupError` instead of losing
3+
exit status. Preserves ``SIGCHLD=SIG_IGN`` compatibility.

0 commit comments

Comments
 (0)