Conversation
A JS process extension runs in a worker thread kept warm between runs, and run() only listened for the worker's 'done'/'error' messages. If the worker died mid-run (an uncaught error outside the awaited processor call, running out of memory on a large mesh, process.exit) neither message ever arrived: the workflow waited on that node forever, and because the dead worker stayed cached, every later run of the node hung the same way until the app was restarted. Listen for the worker's 'error' and 'exit' events for the duration of a run, reject with the cause, and drop the dead worker so the next run starts a fresh one. Errors thrown by the processor itself are still reported through its 'error' message and keep the warm worker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
If the worker thread behind a JS process node dies in the middle of a run, the workflow waits on that node forever. Every later run of the same node hangs the same way until the app is restarted. Ways the worker can die include:
errorevent or a timer callback;process.exit().No error is shown and the run never finishes.
Why it triggers
ProcessRunnerkeeps one warm worker per extension.run()only listens for the worker's'message'events and waits for adoneorerrormessage:A worker that dies never posts either message, so the promise never settles.
extensions:runProcesshas no timeout, so the workflow'sawait window.electron.extensions.runProcess(...)never returns. The dead worker also stays cached withready = true, so the next run skipsensureReady()and posts into a terminated worker. That run hangs too.Fix
For the duration of a run, also listen for the worker's
'error'and'exit'events. When either fires, reject the run with the cause (the error itself, orProcess extension worker exited with code N) and drop the dead worker, so the next run starts a fresh one. All three listeners are removed once the run settles.Errors thrown by the processor itself are unchanged. The worker still catches them and posts an
errormessage, and the warm worker is kept.This touches a different part of
process-runner.tsthan #338, which changes the runner registry.git merge-treemerges the two branches cleanly, and both new test files pass on the merged result (6/6).Verification
New
electron/main/process-runner-worker-exit.test.mjs, esbuild-bundled like the otherelectron/maintests. It drivesProcessRunnerwith a small processor whose behaviour depends onparams.mode.a run whose worker crashes rejects instead of hanging: an uncaught error thrown from a timer. Fails before (times out), passes after.after its worker exits, the runner starts a fresh one for the next run:process.exit(3), then a normal run gets a fresh worker (run counter1). Fails before (times out), passes after.an error thrown by the processor still rejects with its message and keeps the warm worker: passes before and after. The run rejects withError: bad input, and the next run lands on the same worker (run counter2). That proves the change doesn't widen into tearing the worker down on ordinary processor errors.Fail-before output, with the source reverted and the test kept. It is verbatim:
After the fix:
Whole suite (
npm run test:node), unmodifieddevcompared with this branch:dev*.test.mjsnpx eslint .npx tsc --noEmit -p tsconfig.node.jsonThe 5 skips were already there on
dev. The 9tscerrors are also pre-existing, and none of them are inprocess-runner.ts. No Python touched.🤖 Generated with Claude Code