From e3858772d0465d2c5c386cb788642276138b5253 Mon Sep 17 00:00:00 2001 From: AllynSheep <5029547+AllynSheep@users.noreply.github.com> Date: Tue, 12 May 2026 16:37:21 -0700 Subject: [PATCH] fix(dashboard): skip browser-open on headless Linux to prevent process exit Fixes #24127 On headless Linux VPS (no DISPLAY or WAYLAND_DISPLAY), some Python webbrowser backends register TUI programs such as links, lynx, or www-browser. GenericBrowser.open() spawns these without redirecting stdin/stdout, allowing them to take over the terminal. This can cause the process to receive SIGHUP and exit immediately even though uvicorn bound the port successfully, producing a misleading success message followed by an empty --status. Fix: detect headless Linux at startup and skip the auto-open when no display server is available. On such systems the URL is still printed so the user can open it manually or via an SSH tunnel. The webbrowser call is also wrapped in a try/except so any unexpected failure on other platforms is silently absorbed rather than surfacing as an unhandled exception in the daemon thread. --- hermes_cli/web_server.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index f1d14ebf48b..fb5f7ca12d3 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -4432,11 +4432,33 @@ def start_server( if open_browser: import webbrowser - def _open(): - time.sleep(1.0) - webbrowser.open(f"http://{host}:{port}") + # On headless Linux (no DISPLAY or WAYLAND_DISPLAY) some registered + # browsers are TUI programs (links, lynx, www-browser) that try to + # take over the terminal. That can send SIGHUP to the server process + # and cause an immediate exit even though uvicorn bound successfully. + # Skip the auto-open attempt on headless systems and let the user + # open the URL manually. macOS and Windows are always considered + # display-capable. + _has_display = ( + sys.platform != "linux" + or bool(os.environ.get("DISPLAY")) + or bool(os.environ.get("WAYLAND_DISPLAY")) + ) - threading.Thread(target=_open, daemon=True).start() + if _has_display: + def _open(): + try: + time.sleep(1.0) + webbrowser.open(f"http://{host}:{port}") + except Exception: + pass + + threading.Thread(target=_open, daemon=True).start() + else: + _log.debug( + "Skipping browser-open: no DISPLAY or WAYLAND_DISPLAY detected " + "(headless Linux). Pass --no-open to suppress this detection." + ) print(f" Hermes Web UI → http://{host}:{port}") uvicorn.run(app, host=host, port=port, log_level="warning")