From 6cec613daaeecba5e80790e71170ec2afb6f8d98 Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Wed, 28 May 2025 12:22:15 +0100 Subject: [PATCH] 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 --- docs/changelog.rst | 2 ++ mitogen/core.py | 29 +++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1ae61118..748778d9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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) diff --git a/mitogen/core.py b/mitogen/core.py index 057cdbcf..5be36a95 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -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)