Skip to content

Commit a6d865c

Browse files
committed
Issue #25654:
* multiprocessing: open file with closefd=False to avoid ResourceWarning * _test_multiprocessing: open file with O_EXCL to detect bugs in tests (if a previous test forgot to remove TESTFN) * test_sys_exit(): remove TESTFN after each loop iteration Initial patch written by Serhiy Storchaka.
1 parent 47b4557 commit a6d865c

File tree

5 files changed

+39
-19
lines changed

5 files changed

+39
-19
lines changed

‎Doc/library/multiprocessing.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2694,7 +2694,7 @@ Beware of replacing :data:`sys.stdin` with a "file like object"
26942694
in issues with processes-in-processes. This has been changed to::
26952695

26962696
sys.stdin.close()
2697-
sys.stdin = open(os.devnull)
2697+
sys.stdin = open(os.open(os.devnull, os.O_RDONLY), closefd=False)
26982698

26992699
Which solves the fundamental issue of processes colliding with each other
27002700
resulting in a bad file descriptor error, but introduces a potential danger

‎Lib/multiprocessing/forkserver.py‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,7 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None):
147147
except ImportError:
148148
pass
149149

150-
# close sys.stdin
151-
if sys.stdin is not None:
152-
try:
153-
sys.stdin.close()
154-
sys.stdin = open(os.devnull)
155-
except (OSError, ValueError):
156-
pass
150+
util._close_stdin()
157151

158152
# ignoring SIGCHLD means no need to reap zombie processes
159153
handler = signal.signal(signal.SIGCHLD, signal.SIG_IGN)

‎Lib/multiprocessing/process.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,7 @@ def _bootstrap(self):
234234
context._force_start_method(self._start_method)
235235
_process_counter = itertools.count(1)
236236
_children = set()
237-
if sys.stdin is not None:
238-
try:
239-
sys.stdin.close()
240-
sys.stdin = open(os.devnull)
241-
except (OSError, ValueError):
242-
pass
237+
util._close_stdin()
243238
old_process = _current_process
244239
_current_process = self
245240
try:

‎Lib/multiprocessing/util.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import os
1111
import itertools
12+
import sys
1213
import weakref
1314
import atexit
1415
import threading # we want threading to install it's
@@ -356,6 +357,28 @@ def close_all_fds_except(fds):
356357
assert fds[-1] == MAXFD, 'fd too large'
357358
for i in range(len(fds) - 1):
358359
os.closerange(fds[i]+1, fds[i+1])
360+
#
361+
# Close sys.stdin and replace stdin with os.devnull
362+
#
363+
364+
def _close_stdin():
365+
if sys.stdin is None:
366+
return
367+
368+
try:
369+
sys.stdin.close()
370+
except (OSError, ValueError):
371+
pass
372+
373+
try:
374+
fd = os.open(os.devnull, os.O_RDONLY)
375+
try:
376+
sys.stdin = open(fd, closefd=False)
377+
except:
378+
os.close(fd)
379+
raise
380+
except (OSError, ValueError):
381+
pass
359382

360383
#
361384
# Start a program with only specified fds kept open

‎Lib/test/_test_multiprocessing.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -455,13 +455,15 @@ def test_stderr_flush(self):
455455

456456
@classmethod
457457
def _test_stderr_flush(cls, testfn):
458-
sys.stderr = open(testfn, 'w')
458+
fd = os.open(testfn, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
459+
sys.stderr = open(fd, 'w', closefd=False)
459460
1/0 # MARKER
460461

461462

462463
@classmethod
463464
def _test_sys_exit(cls, reason, testfn):
464-
sys.stderr = open(testfn, 'w')
465+
fd = os.open(testfn, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
466+
sys.stderr = open(fd, 'w', closefd=False)
465467
sys.exit(reason)
466468

467469
def test_sys_exit(self):
@@ -472,15 +474,21 @@ def test_sys_exit(self):
472474
testfn = test.support.TESTFN
473475
self.addCleanup(test.support.unlink, testfn)
474476

475-
for reason, code in (([1, 2, 3], 1), ('ignore this', 1)):
477+
for reason in (
478+
[1, 2, 3],
479+
'ignore this',
480+
):
476481
p = self.Process(target=self._test_sys_exit, args=(reason, testfn))
477482
p.daemon = True
478483
p.start()
479484
p.join(5)
480-
self.assertEqual(p.exitcode, code)
485+
self.assertEqual(p.exitcode, 1)
481486

482487
with open(testfn, 'r') as f:
483-
self.assertEqual(f.read().rstrip(), str(reason))
488+
content = f.read()
489+
self.assertEqual(content.rstrip(), str(reason))
490+
491+
os.unlink(testfn)
484492

485493
for reason in (True, False, 8):
486494
p = self.Process(target=sys.exit, args=(reason,))

0 commit comments

Comments
 (0)