diff --git a/rnsh/process.py b/rnsh/process.py index c024f10..0fe7d0d 100644 --- a/rnsh/process.py +++ b/rnsh/process.py @@ -351,11 +351,11 @@ def _launch_child(cmd_line: list[str], env: dict[str, str], stdin_is_pipe: bool, os.dup2(child_stdout, 1) os.dup2(child_stderr, 2) # Make PTY controlling if necessary - if not stdin_is_pipe: + if child_fd is not None: os.setsid() - tmp_fd = os.open(os.ttyname(0), os.O_RDWR) + tmp_fd = os.open(os.ttyname(0 if not stdin_is_pipe else 1 if not stdout_is_pipe else 2), os.O_RDWR) os.close(tmp_fd) - # fcntl.ioctl(0, termios.TIOCSCTTY, 0) + # fcntl.ioctl(0 if not stdin_is_pipe else 1 if not stdout_is_pipe else 2), os.O_RDWR, termios.TIOCSCTTY, 0) # Execute the command os.execvpe(cmd_line[0], cmd_line, env) @@ -365,7 +365,7 @@ def _launch_child(cmd_line: list[str], env: dict[str, str], stdin_is_pipe: bool, print(f"Unable to start {cmd_line[0]}: {err} ({fname}:{exc_tb.tb_lineno})") sys.stdout.flush() # don't let any other modules get in our way, do an immediate silent exit. - os._exit(0) + os._exit(255) else: # We are in the parent process, so close the child-side of the PTY and/or pipes diff --git a/tests/helpers.py b/tests/helpers.py index d77071b..98170d6 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -23,7 +23,8 @@ module_dir = os.path.dirname(module_abs_filename) class SubprocessReader(contextlib.AbstractContextManager): - def __init__(self, argv: [str], env: dict = None, name: str = None): + def __init__(self, argv: [str], env: dict = None, name: str = None, stdin_is_pipe: bool = False, + stdout_is_pipe: bool = False, stderr_is_pipe: bool = False): self._log = module_logger.getChild(self.__class__.__name__ + ("" if name is None else f"({name})")) self.name = name or "subproc" self.process: rnsh.process.CallbackSubprocess @@ -32,16 +33,17 @@ class SubprocessReader(contextlib.AbstractContextManager): self.argv = argv self._lock = threading.RLock() self._stdout = bytearray() + self._stderr = bytearray() self.return_code: int = None self.process = rnsh.process.CallbackSubprocess(argv=self.argv, env=self.env, loop=self.loop, stdout_callback=self._stdout_cb, terminated_callback=self._terminated_cb, - stderr_callback=self._stdout_cb, - stdin_is_pipe=False, - stdout_is_pipe=False, - stderr_is_pipe=False) + stderr_callback=self._stderr_cb, + stdin_is_pipe=stdin_is_pipe, + stdout_is_pipe=stdout_is_pipe, + stderr_is_pipe=stderr_is_pipe) def _stdout_cb(self, data): self._log.debug(f"_stdout_cb({data})") @@ -56,6 +58,19 @@ class SubprocessReader(contextlib.AbstractContextManager): self._log.debug(f"read() returns {data}") return data + def _stderr_cb(self, data): + self._log.debug(f"_stderr_cb({data})") + with self._lock: + self._stderr.extend(data) + + def read_err(self): + self._log.debug(f"read_err()") + with self._lock: + data = self._stderr.copy() + self._stderr.clear() + self._log.debug(f"read_err() returns {data}") + return data + def _terminated_cb(self, rc): self._log.debug(f"_terminated_cb({rc})") self.return_code = rc diff --git a/tests/test_process.py b/tests/test_process.py index 0d5331c..7f8ebe8 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -51,6 +51,125 @@ async def test_echo_live(): assert decoded == message assert not state.process.running + +@pytest.mark.skip_ci +@pytest.mark.asyncio +async def test_echo_live_pipe_in(): + """ + Check for immediate echo + """ + with tests.helpers.SubprocessReader(argv=["/bin/cat"], stdin_is_pipe=True) as state: + state.start() + assert state.process is not None + assert state.process.running + message = "t" + state.process.write(message.encode("utf-8")) + await asyncio.sleep(0.1) + data = state.read() + state.process.close_stdin() + await asyncio.sleep(0.1) + assert len(data) > 0 + decoded = data.decode("utf-8") + assert decoded == message + assert not state.process.running + + +@pytest.mark.skip_ci +@pytest.mark.asyncio +async def test_echo_live_pipe_out(): + """ + Check for immediate echo + """ + with tests.helpers.SubprocessReader(argv=["/bin/cat"], stdout_is_pipe=True) as state: + state.start() + assert state.process is not None + assert state.process.running + message = "t" + state.process.write(message.encode("utf-8")) + state.process.write(rnsh.process.CTRL_D) + await asyncio.sleep(0.1) + data = state.read() + assert len(data) > 0 + decoded = data.decode("utf-8") + assert decoded == message + data = state.read_err() + assert len(data) > 0 + state.process.close_stdin() + await asyncio.sleep(0.1) + assert not state.process.running + + +@pytest.mark.skip_ci +@pytest.mark.asyncio +async def test_echo_live_pipe_err(): + """ + Check for immediate echo + """ + with tests.helpers.SubprocessReader(argv=["/bin/cat"], stderr_is_pipe=True) as state: + state.start() + assert state.process is not None + assert state.process.running + message = "t" + state.process.write(message.encode("utf-8")) + await asyncio.sleep(0.1) + data = state.read() + state.process.write(rnsh.process.CTRL_C) + await asyncio.sleep(0.1) + assert len(data) > 0 + decoded = data.decode("utf-8") + assert decoded == message + assert not state.process.running + + +@pytest.mark.skip_ci +@pytest.mark.asyncio +async def test_echo_live_pipe_out_err(): + """ + Check for immediate echo + """ + with tests.helpers.SubprocessReader(argv=["/bin/cat"], stdout_is_pipe=True, stderr_is_pipe=True) as state: + state.start() + assert state.process is not None + assert state.process.running + message = "t" + state.process.write(message.encode("utf-8")) + state.process.write(rnsh.process.CTRL_D) + await asyncio.sleep(0.1) + data = state.read() + assert len(data) > 0 + decoded = data.decode("utf-8") + assert decoded == message + data = state.read_err() + assert len(data) == 0 + state.process.close_stdin() + await asyncio.sleep(0.1) + assert not state.process.running + + + +@pytest.mark.skip_ci +@pytest.mark.asyncio +async def test_echo_live_pipe_all(): + """ + Check for immediate echo + """ + with tests.helpers.SubprocessReader(argv=["/bin/cat"], stdout_is_pipe=True, stderr_is_pipe=True, + stdin_is_pipe=True) as state: + state.start() + assert state.process is not None + assert state.process.running + message = "t" + state.process.write(message.encode("utf-8")) + await asyncio.sleep(0.1) + data = state.read() + state.process.close_stdin() + await asyncio.sleep(0.1) + assert len(data) > 0 + decoded = data.decode("utf-8") + assert decoded == message + assert not state.process.running + + @pytest.mark.skip_ci @pytest.mark.asyncio async def test_double_echo_live():