diff options
author | Masen Furer <m_github@0x26.net> | 2022-12-16 07:07:47 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-16 07:07:47 -0800 |
commit | 36ec2c0ce071f9605a50040d5a8aa508d62abd6b (patch) | |
tree | 9cd2859f836a464e34405acc6810b3c0f4cde7a3 /src | |
parent | e26eb7b0e8eeafecdfeb9cca788fa37de9adbf6a (diff) | |
download | tox-git-36ec2c0ce071f9605a50040d5a8aa508d62abd6b.tar.gz |
implement pseudo tty on stdout/stderr (#2711)
Diffstat (limited to 'src')
-rw-r--r-- | src/tox/execute/local_sub_process/__init__.py | 68 | ||||
-rw-r--r-- | src/tox/execute/stream.py | 10 |
2 files changed, 68 insertions, 10 deletions
diff --git a/src/tox/execute/local_sub_process/__init__.py b/src/tox/execute/local_sub_process/__init__.py index c0a10088..478fb6b0 100644 --- a/src/tox/execute/local_sub_process/__init__.py +++ b/src/tox/execute/local_sub_process/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import fnmatch +import io import logging import os import shutil @@ -240,12 +241,19 @@ class LocalSubProcessExecuteInstance(ExecuteInstance): @staticmethod def get_stream_file_no(key: str) -> Generator[int, Popen[bytes], None]: - process = yield PIPE - stream = getattr(process, key) - if sys.platform == "win32": # explicit check for mypy # pragma: win32 cover - yield stream.handle + allocated_pty = _pty(key) + if allocated_pty is not None: + main_fd, child_fd = allocated_pty + yield child_fd + os.close(child_fd) # close the child process pipe + yield main_fd else: - yield stream.name + process = yield PIPE + stream = getattr(process, key) + if sys.platform == "win32": # explicit check for mypy # pragma: win32 cover + yield stream.handle + else: + yield stream.name def set_out_err(self, out: SyncWrite, err: SyncWrite) -> tuple[SyncWrite, SyncWrite]: prev = self._out, self._err @@ -256,6 +264,56 @@ class LocalSubProcessExecuteInstance(ExecuteInstance): return prev +def _pty(key: str) -> tuple[int, int] | None: + """ + Allocate a virtual terminal (pty) for a subprocess. + + A virtual terminal allows a process to perform syscalls that fetch attributes related to the tty, + for example to determine whether to use colored output or enter interactive mode. + + The termios attributes of the controlling terminal stream will be copied to the allocated pty. + + :param key: The stream to copy attributes from. Either "stdout" or "stderr". + :return: (main_fd, child_fd) of an allocated pty; or None on error or if unsupported (win32). + """ + if sys.platform == "win32": # explicit check for mypy # pragma: win32 cover + return None + + stream: io.TextIOWrapper = getattr(sys, key) + + # when our current stream is a tty, emulate pty for the child + # to allow host streams traits to be inherited + if not stream.isatty(): + return None + + try: + import fcntl + import pty + import struct + import termios + except ImportError: # pragma: no cover + return None # cannot proceed on platforms without pty support + + try: + main, child = pty.openpty() + except OSError: # could not open a tty + return None # pragma: no cover + + try: + mode = termios.tcgetattr(stream) + termios.tcsetattr(child, termios.TCSANOW, mode) + except (termios.error, OSError): # could not inherit traits + return None # pragma: no cover + + # adjust sub-process terminal size + columns, lines = shutil.get_terminal_size(fallback=(-1, -1)) + if columns != -1 and lines != -1: + size = struct.pack("HHHH", columns, lines, 0, 0) + fcntl.ioctl(child, termios.TIOCSWINSZ, size) + + return main, child + + __all__ = ( "SIG_INTERRUPT", "CREATION_FLAGS", diff --git a/src/tox/execute/stream.py b/src/tox/execute/stream.py index 2141a314..980c97ff 100644 --- a/src/tox/execute/stream.py +++ b/src/tox/execute/stream.py @@ -55,12 +55,12 @@ class SyncWrite: at = content.rfind(b"\n") if at != -1: # pragma: no branch at = len(self._content) - len(content) + at + 1 - if at != -1: - self._cancel() - try: + self._cancel() + try: + if at != -1: self._write(at) - finally: - self._start() + finally: + self._start() def _start(self) -> None: self.timer = Timer(self.REFRESH_RATE, self._trigger_timer) |