Skip to content

Commit 654827a

Browse files
committed
Issue #14252: Fix subprocess.Popen.terminate() to not raise an error under Windows when the child process has already exited.
1 parent 4a7a33e commit 654827a

4 files changed

Lines changed: 82 additions & 1 deletion

File tree

Lib/subprocess.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,17 @@ def send_signal(self, sig):
10161016
def terminate(self):
10171017
"""Terminates the process
10181018
"""
1019-
_subprocess.TerminateProcess(self._handle, 1)
1019+
try:
1020+
_subprocess.TerminateProcess(self._handle, 1)
1021+
except OSError as e:
1022+
# ERROR_ACCESS_DENIED (winerror 5) is received when the
1023+
# process already died.
1024+
if e.winerror != 5:
1025+
raise
1026+
rc = _subprocess.GetExitCodeProcess(self._handle)
1027+
if rc == _subprocess.STILL_ACTIVE:
1028+
raise
1029+
self.returncode = rc
10201030

10211031
kill = terminate
10221032

Lib/test/test_subprocess.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,27 @@ def _kill_process(self, method, *args):
812812
getattr(p, method)(*args)
813813
return p
814814

815+
def _kill_dead_process(self, method, *args):
816+
# Do not inherit file handles from the parent.
817+
# It should fix failures on some platforms.
818+
p = subprocess.Popen([sys.executable, "-c", """if 1:
819+
import sys, time
820+
sys.stdout.write('x\\n')
821+
sys.stdout.flush()
822+
"""],
823+
close_fds=True,
824+
stdin=subprocess.PIPE,
825+
stdout=subprocess.PIPE,
826+
stderr=subprocess.PIPE)
827+
# Wait for the interpreter to be completely initialized before
828+
# sending any signal.
829+
p.stdout.read(1)
830+
# The process should end after this
831+
time.sleep(1)
832+
# This shouldn't raise even though the child is now dead
833+
getattr(p, method)(*args)
834+
p.communicate()
835+
815836
def test_send_signal(self):
816837
p = self._kill_process('send_signal', signal.SIGINT)
817838
_, stderr = p.communicate()
@@ -830,6 +851,18 @@ def test_terminate(self):
830851
self.assertStderrEqual(stderr, '')
831852
self.assertEqual(p.wait(), -signal.SIGTERM)
832853

854+
def test_send_signal_dead(self):
855+
# Sending a signal to a dead process
856+
self._kill_dead_process('send_signal', signal.SIGINT)
857+
858+
def test_kill_dead(self):
859+
# Killing a dead process
860+
self._kill_dead_process('kill')
861+
862+
def test_terminate_dead(self):
863+
# Terminating a dead process
864+
self._kill_dead_process('terminate')
865+
833866
def check_close_std_fds(self, fds):
834867
# Issue #9905: test that subprocess pipes still work properly with
835868
# some standard fds closed
@@ -1126,6 +1159,31 @@ def _kill_process(self, method, *args):
11261159
returncode = p.wait()
11271160
self.assertNotEqual(returncode, 0)
11281161

1162+
def _kill_dead_process(self, method, *args):
1163+
p = subprocess.Popen([sys.executable, "-c", """if 1:
1164+
import sys, time
1165+
sys.stdout.write('x\\n')
1166+
sys.stdout.flush()
1167+
sys.exit(42)
1168+
"""],
1169+
stdin=subprocess.PIPE,
1170+
stdout=subprocess.PIPE,
1171+
stderr=subprocess.PIPE)
1172+
self.addCleanup(p.stdout.close)
1173+
self.addCleanup(p.stderr.close)
1174+
self.addCleanup(p.stdin.close)
1175+
# Wait for the interpreter to be completely initialized before
1176+
# sending any signal.
1177+
p.stdout.read(1)
1178+
# The process should end after this
1179+
time.sleep(1)
1180+
# This shouldn't raise even though the child is now dead
1181+
getattr(p, method)(*args)
1182+
_, stderr = p.communicate()
1183+
self.assertStderrEqual(stderr, b'')
1184+
rc = p.wait()
1185+
self.assertEqual(rc, 42)
1186+
11291187
def test_send_signal(self):
11301188
self._kill_process('send_signal', signal.SIGTERM)
11311189

@@ -1135,6 +1193,15 @@ def test_kill(self):
11351193
def test_terminate(self):
11361194
self._kill_process('terminate')
11371195

1196+
def test_send_signal_dead(self):
1197+
self._kill_dead_process('send_signal', signal.SIGTERM)
1198+
1199+
def test_kill_dead(self):
1200+
self._kill_dead_process('kill')
1201+
1202+
def test_terminate_dead(self):
1203+
self._kill_dead_process('terminate')
1204+
11381205

11391206
@unittest.skipUnless(getattr(subprocess, '_has_poll', False),
11401207
"poll system call not supported")

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Core and Builtins
1818
Library
1919
-------
2020

21+
- Issue #14252: Fix subprocess.Popen.terminate() to not raise an error under
22+
Windows when the child process has already exited.
23+
2124
- Issue #14195: An issue that caused weakref.WeakSet instances to incorrectly
2225
return True for a WeakSet instance 'a' in both 'a < a' and 'a > a' has been
2326
fixed.

PC/_subprocess.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,4 +670,5 @@ init_subprocess()
670670
defint(d, "WAIT_OBJECT_0", WAIT_OBJECT_0);
671671
defint(d, "CREATE_NEW_CONSOLE", CREATE_NEW_CONSOLE);
672672
defint(d, "CREATE_NEW_PROCESS_GROUP", CREATE_NEW_PROCESS_GROUP);
673+
defint(d, "STILL_ACTIVE", STILL_ACTIVE);
673674
}

0 commit comments

Comments
 (0)