Skip to content

Commit 17441e6

Browse files
committed
fix(engine): a rename Windows keeps refusing falls back to a copy so the audio mix finishes
1 parent 25ee3a3 commit 17441e6

2 files changed

Lines changed: 58 additions & 6 deletions

File tree

‎packages/engine/src/utils/renameWithRetry.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,26 @@ describe("renameWithRetry", () => {
4545
await renameWithRetryAsync("a", "b", { platform: "win32", rename: async () => rename.fn() });
4646
expect(rename.calls()).toBe(3);
4747
});
48+
49+
it("copies when win32 keeps refusing the rename, and fails with the rename error if the copy fails too", async () => {
50+
const copied: string[] = [];
51+
renameWithRetry("a", "b", {
52+
platform: "win32",
53+
rename: failing("EPERM", 99).fn,
54+
copy: (from, to) => void copied.push(`${from}>${to}`),
55+
});
56+
expect(copied).toEqual(["a>b"]);
57+
await renameWithRetryAsync("a", "c", {
58+
platform: "win32",
59+
rename: async () => failing("EPERM", 99).fn(),
60+
copy: async (from, to) => void copied.push(`${from}>${to}`),
61+
});
62+
expect(copied).toEqual(["a>b", "a>c"]);
63+
const broken = () => {
64+
throw new Error("copy denied");
65+
};
66+
expect(() =>
67+
renameWithRetry("a", "b", { platform: "win32", rename: failing("EPERM", 99).fn, copy: broken }),
68+
).toThrow(/EPERM/);
69+
});
4870
});

‎packages/engine/src/utils/renameWithRetry.ts‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { renameSync } from "node:fs";
2-
import { rename } from "node:fs/promises";
1+
import { copyFileSync, renameSync } from "node:fs";
2+
import { copyFile, rename } from "node:fs/promises";
3+
import { removeScratch } from "./removeScratch.js";
34

45
// Windows reports EPERM/EACCES/EBUSY while antivirus or a just-exited process still holds the
56
// file; the hold clears within milliseconds. Total wait before giving up: about 1.3 s.
@@ -10,6 +11,9 @@ interface Options {
1011
platform?: NodeJS.Platform;
1112
}
1213

14+
// A folder that denies delete (locked-down TEMP) refuses every rename; copying the bytes still works.
15+
// The source is left behind when it cannot be removed.
16+
1317
function isRetryable(error: unknown, platform: NodeJS.Platform): boolean {
1418
if (platform !== "win32") return false;
1519
return RETRYABLE.has((error as NodeJS.ErrnoException | undefined)?.code ?? "");
@@ -23,16 +27,29 @@ function sleepSync(ms: number): void {
2327
export function renameWithRetry(
2428
from: string,
2529
to: string,
26-
options: Options & { rename?: (from: string, to: string) => void } = {},
30+
options: Options & {
31+
rename?: (from: string, to: string) => void;
32+
copy?: (from: string, to: string) => void;
33+
} = {},
2734
): void {
2835
const doRename = options.rename ?? renameSync;
36+
const doCopy = options.copy ?? copyFileSync;
2937
const platform = options.platform ?? process.platform;
3038
for (let attempt = 0; ; attempt++) {
3139
try {
3240
doRename(from, to);
3341
return;
3442
} catch (error) {
35-
if (attempt >= BACKOFF_MS.length || !isRetryable(error, platform)) throw error;
43+
if (!isRetryable(error, platform)) throw error;
44+
if (attempt >= BACKOFF_MS.length) {
45+
try {
46+
doCopy(from, to);
47+
} catch {
48+
throw error;
49+
}
50+
removeScratch(from);
51+
return;
52+
}
3653
sleepSync(BACKOFF_MS[attempt]!);
3754
}
3855
}
@@ -42,16 +59,29 @@ export function renameWithRetry(
4259
export async function renameWithRetryAsync(
4360
from: string,
4461
to: string,
45-
options: Options & { rename?: (from: string, to: string) => Promise<void> } = {},
62+
options: Options & {
63+
rename?: (from: string, to: string) => Promise<void>;
64+
copy?: (from: string, to: string) => Promise<void>;
65+
} = {},
4666
): Promise<void> {
4767
const doRename = options.rename ?? rename;
68+
const doCopy = options.copy ?? copyFile;
4869
const platform = options.platform ?? process.platform;
4970
for (let attempt = 0; ; attempt++) {
5071
try {
5172
await doRename(from, to);
5273
return;
5374
} catch (error) {
54-
if (attempt >= BACKOFF_MS.length || !isRetryable(error, platform)) throw error;
75+
if (!isRetryable(error, platform)) throw error;
76+
if (attempt >= BACKOFF_MS.length) {
77+
try {
78+
await doCopy(from, to);
79+
} catch {
80+
throw error;
81+
}
82+
removeScratch(from);
83+
return;
84+
}
5585
await new Promise((resolve) => setTimeout(resolve, BACKOFF_MS[attempt]));
5686
}
5787
}

0 commit comments

Comments
 (0)