From 6963effed53bcd0b17168475e177a95851102605 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Fri, 25 Sep 2026 17:25:22 +0700 Subject: [PATCH] fix(sandbox): don't crash the desktop when the VM helper is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bun dev:desktop` on a Mac with Swift 6.4 (Command Line Tools 27) opened "A JavaScript error occurred in the main process: spawn .../codev-vmm ENOENT". Two causes: - The vz provider spawned codev-vmm without an "error" listener. A spawn that fails emits "error" and never "exit", so the event was an uncaught exception in Electron's main process — triggered at launch by sandbox autostart, which does not go through availability()'s helper check. start() now fails with "sandbox VM helper is not installed (expected at …)" (or the spawn error) instead. - The helper was never built: Swift 6.4 defaults `swift build` to the swiftbuild backend, which fails on Command Line Tools setups ("Could not initialize build system ... Unknown error parsing property list"), and predev deliberately treats a failed helper build as non-fatal. build-vzhelper.ts now retries a failed build with `--build-system native`, so toolchains whose default works are unaffected. Co-Authored-By: Claude Opus 5.5 --- .../codev-sandbox/script/build-vzhelper.ts | 18 ++++++++++++++++-- packages/codev-sandbox/src/vz/provider.test.ts | 12 ++++++++++++ packages/codev-sandbox/src/vz/provider.ts | 11 +++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/codev-sandbox/script/build-vzhelper.ts b/packages/codev-sandbox/script/build-vzhelper.ts index 95a1ace699..273f9cedd8 100644 --- a/packages/codev-sandbox/script/build-vzhelper.ts +++ b/packages/codev-sandbox/script/build-vzhelper.ts @@ -14,12 +14,12 @@ const universal = process.argv.includes("--universal") const slices = await (async () => { if (!universal) { - await $`swift build -c release --package-path ${root}` + await swiftBuild([]) return [`${root}.build/release/codev-vmm`] } const triples = ["arm64-apple-macosx", "x86_64-apple-macosx"] for (const triple of triples) { - await $`swift build -c release --package-path ${root} --triple ${triple}` + await swiftBuild(["--triple", triple]) } return triples.map((triple) => `${root}.build/${triple}/release/codev-vmm`) })() @@ -31,3 +31,17 @@ else await $`lipo -create ${slices} -output ${root}dist/codev-vmm` // fat Mach-O, and lipo would strip a per-slice signature anyway. await $`codesign --force --sign - --entitlements ${root}vzhelper.entitlements ${root}dist/codev-vmm` console.log(`built ${root}dist/codev-vmm${universal ? " (universal)" : ""}`) + +// Swift 6.4 made the swiftbuild backend the default, and on Command Line +// Tools setups it fails before compiling anything ("Could not initialize +// build system ... Unknown error parsing property list"). The native backend +// still builds this package, so a failed build retries with it: toolchains +// whose default works never see the retry, and the flag stays out of the +// first attempt in case a later Swift drops it. +async function swiftBuild(extra: string[]) { + const args = ["build", "-c", "release", "--package-path", root, ...extra] + const first = await $`swift ${args}`.nothrow() + if (first.exitCode === 0) return + console.warn("swift build failed with the default build system; retrying with --build-system native") + await $`swift ${args} --build-system native` +} diff --git a/packages/codev-sandbox/src/vz/provider.test.ts b/packages/codev-sandbox/src/vz/provider.test.ts index 3b7b686743..503e8a1ec5 100644 --- a/packages/codev-sandbox/src/vz/provider.test.ts +++ b/packages/codev-sandbox/src/vz/provider.test.ts @@ -211,6 +211,18 @@ test("surfaces helper error events with the stderr tail", async () => { await rm(dir, { recursive: true, force: true }) }) +test("fails the start, instead of throwing, when the helper binary is missing", async () => { + const dir = await mkdtemp(join(tmpdir(), "codev-vz-")) + const helperPath = join(dir, "codev-vmm") + // The real spawn: a missing binary emits "error" and never "exit". + const { options, projectDir } = await preparedOptions(dir, FAKE_HELPER, { helperPath, spawnImpl: spawn }) + const provider = createVzProvider(options) + await expect(provider.start({ ...config, projectDir })).rejects.toThrow( + `sandbox VM helper is not installed (expected at ${helperPath})`, + ) + await rm(dir, { recursive: true, force: true }) +}) + test("times out when the helper never reaches running", async () => { const dir = await mkdtemp(join(tmpdir(), "codev-vz-")) const prepared = await preparedOptions(dir, SILENT_HELPER, { startTimeoutMs: 300 }) diff --git a/packages/codev-sandbox/src/vz/provider.ts b/packages/codev-sandbox/src/vz/provider.ts index e3cd349819..27b355c601 100644 --- a/packages/codev-sandbox/src/vz/provider.ts +++ b/packages/codev-sandbox/src/vz/provider.ts @@ -213,6 +213,17 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider { const running = new Promise<"running">((resolve) => { handlers.running = () => resolve("running") }) + // A helper that cannot be spawned (not built, deleted, not executable) + // emits "error" and never "exit". Unhandled, that event is an uncaught + // exception — in the desktop's main process, a crash dialog at launch + // when a sandbox autostarts — so it fails this start instead. + child.once("error", (err: NodeJS.ErrnoException) => { + handlers.error?.( + err.code === "ENOENT" + ? `sandbox VM helper is not installed (expected at ${options.helperPath})` + : `could not run the sandbox VM helper: ${err.message}`, + ) + }) child.stdout?.on( "data", createVzHelperEventReader((event: VzHelperEvent) => {