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: {
555555 yield* fs.makeDirectory(directory, { recursive: true });
556556 const tempPath = yield* fs.makeTempFileScoped({ directory, prefix: ".service-write-" });
557557 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;
559560 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 );
561568 }),
562569 ).pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
563570 const plan: BootServicePlan = {
apps/server/src/serviceLauncher.ts+8-7
......@@ -70,8 +70,9 @@ async function pathExists(target: string): Promise<boolean> {
7070 }
7171}
7272
73// Opened read-write: Windows refuses to flush a handle without write access.
7374async function syncFile(filePath: string): Promise<void> {
74 const handle = await NodeFSP.open(filePath, "r");
75 const handle = await NodeFSP.open(filePath, "r+");
7576 try {
7677 await handle.sync();
7778 } finally {
......@@ -79,10 +80,15 @@ async function syncFile(filePath: string): Promise<void> {
7980 }
8081}
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.
8286async function syncDirectory(directory: string): Promise<void> {
8387 const handle = await NodeFSP.open(directory, "r");
8488 try {
8589 await handle.sync();
90 } catch (error) {
91 if ((error as NodeJS.ErrnoException).code !== "EPERM") throw error;
8692 } finally {
8793 await handle.close();
8894 }
......@@ -188,12 +194,7 @@ export async function writeServiceState(filePath: string, state: ServiceState):
188194 await handle.close();
189195 handle = undefined;
190196 await NodeFSP.rename(tempPath, filePath);
191 const directoryHandle = await NodeFSP.open(directory, "r");
192 try {
193 await directoryHandle.sync();
194 } finally {
195 await directoryHandle.close();
196 }
197 await syncDirectory(directory);
197198 } finally {
198199 await handle?.close().catch(() => undefined);
199200 await NodeFSP.rm(tempPath, { force: true }).catch(() => undefined);