From 01c4f3fee13c32e1b46908c76f0275376ea82465 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 6 Nov 2018 01:47:36 +0000 Subject: [PATCH] core: rearrange stdio setup to cope with buffering; closes #422 --- docs/changelog.rst | 4 ++++ mitogen/core.py | 42 +++++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 87ad7f00..52388ed0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -245,6 +245,10 @@ Core Library execution of its :keyword:`finally` block was delayed on Python 3. Now callers explicitly close the generator when finished. +* `#421 `_: the fork method could + fail to start if :data:`sys.stdout` was opened in block buffered mode, and + buffered data was pending in the parent prior to fork. + * `16ca111e `_: handle OpenSSH 7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present. diff --git a/mitogen/core.py b/mitogen/core.py index 0876cd8c..b1519c90 100644 --- a/mitogen/core.py +++ b/mitogen/core.py @@ -2808,28 +2808,32 @@ class ExternalContext(object): mitogen.parent_ids = self.config['parent_ids'][:] mitogen.parent_id = mitogen.parent_ids[0] - def _setup_stdio(self): - # We must open this prior to closing stdout, otherwise it will recycle - # a standard handle, the dup2() will not error, and on closing it, we - # lose a standrd handle, causing later code to again recycle a standard - # handle. - fp = open('/dev/null') + def _nullify_stdio(self): + """ + Open /dev/null to replace stdin, and stdout/stderr temporarily. In case + of odd startup, assume we may be allocated a standard handle. + """ + fd = os.open('/dev/null', os.O_RDWR) + try: + for stdfd in (0, 1, 2): + if fd != stdfd: + os.dup2(fd, stdfd) + finally: + if fd not in (0, 1, 2): + os.close(fd) + def _setup_stdio(self): # When sys.stdout was opened by the runtime, overwriting it will not - # cause close to be called. However when forking from a child that - # previously used fdopen, overwriting it /will/ cause close to be - # called. So we must explicitly close it before IoLogger overwrites the - # file descriptor, otherwise the assignment below will cause stdout to - # be closed. + # close FD 1. However when forking from a child that previously used + # fdopen(), overwriting it /will/ close FD 1. So we must swallow the + # close before IoLogger overwrites FD 1, otherwise its new FD 1 will be + # clobbered. Additionally, stdout must be replaced with /dev/null prior + # to stdout.close(), since if block buffering was active in the parent, + # any pre-fork buffered data will be flushed on close(), corrupting the + # connection to the parent. + self._nullify_stdio() sys.stdout.close() - sys.stdout = None - - try: - os.dup2(fp.fileno(), 0) - os.dup2(fp.fileno(), 1) - os.dup2(fp.fileno(), 2) - finally: - fp.close() + self._nullify_stdio() self.stdout_log = IoLogger(self.broker, 'stdout', 1) self.stderr_log = IoLogger(self.broker, 'stderr', 2)