fix(serve): announce both ready tokens so a stale packaged parser still boots
A headless `hermes serve` emitted only HERMES_BACKEND_READY. A packaged Desktop artifact whose readiness parser predates the neutral token (the CLI-only `hermes update` path never rebuilds the packaged app) matched nothing, then killed the healthy backend after the port-announcement timeout — the artifact-skew signature of #60772. Announce the neutral token first and the legacy HERMES_DASHBOARD_READY one after it; current parsers match either, and stale packaged parsers match the legacy line. The legacy `dashboard` backend keeps its token. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
@@ -1353,11 +1353,18 @@ def _on_server_started(
|
||||
_best_effort("host rendezvous publish", lambda: _publish_host_rendezvous(host, actual_port))
|
||||
|
||||
_write_dashboard_ready_file(actual_port)
|
||||
# Port-discovery sentinel parsed by the Desktop spawn (matches either
|
||||
# token). Written to fd 1: tui_gateway.server redirects sys.stdout to
|
||||
# stderr at import, and the Desktop watches child.stdout (#96282).
|
||||
ready_token = "HERMES_BACKEND_READY" if headless else "HERMES_DASHBOARD_READY"
|
||||
_write_machine_sentinel_line(f"{ready_token} port={actual_port}")
|
||||
# Port-discovery sentinel parsed by the Desktop spawn. Written to fd 1:
|
||||
# tui_gateway.server redirects sys.stdout to stderr at import, and the
|
||||
# Desktop watches child.stdout (#96282). A headless `serve` announces the
|
||||
# neutral token FIRST and the legacy HERMES_DASHBOARD_READY one after it:
|
||||
# a packaged Desktop artifact whose parser predates the neutral token
|
||||
# (#60772) still matches the legacy sentinel, while current parsers match
|
||||
# either. The legacy `dashboard` backend keeps its own single token.
|
||||
if headless:
|
||||
_write_machine_sentinel_line(f"HERMES_BACKEND_READY port={actual_port}")
|
||||
_write_machine_sentinel_line(f"HERMES_DASHBOARD_READY port={actual_port}")
|
||||
else:
|
||||
_write_machine_sentinel_line(f"HERMES_DASHBOARD_READY port={actual_port}")
|
||||
if headless:
|
||||
# Auth-gated JSON-RPC/WS only — announce the bind, not a URL. flush:
|
||||
# a piped stdout otherwise surfaces this minutes after the sentinel.
|
||||
|
||||
@@ -216,3 +216,46 @@ def test_ready_sentinel_arrives_on_stdout_not_stderr(tmp_path):
|
||||
proc.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
|
||||
def test_headless_serve_announces_both_ready_tokens(tmp_path):
|
||||
"""#60772 — a headless ``serve`` must announce BOTH ready tokens.
|
||||
|
||||
The packaged Desktop artifact can be older than the Python backend (the
|
||||
CLI-only ``hermes update`` path does not rebuild the packaged app). Its
|
||||
readiness parser may still match only the legacy ``HERMES_DASHBOARD_READY``
|
||||
line; a backend that announces only ``HERMES_BACKEND_READY`` then boots
|
||||
healthily and gets killed after the port-announcement timeout — the exact
|
||||
artifact-skew signature in #60772. The neutral token must come first so
|
||||
current parsers match it before the legacy one.
|
||||
"""
|
||||
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
probe.bind(("127.0.0.1", 0))
|
||||
port = probe.getsockname()[1]
|
||||
probe.close()
|
||||
|
||||
proc = _spawn_serve(port, tmp_path)
|
||||
try:
|
||||
# Read past the NEUTRAL token to the legacy one: _read_until stops at
|
||||
# its token, so waiting for the legacy line proves BOTH were written
|
||||
# (the neutral one precedes it) without a timing race.
|
||||
ready, lines = _read_until(proc, f"HERMES_DASHBOARD_READY port={port}")
|
||||
out = "".join(lines)
|
||||
assert ready, (
|
||||
f"legacy token missing (a stale packaged Desktop would time out); output:\n{out}"
|
||||
)
|
||||
# The neutral token must come first: current parsers stop at their
|
||||
# first hit, and they should bind to the newer contract.
|
||||
assert f"HERMES_BACKEND_READY port={port}" in out
|
||||
backend_at = out.index(f"HERMES_BACKEND_READY port={port}")
|
||||
legacy_at = out.index(f"HERMES_DASHBOARD_READY port={port}")
|
||||
assert backend_at < legacy_at
|
||||
# Exactly one of each — never a loop of announcements.
|
||||
assert out.count("HERMES_BACKEND_READY port=") == 1
|
||||
assert out.count("HERMES_DASHBOARD_READY port=") == 1
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
Reference in New Issue
Block a user