Skip to content

fix(cli): don't hard-exit download:plugins after fetching - #17896

Open
dr14-make wants to merge 1 commit into
eclipse-theia:masterfrom
dr14-make:fix/cli-download-plugins-hard-exit
Open

fix(cli): don't hard-exit download:plugins after fetching#17896
dr14-make wants to merge 1 commit into
eclipse-theia:masterfrom
dr14-make:fix/cli-download-plugins-hard-exit

Conversation

@dr14-make

@dr14-make dr14-make commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What it does

theia download:plugins calls process.exit() on both its success and failure paths, the moment downloadPlugins settles. On Windows that aborts the process rather than exiting it:

Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76
[ELIFECYCLE] Command failed with exit code 3221226505.

3221226505 is 0xC0000409, the Windows fast-fail signature. Node calls uv_async_send on a handle uv_close has already flagged UV_HANDLE_CLOSING while undici tears down the sockets the OVSX requests ran on, and libuv asserts. This is nodejs/node#56645, fixed by nodejs/node#61999 — merged to main on 2026-07-24, but in no released Node version at the time of writing (no 61999 in CHANGELOG_V24/V25/V26, and it is not an ancestor of v24.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:plugins whose 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.exitCode and 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:

  1. Create a scratch directory with a package.json declaring theiaPluginsDir and a couple of theiaPlugins entries.
  2. Run theia download:plugins there, cold (empty plugins dir) and again warm (everything already downloaded).

Measured on Node 24.15.0, Linux x64, with this branch built:

Path Result
Cold, 2 plugins over the network exits 0 in 4.5s, both plugins present
Fully cached exits 0 in under 1s
Download failure (bogus plugin URL) exits 1, error printed

On 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.exit immediately after fetch) 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

  • This PR introduces breaking changes and requires careful review. If yes, the breaking changes section in the changelog has been updated.

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

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>
@github-project-automation github-project-automation Bot moved this to Waiting on reviewers in PR Backlog Aug 5, 2026
@dr14-make

Copy link
Copy Markdown
Contributor Author

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 theia download:plugins 40 times stock, then applied this change to the installed @theia/cli and ran it 40 more times, within a single job on a single windows-2022 runner (Node 24.15.0), across four parallel shards. Every iteration took the all-cached path, where the command exits ~2s after its last request:

shard stock patched Fisher exact
1 0/40 0/40 1
2 12/40 0/40 1.9e-4
3 40/40 0/40 1.8e-15
4 37/40 0/40 8.5e-17
pooled 89/160 0/160 1.0e-15

Every stock abort printed the same Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76. Stock and patched ran on the same machine within each job, so runner variation cannot account for the difference.

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:

path stock patched
cold, 2 plugins over the network 5.86s, exit 0 5.79s, exit 0
fully cached 1.11s, exit 0 0.77s, exit 0
failed download exit 1

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting on reviewers

Development

Successfully merging this pull request may close these issues.

1 participant