// 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<[NewIPN, Terminate]>((resolve) => { globals[name] = (newIPN: NewIPN, terminate: Terminate) => { delete globals[name] resolve([newIPN, terminate]) } }) go.env[INIT_CALLBACK_ENV] = name // 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] 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 { 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. terminate() await exited.catch(() => {}) throw err } } type NewIPN = (config: IPNConfig) => Promise type Terminate = () => void /** Must match initCallbackEnv in wasm_js.go. */ const INIT_CALLBACK_ENV = "TSCONNECT_INIT_CALLBACK"