fix(cli): don't hard-exit download:plugins after fetching - #17896
fix(cli): don't hard-exit download:plugins after fetching#17896dr14-make wants to merge 1 commit into
download:plugins after fetching#17896Conversation
The `download:plugins` handler called `process.exit()` on both its success and
failure paths, as soon as `downloadPlugins` settled. On Windows that aborts the
process outright: Node calls `uv_async_send` on an already-closing handle while
undici tears down the sockets the OVSX requests ran on, libuv asserts, and the
process fast-fails.
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76
The process dies with exit code 3221226505 (0xC0000409) after every plugin has
been resolved and downloaded, so the work has succeeded and only the teardown
fails. It is a race, so it reproduces intermittently — in one downstream CI the
Windows packaging leg aborted on 4 of 6 runs, each time at the end of a
`download:plugins` that had just reported every plugin as already downloaded.
This is nodejs/node#56645, fixed upstream by nodejs/node#61999 but not present
in any released Node version yet. It affects Node 23 and later; the same
downstream job ran 36 Windows legs on Node 22 without a single abort.
Set `process.exitCode` and let the event loop drain instead, matching what the
`.fail()` handler in this file already does. The command still exits promptly:
on Node 24.15.0 a cold run downloading two plugins takes 4.5s and exits 0, and a
fully cached run exits in under a second, so the hard exit was not holding the
process open.
Signed-off-by: Dmitrij Rozdestvensky <dmitrij.rozdestvensky@juliahub.com>
|
Windows verification, which the PR description flagged as the missing piece — the abort reproduces only intermittently, so a handful of green runs would not have been evidence. I ran
Every stock abort printed the same Shard 1 is worth calling out for anyone trying to reproduce this: that runner aborted 0 times in 40 iterations, while shard 3 aborted in all 40 — same image, same Node, same command. Whether the process loses the race appears to be close to a property of the individual machine, so a Windows CI leg passing on this PR would not by itself demonstrate anything. The command still terminates promptly and reports correctly with the hard exit gone, which was the other thing worth checking before removing it:
Downstream this was failing a Windows packaging job on roughly two runs in three, with the abort landing after every plugin had been resolved and skipped as already downloaded. |
What it does
theia download:pluginscallsprocess.exit()on both its success and failure paths, the momentdownloadPluginssettles. On Windows that aborts the process rather than exiting it:3221226505is0xC0000409, the Windows fast-fail signature. Node callsuv_async_sendon a handleuv_closehas already flaggedUV_HANDLE_CLOSINGwhile undici tears down the sockets the OVSX requests ran on, and libuv asserts. This is nodejs/node#56645, fixed by nodejs/node#61999 — merged tomainon 2026-07-24, but in no released Node version at the time of writing (no61999inCHANGELOG_V24/V25/V26, and it is not an ancestor ofv24.19.0).Because it is a race between the exit and the socket teardown, it is intermittent, and it fires after every plugin has resolved and downloaded — the command has done its work and only the teardown fails. In a downstream CI job the Windows packaging leg aborted on 4 of 6 runs on Node 24.15.0, every time at the tail of a
download:pluginswhose plugins were all already downloaded. The same job ran 36 Windows legs on Node 22.23.2 with zero aborts, which matches the upstream report that Node 23 is where this starts.Node 24 is what Electron 42 embeds, so adopting a current Electron makes this reachable for any downstream that packages on Windows.
The fix sets
process.exitCodeand lets the event loop drain, which is what the.fail()handler further down this same file already does. Nothing here needs a hard exit: the command exits on its own once its work is done.How to test
On any platform, to confirm the command still terminates promptly and reports the right code — the only real risk in dropping a hard exit is that something keeps the loop alive and the command hangs instead:
package.jsondeclaringtheiaPluginsDirand a couple oftheiaPluginsentries.theia download:pluginsthere, cold (empty plugins dir) and again warm (everything already downloaded).Measured on Node 24.15.0, Linux x64, with this branch built:
0in 4.5s, both plugins present0in under 1s1, error printedOn Windows with Node 23+, the abort itself reproduces only intermittently, so absence over a handful of runs is weak evidence either way — the downstream sample above needed six runs to show four aborts.
Follow-ups
Once a Node release carries nodejs/node#61999, the underlying fault is gone and this change is no longer load-bearing on Windows. It is worth keeping regardless: a CLI command that has finished its work has no reason to hard-exit, and the same pattern (
process.exitimmediately afterfetch) appears elsewhere in this file's commands, which may deserve the same treatment if they turn out to be reachable after network I/O.Breaking changes
Attribution
This contribution was drafted with AI assistance (Claude Code), working under my direction. I set the investigation, reviewed the change, and ran and checked the verification reported above; the measurements in the table are from runs I monitored, not model output taken on trust. The sign-off, and responsibility for the contribution, are mine.
Review checklist
Reminder for reviewers