From 538b78baa9099972efad283f7b901e2085bd3391 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 23 Apr 2026 13:48:08 +0000 Subject: [PATCH] fix(init): add force-exit safety net for Bun fresh+readline hang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After "Sentry SDK installed successfully!", `sentry init` still hangs until a keypress despite #802/#824/#825/#831. Root cause: a Bun 1.3.11 libuv refcount bug. Opening our fresh /dev/tty ReadStream (the `curl | bash` TTY-delivery workaround) combined with clack's `readline.createInterface(process.stdin)` leaks a libuv handle that NO userland cleanup releases. Verified by a systematic matrix test — process.stdin.{pause,destroy,close}, rl.close(), removeAllListeners, and every combination of the above all leave the event loop ref'd. `process.stdin.unref()` is `undefined` on Bun 1.3.11 (we can't use Node's canonical "let the process exit" escape hatch). The four prior PRs each fixed a legit contributing cause and should stay — #802 and #824 release our own ReadStream, #825 releases keep-alive sockets, #831 transitions stream state to paused. But none address the Bun bug because it's not a userland issue. Fix: restore PR #782's force-exit, wrapped in `setTimeout(..., 100).unref()` so the timer itself doesn't hold the loop. Happy path (loop drains naturally — future Bun fixes, non-TTY flows, `--yes` with no prompts): timer never fires, process exits normally. Bun-bug path: timer fires after 100ms grace — imperceptible to the user, enough for Sentry telemetry + stdio flushes to complete first. Gated on `NODE_ENV !== 'test'` so `bun test` (which sets NODE_ENV=test automatically) doesn't accumulate unref'd timers that would terminate the test runner mid-suite. Manually verified: the production repro (script -q -c "bun …" /dev/null with a real /dev/tty) hangs 7s without this fix, exits in 286ms with it. --- src/commands/init.ts | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index bd43f5a02..bc32b59e8 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -270,8 +270,7 @@ export const initCommand = buildCommand< } // 6. Run the wizard. It owns the temporary `/dev/tty` forwarding used - // for `curl | bash` flows and tears it down on every exit path, so - // init can return naturally once its own refs are released. + // for `curl | bash` flows and tears it down on every exit path. await runWizard({ directory: targetDir, yes: flags.yes, @@ -281,5 +280,41 @@ export const initCommand = buildCommand< org: explicitOrg, project: explicitProject, }); + + // 7. Force-exit safety net for a Bun libuv refcount bug. + // + // On Bun 1.3.11, combining our fresh `/dev/tty` ReadStream (needed for + // the `curl | bash` TTY-delivery workaround in stdin-reopen.ts) with + // clack's internal `readline.createInterface(process.stdin)` leaks a + // libuv handle that NO userland cleanup releases: + // `process.stdin.{pause,destroy,close}`, `rl.close()`, + // `removeAllListeners`, and every combination of the above all leave + // the event loop ref'd. `process.stdin.unref` is `undefined` on Bun. + // The wizard prints "Sentry SDK installed successfully!" and the + // process then hangs until the user presses a key (which wakes libuv). + // + // PRs #802, #824, #825, and #831 each fixed a legit contributing cause + // (ReadStream ref, `using` teardown, MastraClient sockets, stream + // flowing state) and should all stay — but none address the underlying + // Bun refcount bug because that's not a userland issue. PR #782's + // original `process.exit(0)` workaround was removed on the assumption + // that natural drain would work after the other fixes; it doesn't. + // + // Safety net: schedule a force-exit with `.unref()` so the timer + // itself doesn't hold the loop. If the loop drains naturally (future + // Bun versions, non-TTY flows, `--yes` with no prompts), the timer + // never fires and the process exits normally. If the Bun bug bites, + // the timer fires after a 100ms grace period — imperceptible to + // the user and enough for Sentry telemetry + stdio flushes to + // complete first. + // + // Skipped under `bun test` (which sets `NODE_ENV=test`) because the + // test runner calls `initCommand.func` directly; an unref'd timer + // would still fire and terminate the runner mid-suite. + if (process.env.NODE_ENV !== "test") { + setTimeout(() => { + process.exit(process.exitCode ?? 0); + }, 100).unref(); + } }, });