Skip to content

Commit 1e2707d

Browse files
miss-islingtonefiop
authored andcommitted
bpo-37380: subprocess: don't use _active on win (GH-14360) (GH-15706)
As noted by @eryksun in [1] and [2], using _cleanup and _active(in __del__) is not necessary on Windows, since: > Unlike Unix, a process in Windows doesn't have to be waited on by > its parent to avoid a zombie. Keeping the handle open will actually > create a zombie until the next _cleanup() call, which may be never > if Popen() isn't called again. This patch simply defines `subprocess._active` as `None`, for which we already have the proper logic in place in `subprocess.Popen.__del__`, that prevents it from trying to append the process to the `_active`. This patch also defines `subprocess._cleanup` as a noop for Windows. [1] https://bugs.python.org/issue37380GH-msg346333 [2] https://bugs.python.org/issue36067GH-msg336262 Signed-off-by: Ruslan Kuprieiev <[email protected]> (cherry picked from commit 042821a) Co-authored-by: Ruslan Kuprieiev <[email protected]>
1 parent 256f03b commit 1e2707d

File tree

3 files changed

+59
-25
lines changed

3 files changed

+59
-25
lines changed

Lib/subprocess.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -217,22 +217,38 @@ def __repr__(self):
217217
__str__ = __repr__
218218

219219

220-
# This lists holds Popen instances for which the underlying process had not
221-
# exited at the time its __del__ method got called: those processes are wait()ed
222-
# for synchronously from _cleanup() when a new Popen object is created, to avoid
223-
# zombie processes.
224-
_active = []
225-
226-
def _cleanup():
227-
for inst in _active[:]:
228-
res = inst._internal_poll(_deadstate=sys.maxsize)
229-
if res is not None:
230-
try:
231-
_active.remove(inst)
232-
except ValueError:
233-
# This can happen if two threads create a new Popen instance.
234-
# It's harmless that it was already removed, so ignore.
235-
pass
220+
if _mswindows:
221+
# On Windows we just need to close `Popen._handle` when we no longer need
222+
# it, so that the kernel can free it. `Popen._handle` gets closed
223+
# implicitly when the `Popen` instance is finalized (see `Handle.__del__`,
224+
# which is calling `CloseHandle` as requested in [1]), so there is nothing
225+
# for `_cleanup` to do.
226+
#
227+
# [1] https://docs.microsoft.com/en-us/windows/desktop/ProcThread/
228+
# creating-processes
229+
_active = None
230+
231+
def _cleanup():
232+
pass
233+
else:
234+
# This lists holds Popen instances for which the underlying process had not
235+
# exited at the time its __del__ method got called: those processes are
236+
# wait()ed for synchronously from _cleanup() when a new Popen object is
237+
# created, to avoid zombie processes.
238+
_active = []
239+
240+
def _cleanup():
241+
if _active is None:
242+
return
243+
for inst in _active[:]:
244+
res = inst._internal_poll(_deadstate=sys.maxsize)
245+
if res is not None:
246+
try:
247+
_active.remove(inst)
248+
except ValueError:
249+
# This can happen if two threads create a new Popen instance.
250+
# It's harmless that it was already removed, so ignore.
251+
pass
236252

237253
PIPE = -1
238254
STDOUT = -2

Lib/test/test_subprocess.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,14 @@ def setUp(self):
5959
support.reap_children()
6060

6161
def tearDown(self):
62-
for inst in subprocess._active:
63-
inst.wait()
64-
subprocess._cleanup()
65-
self.assertFalse(subprocess._active, "subprocess._active not empty")
62+
if not mswindows:
63+
# subprocess._active is not used on Windows and is set to None.
64+
for inst in subprocess._active:
65+
inst.wait()
66+
subprocess._cleanup()
67+
self.assertFalse(
68+
subprocess._active, "subprocess._active not empty"
69+
)
6670
self.doCleanups()
6771
support.reap_children()
6872

@@ -2622,8 +2626,12 @@ def test_zombie_fast_process_del(self):
26222626
with support.check_warnings(('', ResourceWarning)):
26232627
p = None
26242628

2625-
# check that p is in the active processes list
2626-
self.assertIn(ident, [id(o) for o in subprocess._active])
2629+
if mswindows:
2630+
# subprocess._active is not used on Windows and is set to None.
2631+
self.assertIsNone(subprocess._active)
2632+
else:
2633+
# check that p is in the active processes list
2634+
self.assertIn(ident, [id(o) for o in subprocess._active])
26272635

26282636
def test_leak_fast_process_del_killed(self):
26292637
# Issue #12650: on Unix, if Popen.__del__() was called before the
@@ -2644,8 +2652,12 @@ def test_leak_fast_process_del_killed(self):
26442652
p = None
26452653

26462654
os.kill(pid, signal.SIGKILL)
2647-
# check that p is in the active processes list
2648-
self.assertIn(ident, [id(o) for o in subprocess._active])
2655+
if mswindows:
2656+
# subprocess._active is not used on Windows and is set to None.
2657+
self.assertIsNone(subprocess._active)
2658+
else:
2659+
# check that p is in the active processes list
2660+
self.assertIn(ident, [id(o) for o in subprocess._active])
26492661

26502662
# let some time for the process to exit, and create a new Popen: this
26512663
# should trigger the wait() of p
@@ -2657,7 +2669,11 @@ def test_leak_fast_process_del_killed(self):
26572669
pass
26582670
# p should have been wait()ed on, and removed from the _active list
26592671
self.assertRaises(OSError, os.waitpid, pid, 0)
2660-
self.assertNotIn(ident, [id(o) for o in subprocess._active])
2672+
if mswindows:
2673+
# subprocess._active is not used on Windows and is set to None.
2674+
self.assertIsNone(subprocess._active)
2675+
else:
2676+
self.assertNotIn(ident, [id(o) for o in subprocess._active])
26612677

26622678
def test_close_fds_after_preexec(self):
26632679
fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Don't collect unfinished processes with ``subprocess._active`` on Windows to
2+
cleanup later. Patch by Ruslan Kuprieiev.

0 commit comments

Comments
 (0)