Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

203 lines
6.7 KiB
Python

"""Shared subprocess-streaming primitives, PTY helpers, and module-wide constants."""
from __future__ import annotations
import contextlib
import errno
import os
import subprocess
import sys
import tempfile
import threading
from typing import IO, Any
from rich.console import Console
from rich.markup import escape
from rich.text import Text
from platform.common.task_types import TaskRecord
from surfaces.interactive_shell.ui import DIM, ERROR
from surfaces.interactive_shell.utils.error_handling.exception_reporting import report_exception
from tools.interactive_shell.subprocess import (
CLAUDE_CODE_IMPLEMENTATION_TIMEOUT_SECONDS,
MAX_COMMAND_OUTPUT_CHARS,
MIN_SUBPROCESS_TERMINAL_WIDTH,
SHELL_COMMAND_TIMEOUT_SECONDS,
SYNTHETIC_DIAG_CHARS,
SYNTHETIC_POLL_SECONDS,
SYNTHETIC_TEST_TIMEOUT_SECONDS,
TASK_OUTPUT_JOIN_TIMEOUT_SECONDS,
TASK_OUTPUT_PREFIX_WIDTH,
read_diag,
read_task_output,
subprocess_env_with_width,
terminate_child_process,
)
# Full dotted name of the ``subprocess_runner`` package. Submodules use this to
# look up patchable names from the parent namespace at call time so that tests
# using ``monkeypatch.setattr("…subprocess_runner.X", fake)`` take effect even
# when the implementation lives in a submodule.
_SUBPROCESS_RUNNER_MODULE = "surfaces.interactive_shell.runtime.subprocess_runner"
# Backward-compatible aliases for tests and callers using underscore-prefixed names.
_MAX_COMMAND_OUTPUT_CHARS = MAX_COMMAND_OUTPUT_CHARS
_SYNTHETIC_POLL_SECONDS = SYNTHETIC_POLL_SECONDS
_SYNTHETIC_DIAG_CHARS = SYNTHETIC_DIAG_CHARS
_MIN_SUBPROCESS_TERMINAL_WIDTH = MIN_SUBPROCESS_TERMINAL_WIDTH
_TASK_OUTPUT_PREFIX_WIDTH = TASK_OUTPUT_PREFIX_WIDTH
_TASK_OUTPUT_JOIN_TIMEOUT_SECONDS = TASK_OUTPUT_JOIN_TIMEOUT_SECONDS
def _sr_resolve(name: str, default: Any) -> Any:
"""Return ``subprocess_runner.<name>`` if the package is loaded, else ``default``.
Used by submodules to honour monkeypatches applied to the parent package
namespace (e.g. ``monkeypatch.setattr("…subprocess_runner.read_diag", …)``).
"""
sr = sys.modules.get(_SUBPROCESS_RUNNER_MODULE)
return getattr(sr, name, default) if sr is not None else default
def _print_task_output_line(
console: Console,
task: TaskRecord,
stream_name: str,
line: str,
*,
style: str | None = None,
) -> None:
text = Text()
text.append(f"{task.task_id} {stream_name} │ ", style=DIM)
text.append(line.rstrip("\r\n"), style=style)
console.print(text)
def _subprocess_env_with_aligned_width(console: Console) -> dict[str, str]:
"""Return ``os.environ`` patched so a piped Rich subprocess wraps to fit."""
user_width = console.size.width or _MIN_SUBPROCESS_TERMINAL_WIDTH + _TASK_OUTPUT_PREFIX_WIDTH
return subprocess_env_with_width(
columns=user_width,
lines=console.size.height,
)
def _pump_task_stream(
*,
task: TaskRecord,
stream_name: str,
stream: IO[str],
console: Console,
style: str | None = None,
capture: tempfile.SpooledTemporaryFile[bytes] | None = None, # type: ignore[type-arg]
) -> None:
try:
for line in stream:
if capture is not None:
capture.write(line.encode("utf-8", errors="replace"))
if line.strip():
_print_task_output_line(console, task, stream_name, line, style=style)
task.update_progress(line)
except Exception as exc: # noqa: BLE001
report_exception(exc, context=f"surfaces.interactive_shell.task_stream.{stream_name}")
console.print(f"[{DIM}]task output stream ended unexpectedly:[/] {escape(str(exc))}")
def _start_task_output_streams(
*,
task: TaskRecord,
proc: subprocess.Popen[Any],
console: Console,
stdout_capture: tempfile.SpooledTemporaryFile[bytes] | None = None, # type: ignore[type-arg]
stderr_capture: tempfile.SpooledTemporaryFile[bytes] | None = None, # type: ignore[type-arg]
) -> list[threading.Thread]:
threads: list[threading.Thread] = []
streams: tuple[tuple[str, IO[str] | None, str | None, Any], ...] = (
("stdout", proc.stdout, None, stdout_capture),
("stderr", proc.stderr, ERROR, stderr_capture),
)
for stream_name, stream, style, capture in streams:
if stream is None:
continue
thread = threading.Thread(
target=_pump_task_stream,
kwargs={
"task": task,
"stream_name": stream_name,
"stream": stream,
"console": console,
"style": style,
"capture": capture,
},
daemon=True,
name=f"task-output-{task.task_id}-{stream_name}",
)
thread.start()
threads.append(thread)
return threads
def _join_task_output_streams(threads: list[threading.Thread]) -> None:
for thread in threads:
thread.join(timeout=TASK_OUTPUT_JOIN_TIMEOUT_SECONDS)
def _console_file_is_tty(console: Console) -> bool:
isatty = getattr(console.file, "isatty", None)
return bool(isatty and isatty())
def _should_use_pty(console: Console, requested: bool) -> bool:
return requested and hasattr(os, "openpty") and _console_file_is_tty(console)
def _pump_task_pty(
*,
master_fd: int,
console: Console,
capture: tempfile.SpooledTemporaryFile[bytes], # type: ignore[type-arg]
) -> None:
try:
while True:
try:
chunk = os.read(master_fd, 4096)
except OSError as exc:
if exc.errno == errno.EIO:
break
raise
if not chunk:
break
capture.write(chunk)
console.file.write(chunk.decode("utf-8", errors="replace"))
console.file.flush()
except Exception as exc: # noqa: BLE001
report_exception(exc, context="surfaces.interactive_shell.task_pty_stream")
console.print(f"[{DIM}]task terminal stream ended unexpectedly:[/] {escape(str(exc))}")
finally:
with contextlib.suppress(OSError):
os.close(master_fd)
__all__ = [
"SHELL_COMMAND_TIMEOUT_SECONDS",
"SYNTHETIC_TEST_TIMEOUT_SECONDS",
"CLAUDE_CODE_IMPLEMENTATION_TIMEOUT_SECONDS",
"_SYNTHETIC_POLL_SECONDS",
"_MAX_COMMAND_OUTPUT_CHARS",
"_SYNTHETIC_DIAG_CHARS",
"_TASK_OUTPUT_PREFIX_WIDTH",
"_MIN_SUBPROCESS_TERMINAL_WIDTH",
"_TASK_OUTPUT_JOIN_TIMEOUT_SECONDS",
"terminate_child_process",
"read_diag",
"read_task_output",
"_print_task_output_line",
"_subprocess_env_with_aligned_width",
"_pump_task_stream",
"_start_task_output_streams",
"_join_task_output_streams",
"_console_file_is_tty",
"_should_use_pty",
"_pump_task_pty",
]