mitogen: Only close stdio file descriptors that were open at process startup

File descriptors 0, 1, and 2 are usually stdin, stdout, stderr; but not
always. If a process is started without one of these then the first descriptor
allocated by the process opening a file or socket will be allocated an fd <=
STDERR_FILENO. This isn't common, but it does occur, e.g. Windows GUI apps
started without being connected to a console, controller side plugins run
under Ansible 12 (ansible-core 2.19).

In such cases the corresponding sys attribute (e.g. sys.stderr) will be None.

refs #1258

See also
- https://docs.python.org/3/library/sys.html#sys.__stdin__
- https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_12.html#porting-guide-for-v12-0-0a1
- https://github.com/ansible/ansible/pull/82770
- https://github.com/python/typeshed/issues/11778
- https://gist.github.com/moreati/034fef45f73d809d9411a8a63eca34d6
pull/1268/head
Alex Willmer 6 months ago
parent 49fea37879
commit 6cec613daa

@ -21,6 +21,8 @@ To avail of fixes in an unreleased version, please download a ZIP file
In progress (unreleased)
------------------------
* :gh:issue:`1268` :mod:`mitogen` Only close stdin, stdout, and stderr file
descriptors (0, 1, and 2) if they were open at process startup.
v0.3.23 (2025-04-28)

@ -545,7 +545,16 @@ def set_cloexec(fd):
they must be explicitly closed through some other means, such as
:func:`mitogen.fork.on_fork`.
"""
assert fd > pty.STDERR_FILENO, 'fd %r <= 2 (STDERR_FILENO)' % (fd,)
stdfds = [
stdfd
for stdio, stdfd in [
(sys.stdin, pty.STDIN_FILENO),
(sys.stdout, pty.STDOUT_FILENO),
(sys.stderr, pty.STDERR_FILENO),
]
if stdio is not None and not stdio.closed
]
assert fd not in stdfds, 'fd %r is one of the stdio fds: %r' % (fd, stdfds)
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
@ -4106,11 +4115,13 @@ class ExternalContext(object):
Open /dev/null to replace stdio temporarily. In case of odd startup,
assume we may be allocated a standard handle.
"""
for stdfd, mode in [
(pty.STDIN_FILENO, os.O_RDONLY),
(pty.STDOUT_FILENO, os.O_RDWR),
(pty.STDERR_FILENO, os.O_RDWR),
for stdio, stdfd, mode in [
(sys.stdin, pty.STDIN_FILENO, os.O_RDONLY),
(sys.stdout, pty.STDOUT_FILENO, os.O_RDWR),
(sys.stderr, pty.STDERR_FILENO, os.O_RDWR),
]:
if stdio is None:
continue
fd = os.open('/dev/null', mode)
if fd != stdfd:
os.dup2(fd, stdfd)
@ -4148,10 +4159,12 @@ class ExternalContext(object):
self._nullify_stdio()
self.loggers = []
for stdfd, name in [
(pty.STDOUT_FILENO, 'stdout'),
(pty.STDERR_FILENO, 'stderr'),
for stdio, stdfd, name in [
(sys.stdout, pty.STDOUT_FILENO, 'stdout'),
(sys.stderr, pty.STDERR_FILENO, 'stderr'),
]:
if stdio is None:
continue
log = IoLoggerProtocol.build_stream(name, stdfd)
self.broker.start_receive(log)
self.loggers.append(log)

Loading…
Cancel
Save