Skip to content

cq-viewer 1.1.1: fix concurrent ~/.cqview writes and Python after dispose (review of #18) - #20

Merged
HanZijie merged 1 commit into
mainfrom
fix/cq-viewer-review
Oct 15, 2026
Merged

HanZijie merged 1 commit into
mainfrom
fix/cq-viewer-review

Conversation

@HanZijie

@HanZijie HanZijie commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to @MyPrototypeWhat's post-merge review of #18 — thanks for the careful testing. Both required changes are fixed and the three optional suggestions are done too. Version 1.1.1.

Required

1. Concurrent writes to ~/.cqview

  • writeJsonAtomic names temp files <file>.<pid>.<randomUUID>.tmp, removes the temp file on failure, and gives it the existing file's mode before the rename (docs/security.md).
  • The selection save now goes through the same write queue (serial()) as roots.json, the scene copy, and the request log. (addRoot's read-modify-write was already queued in 1.1.0 / cq-viewer 1.1.0: MCP tools to add model folders and files, bundled CadQuery skill #19, which is why the roots part of the report no longer reproduced on main; the temp-name collision still did.)
  • Reproduced on main before the fix: 40 concurrent POST /api/selection → 27 × 500. After: 40 × 200; 8 concurrent folder registrations, page and MCP mixed → 8 × 200, all 8 in roots.json.

2. No Python after dispose

  • runChild rejects with disposed once dispose() has run, and a child that exits because dispose killed it reports disposed too, so the fallback cannot start.
  • No fallback when the conversion timed out (worst case is now 1 × 300 s), was stopped by dispose, or was killed with SIGKILL/SIGTERM.
  • One deliberate difference from the suggestion: a native crash (SIGSEGV, SIGABRT, …) of step_mesh.py still tries the simpler inline fallback, since that is the case the fallback exists for (the XCAF path crashing on a file that plain importStep handles). Happy to drop that if you prefer the stricter rule.
  • Also: the Python probe stops once disposed, watchers are not recreated by a request racing with dispose, and dispose waits for a write already in progress.

Optional suggestions

  1. Host / Origin: every route, the page included, now refuses a Host that is not 127.0.0.1 / localhost / ::1, and an Origin that is not this listener itself (loopback host and our port). Verified inside MiniMax Code: the page opened with ?root= still reads and registers folders, and its SSE connection stays up (viewer_connected: true).
  2. step-cache: pruned on the first conversion after each start — entries unused for 30 days (a cache hit refreshes the mtime), then least recently used beyond 1 GB, plus temp files older than a day. Only files matching the cache's own name pattern are touched.
  3. Versions: host-bridge.js APP_INFO and the MCP serverInfo follow plugin.json (1.1.1); a test fails if they drift.

Checks

  • npm run check: plugins/hanzijie/cq-viewer: OK — 0 errors, 0 warnings; 55/55 repo tests.
  • node --test plugins/hanzijie/cq-viewer/tests/*.test.mjs: 19/19. New tests/server.test.mjs (9 tests): concurrency (above); with a fake python via CQVIEW_PYTHON: dispose mid-conversion kills step_mesh.py (pid gone) and no fallback runs within 1.5 s, a SIGKILLed conversion returns 422 without fallback, a SIGSEGV one does fall back; cache pruning; 403 for foreign Host/Origin on the page, API, and MCP; version sync. The fake-Python tests are skipped on Windows.
  • Real CadQuery 2.5.2: a coloured two-part assembly still converts through step_mesh.py (cqvm, 1.2 s) and the second request is a cache hit.
  • Headless Chrome E2E from cq-viewer 1.1.0: MCP tools to add model folders and files, bundled CadQuery skill #19 (13 checks, page against the real Node entry): all pass under the new Origin check.

Not verified

  • Windows (the cache pruning unlink of a file being streamed is caught, but not exercised there).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

… loopback-only routes

Follow-up to the post-merge review of #18.

Required:
- Concurrent writes: temp files are named <file>.<pid>.<uuid>.tmp (two writes in the
  same millisecond collided before: 40 concurrent selection saves gave 27x 500), and the
  selection save joins the existing write queue with roots/scene/request writes. The temp
  file is removed on failure and takes the existing file's mode (docs/security.md).
- dispose during STEP conversion: runChild refuses once disposed, so the fallback can no
  longer start a Python nobody cleans up. A conversion stopped by the time limit, by
  dispose, or killed with SIGKILL/SIGTERM fails without the fallback (worst case is now
  1x300 s, not 2x). A native crash (SIGSEGV, SIGABRT ...) still tries the simpler
  fallback, which is what it is for. Watchers are not recreated after dispose, and
  dispose waits for a write already in progress.

Optional suggestions, also done:
- Every route (page included) refuses a non-loopback Host and an Origin other than this
  listener (DNS rebinding could register folders and read models before).
- step-cache pruning on the first conversion after start: unused for 30 days, beyond
  1 GB (LRU, a hit refreshes mtime), temp files older than a day; only its own files.
- host-bridge APP_INFO and MCP serverInfo versions follow plugin.json (test checks).

tests/server.test.mjs (9 tests, fake Python for the conversion paths); 19/19 plugin tests.
npm run check: plugins/hanzijie/cq-viewer OK (0 errors, 0 warnings).
@HanZijie
HanZijie merged commit f241a69 into main Oct 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant