summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMasen Furer <m_github@0x26.net>2022-12-16 07:07:47 -0800
committerGitHub <noreply@github.com>2022-12-16 07:07:47 -0800
commit36ec2c0ce071f9605a50040d5a8aa508d62abd6b (patch)
tree9cd2859f836a464e34405acc6810b3c0f4cde7a3 /src
parente26eb7b0e8eeafecdfeb9cca788fa37de9adbf6a (diff)
downloadtox-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__.py68
-rw-r--r--src/tox/execute/stream.py10
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)