From 8ad3e7b947b7a2d8d7d09acb0f45e605e986287a Mon Sep 17 00:00:00 2001 From: rendianmeng Date: Fri, 31 Jul 2026 19:05:53 +0800 Subject: [PATCH] feat: win bl update exe file test --- packages/runtime/src/utils/binary-update.ts | 59 +++++++++++++++---- .../tests/binary-update-layout.test.ts | 20 +++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/runtime/src/utils/binary-update.ts b/packages/runtime/src/utils/binary-update.ts index bfc1531..710d247 100644 --- a/packages/runtime/src/utils/binary-update.ts +++ b/packages/runtime/src/utils/binary-update.ts @@ -269,9 +269,26 @@ export async function ensureBinaryPathEntries(version: string): Promise { } } -async function ensureWindowsBinJunction(binDir: string): Promise { +function errnoCode(error: unknown): string { + if (error && typeof error === "object" && "code" in error) { + return String((error as { code?: unknown }).code ?? ""); + } + return ""; +} + +/** + * Ensure `shareRoot/bin` is a junction → `current`. + * + * Install scripts / older layouts may leave a real `bin/` directory with + * `bl.exe` inside. Deleting that directory fails with EACCES while this + * process is the running image — rename-away first (Windows allows that), + * then create the junction. Stale `bin.migrating-*` dirs are best-effort GC. + */ +export async function ensureWindowsBinJunction(binDir: string): Promise { const currentPath = getBinaryCurrentPath(); - const { symlink } = await import("node:fs/promises"); + const { symlink, rename } = await import("node:fs/promises"); + + let migratedAside: string | null = null; try { const stats = await lstat(binDir); @@ -288,18 +305,33 @@ async function ensureWindowsBinJunction(binDir: string): Promise { return; } await unlink(binDir); - await symlink(currentPath, binDir, "junction"); - return; + } else if (stats.isDirectory()) { + // Prefer rename over rm: a running bl.exe inside bin locks delete/rm, + // but rename of the directory usually succeeds on Windows. + migratedAside = `${binDir}.migrating.${process.pid}`; + try { + await rename(binDir, migratedAside); + } catch (renameError) { + // Fallback: empty / unlocked real dirs can still be removed. + try { + await rm(binDir, { recursive: true, force: true }); + migratedAside = null; + } catch (rmError) { + const code = errnoCode(renameError) || errnoCode(rmError) || "EACCES"; + throw new Error( + `Failed to migrate ${binDir} to a junction pointing at current (${code}). ` + + `Close other bl sessions and re-run update, or re-run the install script once.`, + { cause: rmError }, + ); + } + } + } else { + await unlink(binDir).catch(() => rm(binDir, { recursive: true, force: true })); } - // Real directory from older installs: replace with junction to current. - // May fail if a running exe inside bin is locked — surface a clear error. - await rm(binDir, { recursive: true, force: true }); } catch (error) { - const code = - error && typeof error === "object" && "code" in error - ? String((error as { code?: unknown }).code) - : ""; + const code = errnoCode(error); if (code && code !== "ENOENT") { + if (error instanceof Error && error.message.includes("Failed to migrate")) throw error; throw new Error( `Failed to migrate ${binDir} to a junction pointing at current (${code}). ` + `Close other bl sessions and re-run update, or re-run the install script once.`, @@ -310,6 +342,11 @@ async function ensureWindowsBinJunction(binDir: string): Promise { await mkdir(dirname(binDir), { recursive: true }); await symlink(currentPath, binDir, "junction"); + + if (migratedAside) { + // Best-effort: locked exes may keep the aside dir until process exit. + await rm(migratedAside, { recursive: true, force: true }).catch(() => {}); + } } /** diff --git a/packages/runtime/tests/binary-update-layout.test.ts b/packages/runtime/tests/binary-update-layout.test.ts index 210ce8f..7b67fb3 100644 --- a/packages/runtime/tests/binary-update-layout.test.ts +++ b/packages/runtime/tests/binary-update-layout.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { expect, test } from "vite-plus/test"; import { ensureBinaryPathEntries, + ensureWindowsBinJunction, getBinaryBinRoot, getBinaryCurrentPath, getBinaryShareRoot, @@ -143,3 +144,22 @@ test("ensureBinaryPathEntries wires PATH entries through current", async () => { } }); }); + +test("ensureWindowsBinJunction migrates a real bin directory via rename", async () => { + await withTempBinaryRoots(async () => { + seedVersion("3.0.0"); + await switchCurrentToVersion("3.0.0"); + + const binRoot = getBinaryBinRoot(); + mkdirSync(binRoot, { recursive: true }); + writeFileSync(join(binRoot, "bl.exe"), "old-copy"); + + await ensureWindowsBinJunction(binRoot); + + const target = readlinkSync(binRoot); + expect(target.replaceAll("\\", "/").toLowerCase()).toBe( + getBinaryCurrentPath().replaceAll("\\", "/").toLowerCase(), + ); + expect(existsSync(`${binRoot}.migrating.${process.pid}`)).toBe(false); + }); +});