From 2bb64ce81e217bb3085c968c35214174658a25a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=90?= <1578804454@qq.com> Date: Sun, 25 Oct 2026 22:11:48 +0800 Subject: [PATCH 1/2] Propagate cwd errors through the child error pipe Include chdir in the existing OSError reporting boundary while preserving preexec_fn ordering. --- ptyprocess/ptyprocess.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ptyprocess/ptyprocess.py b/ptyprocess/ptyprocess.py index daf20b1..7740f78 100644 --- a/ptyprocess/ptyprocess.py +++ b/ptyprocess/ptyprocess.py @@ -275,30 +275,30 @@ def spawn( for pair in zip([2] + spass_fds, spass_fds + [max_fd]): os.closerange(pair[0]+1, pair[1]) - if cwd is not None: - os.chdir(cwd) - - if preexec_fn is not None: - try: - preexec_fn() - except Exception as e: - ename = type(e).__name__ - tosend = '{}:0:{}'.format(ename, str(e)) - if PY3: - tosend = tosend.encode('utf-8') - - os.write(exec_err_pipe_write, tosend) - os.close(exec_err_pipe_write) - os._exit(1) - try: + if cwd is not None: + os.chdir(cwd) + + if preexec_fn is not None: + try: + preexec_fn() + except Exception as e: + ename = type(e).__name__ + tosend = '{}:0:{}'.format(ename, str(e)) + if PY3: + tosend = tosend.encode('utf-8') + + os.write(exec_err_pipe_write, tosend) + os.close(exec_err_pipe_write) + os._exit(1) + if env is None: os.execv(command, argv) else: os.execvpe(command, argv, env) except OSError as err: - # [issue #119] 5. If exec fails, the child writes the error - # code back to the parent using the pipe, then exits. + # [issue #119] 5. If chdir or exec fails, the child writes the + # error code back to the parent using the pipe, then exits. tosend = 'OSError:{}:{}'.format(err.errno, str(err)) if PY3: tosend = tosend.encode('utf-8') From 79d032b69f09d6cc5f4eda35ac58321ba1932906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=90?= <1578804454@qq.com> Date: Sun, 25 Oct 2026 22:12:57 +0800 Subject: [PATCH 2/2] Test cwd error propagation and preexec ordering Use isolated interpreters for invalid cwd regressions, and retain a valid cwd/preexec control. --- tests/test_cwd.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_cwd.py diff --git a/tests/test_cwd.py b/tests/test_cwd.py new file mode 100644 index 0000000..e67cb50 --- /dev/null +++ b/tests/test_cwd.py @@ -0,0 +1,72 @@ +import errno +import os +import subprocess +import sys +import tempfile +import unittest + +from ptyprocess import PtyProcess + + +class CwdTestCase(unittest.TestCase): + def _spawn_with_invalid_cwd(self, cwd): + # Isolate the call: an unreported child-side exception must not let the + # forked child continue running the test suite. + code = """ +from ptyprocess import PtyProcess +import sys +try: + child = PtyProcess.spawn([sys.executable, '-c', 'pass'], cwd=sys.argv[1]) +except OSError as err: + print('error:{}'.format(err.errno)) +else: + child.close(force=True) + print('spawned') +""" + return subprocess.check_output( + [sys.executable, '-c', code, cwd], + cwd=os.path.dirname(os.path.dirname(__file__)), + timeout=10, + ) + + def test_missing_cwd(self): + with tempfile.TemporaryDirectory() as directory: + output = self._spawn_with_invalid_cwd( + os.path.join(directory, 'missing') + ) + self.assertEqual(output, 'error:{}\n'.format(errno.ENOENT).encode()) + + def test_cwd_is_not_a_directory(self): + with tempfile.NamedTemporaryFile() as file: + output = self._spawn_with_invalid_cwd(file.name) + self.assertEqual(output, 'error:{}\n'.format(errno.ENOTDIR).encode()) + + def test_valid_cwd_before_preexec(self): + original_cwd = os.getcwd() + with tempfile.TemporaryDirectory() as directory: + def preexec(): + with open('preexec-marker', 'w') as file: + file.write('ready') + + child = PtyProcess.spawn( + [sys.executable, '-c', 'import os; print(os.getcwd())'], + cwd=directory, + preexec_fn=preexec, + ) + try: + output = b'' + while True: + try: + output += child.read() + except EOFError: + break + self.assertEqual(child.wait(), 0) + finally: + child.close(force=True) + + self.assertEqual( + output.strip(), os.fsencode(os.path.realpath(directory)) + ) + with open(os.path.join(directory, 'preexec-marker')) as file: + self.assertEqual(file.read(), 'ready') + self.assertEqual(os.getcwd(), original_cwd)