Skip to content

Commit 042821a

Browse files
efiopvstinner
authored andcommitted
bpo-37380: subprocess: don't use _active on win (GH-14360)
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/issue37380#msg346333 [2] https://bugs.python.org/issue36067#msg336262 Signed-off-by: Ruslan Kuprieiev <[email protected]>
1 parent 64580da commit 042821a

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
@@ -218,22 +218,38 @@ def __repr__(self):
218218
_PopenSelector = selectors.SelectSelector
219219

220220

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

238254
PIPE = -1
239255
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

@@ -2679,8 +2683,12 @@ def test_zombie_fast_process_del(self):
26792683
with support.check_warnings(('', ResourceWarning)):
26802684
p = None
26812685

2682-
# check that p is in the active processes list
2683-
self.assertIn(ident, [id(o) for o in subprocess._active])
2686+
if mswindows:
2687+
# subprocess._active is not used on Windows and is set to None.
2688+
self.assertIsNone(subprocess._active)
2689+
else:
2690+
# check that p is in the active processes list
2691+
self.assertIn(ident, [id(o) for o in subprocess._active])
26842692

26852693
def test_leak_fast_process_del_killed(self):
26862694
# Issue #12650: on Unix, if Popen.__del__() was called before the
@@ -2701,8 +2709,12 @@ def test_leak_fast_process_del_killed(self):
27012709
p = None
27022710

27032711
os.kill(pid, signal.SIGKILL)
2704-
# check that p is in the active processes list
2705-
self.assertIn(ident, [id(o) for o in subprocess._active])
2712+
if mswindows:
2713+
# subprocess._active is not used on Windows and is set to None.
2714+
self.assertIsNone(subprocess._active)
2715+
else:
2716+
# check that p is in the active processes list
2717+
self.assertIn(ident, [id(o) for o in subprocess._active])
27062718

27072719
# let some time for the process to exit, and create a new Popen: this
27082720
# should trigger the wait() of p
@@ -2714,7 +2726,11 @@ def test_leak_fast_process_del_killed(self):
27142726
pass
27152727
# p should have been wait()ed on, and removed from the _active list
27162728
self.assertRaises(OSError, os.waitpid, pid, 0)
2717-
self.assertNotIn(ident, [id(o) for o in subprocess._active])
2729+
if mswindows:
2730+
# subprocess._active is not used on Windows and is set to None.
2731+
self.assertIsNone(subprocess._active)
2732+
else:
2733+
self.assertNotIn(ident, [id(o) for o in subprocess._active])
27182734

27192735
def test_close_fds_after_preexec(self):
27202736
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)