fix(gateway): survive faulthandler.enable() when sys.stderr is None

faulthandler.enable() writes to sys.stderr by default, and raises
RuntimeError('sys.stderr is None') when the gateway is launched
without an attached console — e.g. via the Windows Startup VBS shim,
pythonw.exe, a detached service, or any parent that redirects stderr
to DEVNULL. Because this happens on the very first line of
GatewayRunner.start(), the whole gateway used to die at startup and
every configured platform adapter (Discord bot, Telegram, Slack, …)
would silently show offline until the user manually re-ran
'hermes gateway run --replace' from a real terminal.

Wrap the call and fall back to a log-file file descriptor
(logs/gateway_faulthandler.log) when stderr is unavailable, so
fatal-error stack dumps still land somewhere useful. If even the
fallback fails we log-and-continue rather than kill the gateway.

Repro traceback (from a real user's gateway-exit-diag.log, launched
via the Startup VBS with stdin_is_tty=false):

    File "gateway/run.py", line 7821, in start
        faulthandler.enable()
    RuntimeError: sys.stderr is None
This commit is contained in:
hereicq 2026-07-26 09:22:36 +08:00 committed by Teknium
parent b792bd0529
commit 3ec5fb076e

View file

@ -7823,10 +7823,24 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
Returns True if at least one adapter connected successfully.
"""
logger.info("Starting Hermes Gateway...")
# Enable faulthandler at gateway start so that SIGUSR2 (or an
# internal watchdog) can dump all thread and task stacks to stderr
# for post-mortem diagnosis of event-loop freezes (#70344).
faulthandler.enable()
# Enable faulthandler for stack dumps on freezes/crashes (#70344).
# Falls back to a log file when sys.stderr is None (Windows VBS /
# pythonw / detached service) — otherwise the gateway would die
# here and take every adapter offline. See #71671.
try:
faulthandler.enable()
except (RuntimeError, ValueError, OSError):
try:
_fh_log_dir = getattr(self.config, "log_dir", None) or os.path.join(
os.environ.get("HERMES_HOME", str(Path.home() / ".hermes")),
"logs",
)
os.makedirs(_fh_log_dir, exist_ok=True)
_fh_enable_path = os.path.join(_fh_log_dir, "gateway_faulthandler.log")
_fh_enable_file = open(_fh_enable_path, "a", encoding="utf-8")
faulthandler.enable(file=_fh_enable_file, all_threads=True)
except Exception:
logger.debug("faulthandler.enable() unavailable", exc_info=True)
# Also dump stacks to a rotating file for off-line analysis when
# the gateway is running under a service manager that doesn't
# capture stderr.