Skip to content

Commit 8ad3e7b

Browse files
committed
feat: win bl update exe file test
1 parent 525412f commit 8ad3e7b

2 files changed

Lines changed: 68 additions & 11 deletions

File tree

packages/runtime/src/utils/binary-update.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,26 @@ export async function ensureBinaryPathEntries(version: string): Promise<void> {
269269
}
270270
}
271271

272-
async function ensureWindowsBinJunction(binDir: string): Promise<void> {
272+
function errnoCode(error: unknown): string {
273+
if (error && typeof error === "object" && "code" in error) {
274+
return String((error as { code?: unknown }).code ?? "");
275+
}
276+
return "";
277+
}
278+
279+
/**
280+
* Ensure `shareRoot/bin` is a junction → `current`.
281+
*
282+
* Install scripts / older layouts may leave a real `bin/` directory with
283+
* `bl.exe` inside. Deleting that directory fails with EACCES while this
284+
* process is the running image — rename-away first (Windows allows that),
285+
* then create the junction. Stale `bin.migrating-*` dirs are best-effort GC.
286+
*/
287+
export async function ensureWindowsBinJunction(binDir: string): Promise<void> {
273288
const currentPath = getBinaryCurrentPath();
274-
const { symlink } = await import("node:fs/promises");
289+
const { symlink, rename } = await import("node:fs/promises");
290+
291+
let migratedAside: string | null = null;
275292

276293
try {
277294
const stats = await lstat(binDir);
@@ -288,18 +305,33 @@ async function ensureWindowsBinJunction(binDir: string): Promise<void> {
288305
return;
289306
}
290307
await unlink(binDir);
291-
await symlink(currentPath, binDir, "junction");
292-
return;
308+
} else if (stats.isDirectory()) {
309+
// Prefer rename over rm: a running bl.exe inside bin locks delete/rm,
310+
// but rename of the directory usually succeeds on Windows.
311+
migratedAside = `${binDir}.migrating.${process.pid}`;
312+
try {
313+
await rename(binDir, migratedAside);
314+
} catch (renameError) {
315+
// Fallback: empty / unlocked real dirs can still be removed.
316+
try {
317+
await rm(binDir, { recursive: true, force: true });
318+
migratedAside = null;
319+
} catch (rmError) {
320+
const code = errnoCode(renameError) || errnoCode(rmError) || "EACCES";
321+
throw new Error(
322+
`Failed to migrate ${binDir} to a junction pointing at current (${code}). ` +
323+
`Close other bl sessions and re-run update, or re-run the install script once.`,
324+
{ cause: rmError },
325+
);
326+
}
327+
}
328+
} else {
329+
await unlink(binDir).catch(() => rm(binDir, { recursive: true, force: true }));
293330
}
294-
// Real directory from older installs: replace with junction to current.
295-
// May fail if a running exe inside bin is locked — surface a clear error.
296-
await rm(binDir, { recursive: true, force: true });
297331
} catch (error) {
298-
const code =
299-
error && typeof error === "object" && "code" in error
300-
? String((error as { code?: unknown }).code)
301-
: "";
332+
const code = errnoCode(error);
302333
if (code && code !== "ENOENT") {
334+
if (error instanceof Error && error.message.includes("Failed to migrate")) throw error;
303335
throw new Error(
304336
`Failed to migrate ${binDir} to a junction pointing at current (${code}). ` +
305337
`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<void> {
310342

311343
await mkdir(dirname(binDir), { recursive: true });
312344
await symlink(currentPath, binDir, "junction");
345+
346+
if (migratedAside) {
347+
// Best-effort: locked exes may keep the aside dir until process exit.
348+
await rm(migratedAside, { recursive: true, force: true }).catch(() => {});
349+
}
313350
}
314351

315352
/**

packages/runtime/tests/binary-update-layout.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from "node:path";
44
import { expect, test } from "vite-plus/test";
55
import {
66
ensureBinaryPathEntries,
7+
ensureWindowsBinJunction,
78
getBinaryBinRoot,
89
getBinaryCurrentPath,
910
getBinaryShareRoot,
@@ -143,3 +144,22 @@ test("ensureBinaryPathEntries wires PATH entries through current", async () => {
143144
}
144145
});
145146
});
147+
148+
test("ensureWindowsBinJunction migrates a real bin directory via rename", async () => {
149+
await withTempBinaryRoots(async () => {
150+
seedVersion("3.0.0");
151+
await switchCurrentToVersion("3.0.0");
152+
153+
const binRoot = getBinaryBinRoot();
154+
mkdirSync(binRoot, { recursive: true });
155+
writeFileSync(join(binRoot, "bl.exe"), "old-copy");
156+
157+
await ensureWindowsBinJunction(binRoot);
158+
159+
const target = readlinkSync(binRoot);
160+
expect(target.replaceAll("\\", "/").toLowerCase()).toBe(
161+
getBinaryCurrentPath().replaceAll("\\", "/").toLowerCase(),
162+
);
163+
expect(existsSync(`${binRoot}.migrating.${process.pid}`)).toBe(false);
164+
});
165+
});

0 commit comments

Comments
 (0)