Files
hermes-agent/agent/thread_scoped_output.py
Teknium 624a752e79 refactor(agent/runtime): deadline/file_safety/estop/lifecycle leaf modules — dedupe result plumbing, drop dead guard code
- deadline: _result()/_abandon() replace 7 BoundedResult constructions and 2
  cancel+callback sites; timers handled as a list; dead raise_if_timed_out removed.
- file_safety: retired classify_cross_profile_target (0 refs; get_cross_profile_warning
  stub kept for external callers), _home_and_resolved/_mirror_warning shared by
  the sandbox/container mirror guards; _find_sandbox_mirror_segments inlined.
- estop: _hermes_home/_canonical_root now the file_safety helpers;
  _reset_log_state_for_tests inlined into its only test.
- subagent_lifecycle: _validate_request driven by _UNSUPPORTED_REQUEST_FIELDS table.
- turn_liveness: dead start()/_abort_message removed, _emit_warning shared.
- process_bootstrap: _enable_happy_eyeballs reused by the client variant.
- Comment/docstring compaction across the remaining leaf modules.
2026-09-02 13:29:48 -07:00

139 lines
4.9 KiB
Python

"""Thread-scoped stdout/stderr silencing for background worker threads.
``contextlib.redirect_stdout`` reassigns the *process-global* stream, so a
daemon worker silencing itself also silences every other thread (gateway
event loop included) for the duration. This module installs a per-thread
routing proxy as ``sys.stdout``/``sys.stderr``: silenced threads write to a
sink, everyone else passes through to the original stream. Installed once,
idempotently, and never uninstalled (that would race other threads mid-write).
"""
from __future__ import annotations
import contextlib
import os
import sys
import threading
from typing import Iterator, TextIO
__all__ = ["thread_scoped_silence"]
_install_lock = threading.Lock()
# Proxy installed per attribute ("stdout"/"stderr"): never double-wrap.
_installed: dict[str, "_ThreadRoutingStream"] = {}
# One process-lifetime sink per stream: global redirects that displace and
# restore a proxy must not leak a new /dev/null descriptor each time.
_sinks: dict[str, TextIO] = {}
_routing_states: dict[str, "_RoutingState"] = {}
class _RoutingState:
"""Silencing registry shared by every proxy generation for one stream."""
def __init__(self, sink: TextIO) -> None:
self.sink = sink
self.silenced: dict[int, int] = {}
self.lock = threading.Lock()
class _ThreadRoutingStream:
"""``sys.stdout``/``sys.stderr`` stand-in routing writes per calling thread;
unknown attributes delegate to the current thread's target."""
def __init__(self, passthrough: TextIO, state: _RoutingState) -> None:
self._passthrough = passthrough
self._state = state
def _target(self) -> TextIO:
if self._state.silenced.get(threading.get_ident(), 0) > 0:
return self._state.sink
return self._passthrough
def silence(self, ident: int) -> None:
with self._state.lock:
self._state.silenced[ident] = self._state.silenced.get(ident, 0) + 1
def unsilence(self, ident: int) -> None:
with self._state.lock:
depth = self._state.silenced.get(ident, 0) - 1
if depth > 0:
self._state.silenced[ident] = depth
else:
self._state.silenced.pop(ident, None)
def write(self, data): # type: ignore[no-untyped-def]
try:
return self._target().write(data)
except Exception:
return len(data) if isinstance(data, str) else 0
def flush(self): # type: ignore[no-untyped-def]
try:
return self._target().flush()
except Exception:
return None
def writelines(self, lines): # type: ignore[no-untyped-def]
target = self._target()
try:
return target.writelines(lines)
except Exception:
return None
def isatty(self) -> bool:
try:
return bool(self._target().isatty())
except Exception:
return False
def fileno(self): # type: ignore[no-untyped-def]
return self._target().fileno()
def __getattr__(self, name): # type: ignore[no-untyped-def]
return getattr(self._target(), name)
def _ensure_installed(attr: str, passthrough: TextIO) -> "_ThreadRoutingStream":
"""Install (idempotently) a routing proxy as ``sys.<attr>`` and return it."""
with _install_lock:
proxy = _installed.get(attr)
current = getattr(sys, attr, None)
if isinstance(current, _ThreadRoutingStream):
# A redirect context may restore an older proxy; adopt it rather
# than wrapping it into an unbounded chain.
_installed[attr] = current
_routing_states[attr] = current._state
return current
if proxy is not None and current is proxy:
return proxy
# Route non-silenced threads to whatever is currently bound (an active
# global redirect keeps its old behavior).
passthrough = current if current is not None else passthrough
sink = _sinks.get(attr)
if sink is None or sink.closed:
sink = open(os.devnull, "w", encoding="utf-8")
_sinks[attr] = sink
state = _routing_states.get(attr)
if state is None or state.sink is not sink:
state = _RoutingState(sink)
_routing_states[attr] = state
proxy = _ThreadRoutingStream(passthrough, state)
setattr(sys, attr, proxy)
_installed[attr] = proxy
return proxy
@contextlib.contextmanager
def thread_scoped_silence() -> Iterator[None]:
"""Silence ``stdout``/``stderr`` for the *current thread only*."""
ident = threading.get_ident()
out_proxy = _ensure_installed("stdout", sys.__stdout__ or sys.stdout)
err_proxy = _ensure_installed("stderr", sys.__stderr__ or sys.stderr)
out_proxy.silence(ident)
err_proxy.silence(ident)
try:
yield
finally:
out_proxy.unsilence(ident)
err_proxy.unsilence(ident)