fix(tracing): reject a dashboard viewer whose ui files are gone - #316
fix(tracing): reject a dashboard viewer whose ui files are gone#316arelchan wants to merge 1 commit into
Conversation
The port-reuse guard asked the viewer whether it was alive, not whether it could still serve a page. Both the health payload and the HTML shell are answered from memory, so a viewer whose install directory was removed rather than overwritten -- a switch to an editable install, a rebuild on a different Python version -- keeps reporting itself healthy while every asset read 404s. The launcher reused it and handed the user an unstyled page that never connected, for as long as the process stayed up. Probe an asset the page cannot work without, and let the viewer report the UI as part of its own health so a fresh process is honest about the same condition. Either probe alone accepts a viewer that cannot render. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
|
Claude finished @arelchan's task in 2m 22s —— View job Review: no blockers; this can mergeVerified the two probes actually change the outcome (traced through [nit] Checked and found no issues with:
Branch: |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
What I actually verified
The fix changes the outcome, checked against the real viewer (not only the tests). Copied the viewer out of the tree and ran it under node: with ui/app.js present, /api/health answers 200 {"ok":true,...,"ui":"ok"}; after deleting ui/app.js the same server answers 503 {"ok":false,...,"ui":"missing"} and /app.js returns 404. So both halves bite, and they cover different populations - the server-side uiOk covers viewers started after this lands, and the client-side _VIEWER_UI_PROBE covers the case that motivated the change, an already-running viewer from the old build that keeps answering ok:true from memory forever.
The probe target is the right asset. SHELL_HTML is require('./ui/shell.js') at startup, so the shell is in memory and survives its file being deleted, while serveStatic resolves /app.js under STATIC_DIR per request. ui/shell.js loads exactly /app.js, so the probe asserts the asset the page needs. There is no SPA fallback that would make it vacuously pass: serveStatic special-cases only / and /index.html, everything else 404s when missing.
The test is non-vacuous and nothing was weakened. uv run pytest tests/test_cli_tracing_commands.py -q -> 5 passed, 0 skipped. With raven/cli/tracing_commands.py reverted to main and the new test kept, test_viewer_health_false_when_ui_assets_are_gone fails - so it would catch the regression. test_viewer_health_true_for_our_viewer still asserts True via the default ui_ok=True, so the happy path was not loosened to accommodate the new case.
Backward compatibility. An old CLI against a new server gets an HTTPError from the 503 and returns False, which is the direction you want (do not reuse a UI-less viewer). Nothing in ui/ reads /api/health, so the new 503 has no in-page consumer to break.
Repo rules and packaging. Comments are English and explain a hidden constraint (in-memory shell vs per-request asset read) rather than restating the code, per AGENTS.md 1.1/1.2; branch and commit grammar match 2.1/3.1; the test lands in tests/test_cli_tracing_commands.py, which is the file 5.1 requires for raven/cli/tracing_commands.py. raven/tracing/viewer/**/*.js is in the hatch wheel include list, so app.js ships and a normal install will not trip the new probe.
Covered: the diff, the callers (_open_dashboard reuse and startup-wait paths), AGENTS.md, backward compatibility in both directions, and whether the tests were weakened. One nonblocking note inline, about the message the newly-covered False path lands on.
| with urllib.request.urlopen(f"http://127.0.0.1:{port}{_VIEWER_UI_PROBE}", timeout=0.5) as resp: | ||
| return resp.status == 200 | ||
| except Exception: # noqa: BLE001 — a 404 raises here; either way the UI is gone | ||
| return False |
There was a problem hiding this comment.
Nonblocking, and about the call site rather than this line.
When this returns False for the newly covered reason - our own viewer, assets gone - _open_dashboard falls into the else branch and prints, at raven/cli/tracing_commands.py:141:
Port {port} is held by another process (not the tracing viewer); starting on {free} instead.
In that case it is the tracing viewer, and the message points the user away from the only action that clears it. The stale process keeps holding the default port, so every later raven tracing dashboard walks to a fresh port and the user never learns why.
The two cases are already distinguishable here (health ok but UI probe failed, vs no health at all), so the branch could say something like "a stale tracing viewer is holding {port} with no UI assets; kill it or pass --port". Not a merge blocker - the fallback still hands the user a working dashboard.
Summary
The dashboard opened as unstyled HTML that never connected:
/and/api/healthanswered 200 while/app.cssand/app.jsboth 404'd.The port-reuse guard added in #141 asks the viewer whether it is alive, not whether it can still serve a page. Both the health payload and the HTML shell are produced from memory, while
STATIC_DIRis read per request -- so a viewer whose install directory was removed rather than overwritten keeps reporting itself healthy for as long as the process stays up, and the launcher happily reuses it. Observed after the local install changed form (a switch to an editable install rebuilt the environment on a different Python version, deleting the directory the running viewer had been started from); a plain in-place upgrade overwrites the same path and does not trigger it.Two changes, because either probe alone accepts a viewer that cannot render:
_viewer_healthnow also fetches an asset the page cannot work without, and requires 200. This catches any stale viewer regardless of the code it is running -- including one started before this change./api/healthnow reports the UI as part of health, answering503 {"ok": false, "ui": "missing"}when its static directory no longer holdsapp.js, so a fresh process is honest about the same condition to any other consumer.Type
Verification
The new test is not vacuous -- against the same fake viewer (health 200, assets 404) the previous implementation of
_viewer_healthreturnsTrueand the new one returnsFalse.End-to-end against a real viewer started with
app.jsabsent from its static directory:And against a healthy viewer,
/,/app.css,/app.jsand/api/healthall return 200 with"ui":"ok".Risk
The health endpoint gains a field and can now return 503. The only in-tree consumer is
_viewer_health, which is updated here; an external consumer that treated any 200 as healthy would newly see a 503, which is the intended signal. No change to what the dashboard renders when it is working._VIEWER_UI_PROBEadds one localhost request per launch, on the path already taken only when a port is found occupied.Rollback is reverting the commit: the guard returns to health-only and the endpoint to a bare
ok: true.Related Issues
N/A -- follow-up to #141, which introduced the guard this hardens.