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
203 lines
6.7 KiB
Python
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",
|
|
]
|