From 738fea52f834e14896456f7822139b62f237aab2 Mon Sep 17 00:00:00 2001 From: Codinget Date: Sun, 30 Aug 2026 01:27:21 +0000 Subject: [PATCH] fix(tsconnect): stop reporting a deliberate shutdown as a panic The exit handler called onExit("Unexpected shutdown") whenever the Go runtime exited. That was upstream's wording from when nothing could stop the runtime, so every exit really was a panic. This fork added shutdown(), so a clean teardown now reports itself as a crash to the panic handler createIPN() hands to its callers. Split the two cases. Before the IPN reaches the caller an exit is a startup failure, and rejecting hands it back as an error rather than as a side-channel callback. After that the caller holds the only shutdown path, so report the exit without claiming it was unexpected. Found in review of webnet/webnet#188. Co-Authored-By: claude-opus-5 --- cmd/tsconnect/src/lib/start-ipn.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/cmd/tsconnect/src/lib/start-ipn.ts b/cmd/tsconnect/src/lib/start-ipn.ts index 9b364548e..378297f5e 100644 --- a/cmd/tsconnect/src/lib/start-ipn.ts +++ b/cmd/tsconnect/src/lib/start-ipn.ts @@ -27,15 +27,29 @@ export async function startIPN( }) go.env[INIT_CALLBACK_ENV] = name - const exited = go.run(instance).then(() => { + + // An exit before the IPN reaches the caller is a startup failure, and throwing + // hands it back as a rejection. Afterwards the caller holds the only shutdown + // path, so an exit is either that shutdown or a panic; report it either way, + // because the IPN is dead in both cases. + let handedOver = false + const exited: Promise = go.run(instance).then(() => { delete globals[name] - onExit("Unexpected shutdown") - throw new Error("Go runtime exited before the IPN was ready") + if (handedOver) onExit("Go runtime exited") + // Always reject: before the handover this is what fails the race below, + // and after it the race has settled, so nothing observes the rejection. + throw new Error( + handedOver + ? "Go runtime exited" + : "Go runtime exited before the IPN was ready" + ) }) const [newIPN, terminate] = await Promise.race([ready, exited]) try { - return await newIPN(config) + const ipn = await newIPN(config) + handedOver = true + return ipn } catch (err) { // Nothing was built, so nothing can shut the runtime down. Exit it here and // wait for it, or the page keeps a blocked runtime for a failed startup.