authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 13:33:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 20:33:21+00:00
logf083f520fbde1e1bf4332399df47b45c2bd99722
tree2386fa3defcbc74d8c3a1d571cc30a0c68e23951
parentc251e41b5e5287b786cda44ddc263b8e6a74859c
signature Signed by PGP key B5690EEEBB952194

fix(server): tolerate the missing directory fsync on Windows (#9569)

Co-authored-by: Claude Code <noreply@anthropic.com>

2 files changed, 17 insertions(+), 9 deletions(-)

apps/server/src/cloud/bootService.ts+9-2
...@@ -555,9 +555,16 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {...@@ -555,9 +555,16 @@ export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
555 yield* fs.makeDirectory(directory, { recursive: true });555 yield* fs.makeDirectory(directory, { recursive: true });
556 const tempPath = yield* fs.makeTempFileScoped({ directory, prefix: ".service-write-" });556 const tempPath = yield* fs.makeTempFileScoped({ directory, prefix: ".service-write-" });
557 yield* fs.writeFileString(tempPath, contents, { mode: 0o600 });557 yield* fs.writeFileString(tempPath, contents, { mode: 0o600 });
558 yield* (yield* fs.open(tempPath, { flag: "r" })).sync;558 // Opened read-write: Windows refuses to flush a handle without write access.
559 yield* (yield* fs.open(tempPath, { flag: "r+" })).sync;
559 yield* fs.rename(tempPath, filePath);560 yield* fs.rename(tempPath, filePath);
560 yield* (yield* fs.open(directory, { flag: "r" })).sync;561 // Windows has no directory fsync (EPERM); NTFS journals the rename.
562 yield* (yield* fs.open(directory, { flag: "r" })).sync.pipe(
563 Effect.catchIf(
564 (error) => (error.reason.cause as NodeJS.ErrnoException | undefined)?.code === "EPERM",
565 () => Effect.void,
566 ),
567 );
561 }),568 }),
562 ).pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));569 ).pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
563 const plan: BootServicePlan = {570 const plan: BootServicePlan = {
apps/server/src/serviceLauncher.ts+8-7
...@@ -70,8 +70,9 @@ async function pathExists(target: string): Promise<boolean> {...@@ -70,8 +70,9 @@ async function pathExists(target: string): Promise<boolean> {
70 }70 }
71}71}
7272
73// Opened read-write: Windows refuses to flush a handle without write access.
73async function syncFile(filePath: string): Promise<void> {74async function syncFile(filePath: string): Promise<void> {
74 const handle = await NodeFSP.open(filePath, "r");75 const handle = await NodeFSP.open(filePath, "r+");
75 try {76 try {
76 await handle.sync();77 await handle.sync();
77 } finally {78 } finally {
...@@ -79,10 +80,15 @@ async function syncFile(filePath: string): Promise<void> {...@@ -79,10 +80,15 @@ async function syncFile(filePath: string): Promise<void> {
79 }80 }
80}81}
8182
83// Flushes a directory entry so a rename into it survives power loss. Windows
84// has no directory fsync: the handle opens but sync fails with EPERM, and
85// NTFS journals the rename on its own.
82async function syncDirectory(directory: string): Promise<void> {86async function syncDirectory(directory: string): Promise<void> {
83 const handle = await NodeFSP.open(directory, "r");87 const handle = await NodeFSP.open(directory, "r");
84 try {88 try {
85 await handle.sync();89 await handle.sync();
90 } catch (error) {
91 if ((error as NodeJS.ErrnoException).code !== "EPERM") throw error;
86 } finally {92 } finally {
87 await handle.close();93 await handle.close();
88 }94 }
...@@ -188,12 +194,7 @@ export async function writeServiceState(filePath: string, state: ServiceState):...@@ -188,12 +194,7 @@ export async function writeServiceState(filePath: string, state: ServiceState):
188 await handle.close();194 await handle.close();
189 handle = undefined;195 handle = undefined;
190 await NodeFSP.rename(tempPath, filePath);196 await NodeFSP.rename(tempPath, filePath);
191 const directoryHandle = await NodeFSP.open(directory, "r");197 await syncDirectory(directory);
192 try {
193 await directoryHandle.sync();
194 } finally {
195 await directoryHandle.close();
196 }
197 } finally {198 } finally {
198 await handle?.close().catch(() => undefined);199 await handle?.close().catch(() => undefined);
199 await NodeFSP.rm(tempPath, { force: true }).catch(() => undefined);200 await NodeFSP.rm(tempPath, { force: true }).catch(() => undefined);