Skip to content

Commit 8f7f3a0

Browse files
committed
Popen2IO: Fix "Bad file descriptor" error
Fix init_popen_io to leave behind a sane state (0 and 1 file descriptors open), in order to prevent "Bad file descriptor" errors. Also fix test_stdouterrin_setnull to restore stdin state, while relying on capfd to do this for stdout and stderr. The "Bad file descriptor" error doesn't trigger reliably because it's triggered by garbage collection of the Popen2IO instance returned from init_popen_io as shown in this job: https://github.com/pytest-dev/execnet/actions/runs/7955155978/job/21716386705?pr=243 =================================== FAILURES =================================== __________________ test_stdouterrin_setnull[main_thread_only] __________________ execmodel = <ExecModel 'main_thread_only'> capfd = <_pytest.capture.CaptureFixture object at 0x7fba8c333990> @pytest.mark.skipif("not hasattr(os, 'dup')") def test_stdouterrin_setnull(execmodel, capfd): gateway_base.init_popen_io(execmodel) os.write(1, b"hello") > os.read(0, 1) E OSError: [Errno 9] Bad file descriptor testing/test_basics.py:254: OSError
1 parent 6b87f99 commit 8f7f3a0

File tree

2 files changed

+30
-15
lines changed

2 files changed

+30
-15
lines changed

src/execnet/gateway_base.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def sleep(self, delay):
6161
raise NotImplementedError()
6262

6363
@abc.abstractmethod
64-
def fdopen(self, fd, mode, bufsize=1):
64+
def fdopen(self, fd, mode, bufsize=1, closefd=True):
6565
raise NotImplementedError()
6666

6767
@abc.abstractmethod
@@ -113,10 +113,10 @@ def start(self, func, args=()):
113113

114114
return _thread.start_new_thread(func, args)
115115

116-
def fdopen(self, fd, mode, bufsize=1):
116+
def fdopen(self, fd, mode, bufsize=1, closefd=True):
117117
import os
118118

119-
return os.fdopen(fd, mode, bufsize, encoding="utf-8")
119+
return os.fdopen(fd, mode, bufsize, encoding="utf-8", closefd=closefd)
120120

121121
def Lock(self):
122122
import threading
@@ -174,10 +174,10 @@ def start(self, func, args=()):
174174

175175
return eventlet.spawn_n(func, *args)
176176

177-
def fdopen(self, fd, mode, bufsize=1):
177+
def fdopen(self, fd, mode, bufsize=1, closefd=True):
178178
import eventlet.green.os
179179

180-
return eventlet.green.os.fdopen(fd, mode, bufsize)
180+
return eventlet.green.os.fdopen(fd, mode, bufsize, closefd=closefd)
181181

182182
def Lock(self):
183183
import eventlet.green.threading
@@ -231,11 +231,11 @@ def start(self, func, args=()):
231231

232232
return gevent.spawn(func, *args)
233233

234-
def fdopen(self, fd, mode, bufsize=1):
234+
def fdopen(self, fd, mode, bufsize=1, closefd=True):
235235
# XXX
236236
import gevent.fileobject
237237

238-
return gevent.fileobject.FileObjectThread(fd, mode, bufsize)
238+
return gevent.fileobject.FileObjectThread(fd, mode, bufsize, closefd=closefd)
239239

240240
def Lock(self):
241241
import gevent.lock
@@ -1682,8 +1682,10 @@ def init_popen_io(execmodel):
16821682
os.dup2(fd, 2)
16831683
os.close(fd)
16841684
io = Popen2IO(stdout, stdin, execmodel)
1685-
sys.stdin = execmodel.fdopen(0, "r", 1)
1686-
sys.stdout = execmodel.fdopen(1, "w", 1)
1685+
# Use closefd=False since 0 and 1 are shared with
1686+
# sys.__stdin__ and sys.__stdout__.
1687+
sys.stdin = execmodel.fdopen(0, "r", 1, closefd=False)
1688+
sys.stdout = execmodel.fdopen(1, "w", 1, closefd=False)
16871689
return io
16881690

16891691

testing/test_basics.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,25 @@ class Arg:
249249

250250
@pytest.mark.skipif("not hasattr(os, 'dup')")
251251
def test_stdouterrin_setnull(execmodel, capfd):
252-
gateway_base.init_popen_io(execmodel)
253-
os.write(1, b"hello")
254-
os.read(0, 1)
255-
out, err = capfd.readouterr()
256-
assert not out
257-
assert not err
252+
# Backup and restore stdin state, and rely on capfd to handle
253+
# this for stdout and stderr.
254+
orig_stdin = sys.stdin
255+
orig_stdin_fd = os.dup(0)
256+
try:
257+
# The returned Popen2IO instance can be garbage collected
258+
# prematurely since we don't hold a reference here, but we
259+
# tolerate this because it is intended to leave behind a
260+
# sane state afterwards.
261+
gateway_base.init_popen_io(execmodel)
262+
os.write(1, b"hello")
263+
os.read(0, 1)
264+
out, err = capfd.readouterr()
265+
assert not out
266+
assert not err
267+
finally:
268+
sys.stdin = orig_stdin
269+
os.dup2(orig_stdin_fd, 0)
270+
os.close(orig_stdin_fd)
258271

259272

260273
class PseudoChannel:

0 commit comments

Comments
 (0)