fix(uninstall): spawn windows cleanup helper via cmd start trampoline#248
Conversation
On Windows, `oo uninstall` left the running executable behind. The
post-exit cleanup helper was launched with `Bun.spawn({ detached: true })`
+ `unref()`, but on Windows Bun places spawned children in a kill-on-close
job object and `detached` (UV_PROCESS_DETACHED) does not break the child
out of it. The helper was killed the instant the main process exited —
before it could unlink the running image — leaving `oo.exe` on disk.
Launch the helper through a `cmd /c start "" /b` trampoline instead, which
creates it as a process that breaks away from the job object and outlives
the parent. Await the bootstrap `cmd` (rather than detached + unref) so
`start` has launched the helper before we exit, closing the race where the
job could close and kill `cmd` before `start` ran. The generated
self-delete script is unchanged.
Update the Windows unit assertion for the new command shape, and add a
real (non-mock) win32 integration test: a child process runs
`performSelfUninstall`, exits, and the deferred executable must then be
removed by the helper — covering the real spawn path that was previously
exercised only through a mock.
The job-object behavior reproduces on bun 1.3.14 and 1.4.0-canary.1 (the
Rust rewrite), so this is a deliberate long-term workaround rather than a
version-specific patch.
Repro: https://github.com/BlackHole1/bun-detached-windows-bug
Signed-off-by: Kevin Cui <bh@bugs.cc>
Summary by CodeRabbitRelease Notes
WalkthroughThis PR refactors the Windows self-uninstall helper spawning mechanism. The implementation in 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/application/self-update/uninstall.ts`:
- Around line 498-511: The current awaiting of the Bun.spawn trampoline uses
await subprocess.exited but doesn't check the returned exit code; capture the
resolved exit code from subprocess.exited (e.g., const exitCode = await
subprocess.exited) and if it's non-zero throw an Error with a clear message that
includes the exit code and context (referencing the subprocess variable and the
Bun.spawn call that invoked the Windows bootstrap) so failures of the `cmd /c
start ...` bootstrap fail fast instead of being treated as success.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e1efa7da-433d-45be-adeb-1f4160992713
📒 Files selected for processing (2)
src/application/self-update/uninstall.test.tssrc/application/self-update/uninstall.ts
On Windows,
oo uninstallleft the running executable behind. The post-exit cleanup helper was launched withBun.spawn({ detached: true })unref(), but on Windows Bun places spawned children in a kill-on-close job object anddetached(UV_PROCESS_DETACHED) does not break the child out of it. The helper was killed the instant the main process exited — before it could unlink the running image — leavingoo.exeon disk.Launch the helper through a
cmd /c start "" /btrampoline instead, which creates it as a process that breaks away from the job object and outlives the parent. Await the bootstrapcmd(rather than detached + unref) sostarthas launched the helper before we exit, closing the race where the job could close and killcmdbeforestartran. The generated self-delete script is unchanged.Update the Windows unit assertion for the new command shape, and add a real (non-mock) win32 integration test: a child process runs
performSelfUninstall, exits, and the deferred executable must then be removed by the helper — covering the real spawn path that was previously exercised only through a mock.The job-object behavior reproduces on bun 1.3.14 and 1.4.0-canary.1 (the Rust rewrite), so this is a deliberate long-term workaround rather than a version-specific patch.
Repro: https://github.com/BlackHole1/bun-detached-windows-bug
Issue: oven-sh/bun#31603