diff --git a/cmd/tsconnect/src/app/index.ts b/cmd/tsconnect/src/app/index.ts index bdbcaf3e5..aec47dbea 100644 --- a/cmd/tsconnect/src/app/index.ts +++ b/cmd/tsconnect/src/app/index.ts @@ -5,6 +5,7 @@ import "../wasm_exec" import wasmUrl from "./main.wasm" import { sessionStateStorage } from "../lib/js-state-store" import { renderApp } from "./app" +import { startIPN } from "../lib/start-ipn" async function main() { const app = await renderApp() @@ -13,23 +14,25 @@ async function main() { fetch(`./dist/${wasmUrl}`), go.importObject ) - // The Go process should never exit, if it does then it's an unhandled panic. - go.run(wasmInstance.instance).then(() => - app.handleGoPanic("Unexpected shutdown") - ) const params = new URLSearchParams(window.location.search) const authKey = params.get("authkey") ?? undefined - const ipn = newIPN({ - // Persist IPN state in sessionStorage in development, so that we don't need - // to re-authorize every time we reload the page. - stateStorage: DEBUG ? sessionStateStorage : undefined, - // authKey allows for an auth key to be - // specified as a url param which automatically - // authorizes the client for use. - authKey: DEBUG ? authKey : undefined, - }) + // The Go process should never exit, if it does then it's an unhandled panic. + const ipn = await startIPN( + go, + wasmInstance.instance, + { + // Persist IPN state in sessionStorage in development, so that we don't + // need to re-authorize every time we reload the page. + stateStorage: DEBUG ? sessionStateStorage : undefined, + // authKey allows for an auth key to be + // specified as a url param which automatically + // authorizes the client for use. + authKey: DEBUG ? authKey : undefined, + }, + (reason) => app.handleGoPanic(reason) + ) app.runWithIPN(ipn) } diff --git a/cmd/tsconnect/src/lib/start-ipn.ts b/cmd/tsconnect/src/lib/start-ipn.ts new file mode 100644 index 000000000..a9b7666ba --- /dev/null +++ b/cmd/tsconnect/src/lib/start-ipn.ts @@ -0,0 +1,43 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +/** + * Starts a Go runtime and returns the single IPN it owns. + * + * The runtime does not publish its bridge on a global. It reads the name of a + * callback from its environment and invokes it once the bridge is ready, so + * this resolves on an explicit signal rather than on the Go scheduler having + * run far enough. The name is generated per runtime, so several runtimes can + * start in one page without racing each other. + */ +export async function startIPN( + go: Go, + instance: WebAssembly.Instance, + config: IPNConfig, + onExit: (reason: string) => void +): Promise { + const name = `__tsconnectInit_${Math.random().toString(36).slice(2)}` + const globals = globalThis as Record + + const ready = new Promise<(config: IPNConfig) => Promise>( + (resolve) => { + globals[name] = (newIPN: (config: IPNConfig) => Promise) => { + delete globals[name] + resolve(newIPN) + } + } + ) + + go.env[INIT_CALLBACK_ENV] = name + const exited = go.run(instance).then(() => { + delete globals[name] + onExit("Unexpected shutdown") + throw new Error("Go runtime exited before the IPN was ready") + }) + + const newIPN = await Promise.race([ready, exited]) + return newIPN(config) +} + +/** Must match initCallbackEnv in wasm_js.go. */ +const INIT_CALLBACK_ENV = "TSCONNECT_INIT_CALLBACK" diff --git a/cmd/tsconnect/src/pkg/pkg.ts b/cmd/tsconnect/src/pkg/pkg.ts index a44c57150..b28bb2bc3 100644 --- a/cmd/tsconnect/src/pkg/pkg.ts +++ b/cmd/tsconnect/src/pkg/pkg.ts @@ -7,6 +7,7 @@ /// import "../wasm_exec" +import { startIPN } from "../lib/start-ipn" import wasmURL from "./main.wasm" /** @@ -30,11 +31,7 @@ export async function createIPN(config: IPNPackageConfig): Promise { go.importObject ) // The Go process should never exit, if it does then it's an unhandled panic. - go.run(wasmInstance.instance).then(() => - config.panicHandler("Unexpected shutdown") - ) - - return newIPN(config) + return startIPN(go, wasmInstance.instance, config, config.panicHandler) } export { runSSHSession } from "../lib/ssh" diff --git a/cmd/tsconnect/src/types/wasm_js.d.ts b/cmd/tsconnect/src/types/wasm_js.d.ts index 938ec759c..629cf2869 100644 --- a/cmd/tsconnect/src/types/wasm_js.d.ts +++ b/cmd/tsconnect/src/types/wasm_js.d.ts @@ -7,8 +7,6 @@ */ declare global { - function newIPN(config: IPNConfig): IPN - interface IPN { run(callbacks: IPNCallbacks): void login(): void