Skip to content

fix: use process.exitCode instead of process.exit() in cli entry - #50

Closed
jutaz wants to merge 3 commits into
mainfrom
bugfix/windows-crash
Closed

jutaz wants to merge 3 commits into
mainfrom
bugfix/windows-crash

Conversation

@jutaz

@jutaz jutaz commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Skeleton PR for visibility — this branch was unmerged with no open PR.

Commits vs main:

160e03a fix: use process.exitCode instead of process.exit() in cli entry
de7af85 test: instrument cli subprocess runs to diagnose windows crash
1f1c9a3 test: skip windows-specific crash on strict-without-sum-file

jutaz added 3 commits July 4, 2026 12:13
The 'exits 2 for --strict without a sum file' subprocess test crashes
reproducibly on Node 24 + windows-latest CI with exit code 0xC0000135
(3221226505, a Windows loader status that Node reports when a child
process terminates abnormally). The retry: 1 config added in the last
commit doesn't help; both attempts fail identically. The crash cannot
be reproduced on macOS (Node 26) or on any other test in the same
file, and only shows up on the specific 'strict + no sum file' code
path.

Every other --strict test on Windows passes, including the parallel
'--strict=warn reports stale but exits 0; --strict fails' case which
runs the same applyStrict entry point with a sum file present. The
'exits 2 without a sum file' path is a thin sum-file-null branch that
throws UsageError.

Rather than dig further into what looks like a Windows subprocess /
tree-sitter WASM interaction bug outside our control, skip the one
test on win32. The behavior is verified on Linux (Node 20, 22, 24)
and macOS (Node 24). Also removed the retry: 1 config since it did
not help this failure and offers no benefit for the current suite.
Reverting the skipIf and adding real instrumentation instead. The
Windows CI job returned exit code 3221226505 (0xC0000135) for one
specific test with no other information: no stderr, no stdout, no
signal, no stack. Skipping the test hides the bug. Instrumenting
should surface it.

Two changes to the run() helper:

1. Node debug flags forwarded to the subprocess:
     --trace-uncaught         stack for uncaught exceptions before exit
     --trace-warnings         stack for warnings that arrive without one
     --unhandled-rejections=strict  turn any unhandled rejection into
                              an uncaught exception (which then gets
                              the trace above)

   These make silent crashes talk. Kept always-on: they cost nothing
   on the happy path and light up CI-only failures.

2. On unexpected exit (code not in 0/1/2), dump the subprocess's
   stdout, stderr, and signal to console.error so the CI log carries
   the full picture. The current test only surfaces the assertion
   failure, not what the process managed to print before dying.

If the crash is caused by an unhandled rejection in the check-strict
path (likely candidate: a grammar-load failure racing with
applyStrict throwing UsageError, so both go up at the same time and
one is lost), --unhandled-rejections=strict + --trace-uncaught will
print the stack in the failed-attempt log. If it is genuinely a
Windows loader bug at process startup, the dumped output will show
what got printed before the loader gave up.

Local run: 216/216 pass in 10.4s wall time. The debug flags do not
change behavior on the happy path.
Root cause of the Windows CI crash found. The instrumentation from
de7af85 caught the crucial stderr line I couldn't see before:

    --strict requires symtether.sum. Run `symtether update` first
    Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
    file src\\win\\async.c, line 94

The CLI printed its error correctly. What crashed the process was
libuv's teardown assertion, not the user code: something scheduled
work on an async handle while another async handle was already in
the closing state.

The specific race: check --strict fires all grammar-load Promises
via loadDocs (18 tree-sitter WASMs), then reads the sum file. When
the sum file is missing, applyStrict throws UsageError while the
grammar-load Promises are still resolving. cli.ts's top-level catch
calls process.exit(2), which tears down the libuv loop synchronously,
which on Windows asserts because the WASM cleanup handles are still
mid-close. On Linux and macOS the same race is tolerated silently.

The exit-code path in cli.ts was two synchronous process.exit() calls
in the top-level catch. Replaced both with process.exitCode +
natural loop drain (the Node.js recommendation for exactly this
class of bug). Now:

  process.exitCode = 0 / 2      // signal only
  fall through, let the loop drain, Node exits with that code

The --strict=warn sibling test passes on Windows because it runs to
completion before exiting (no throw, no premature teardown). The
newly-failing test throws mid-flight, which is why it hit this and
--strict=warn did not.

No other process.exit() calls remain in src/. Verified locally:
216/216 pass on Node 26 + macOS.

Refs: https://nodejs.org/api/process.html#processexitcode_1
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
symtether 160e03a Commit Preview URL

Branch Preview URL
Aug 09 2026, 05:01 PM

@jutaz

jutaz commented Sep 21, 2026

Copy link
Copy Markdown
Owner Author

Already in main — this landed as #8 via squash-merge, which rewrote the commit SHA so GitHub never auto-closed this one. Verified the content is present rather than going by the title. Closing as bookkeeping; no work is lost.

@jutaz jutaz closed this Sep 21, 2026
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