fix(shared): tolerate EPERM during fsync in writeFileAtomically

Replaces fsyncSync(tempFileDescriptor) with tolerantFsyncSync, allowing
EPERM/EACCES/ENOTSUP/EINVAL during fsync while still propagating real
errors. Adds an optional deps.fsyncSync injection point used solely by
the new EPERM tolerance regression tests.

Without this fix, plugin startup itself can fail on synced folders
because writeFileAtomically is used by config migrations and posthog
activity state — the same EPERM-on-fsync failure pattern reported for
team_create.
This commit is contained in:
YeonGyu-Kim
2026-05-08 14:05:57 +09:00
parent c69d6bd964
commit 7735b2abd5
2 changed files with 53 additions and 5 deletions
+35
View File
@@ -51,4 +51,39 @@ describe("writeFileAtomically", () => {
// when/then
expect(() => writeFileAtomically(filePath, "content")).toThrow()
})
it("#given fsync fails with EPERM (synced folder) #when writeFileAtomically called #then write succeeds", () => {
// given
const filePath = join(testDir, "synced-folder.txt")
const content = "content from a synced folder where fsync is rejected"
// when
writeFileAtomically(filePath, content, {
fsyncSync: () => {
const error = new Error("EPERM: operation not permitted, fsync") as NodeJS.ErrnoException
error.code = "EPERM"
throw error
},
})
// then
expect(existsSync(filePath)).toBe(true)
expect(readFileSync(filePath, "utf-8")).toBe(content)
})
it("#given fsync fails with EIO (real I/O error) #when writeFileAtomically called #then propagates the error", () => {
// given
const filePath = join(testDir, "io-error.txt")
// when/then
expect(() =>
writeFileAtomically(filePath, "content", {
fsyncSync: () => {
const error = new Error("EIO: input/output error") as NodeJS.ErrnoException
error.code = "EIO"
throw error
},
}),
).toThrow("EIO")
})
})
+18 -5
View File
@@ -1,11 +1,24 @@
import { closeSync, fsyncSync, openSync, renameSync, unlinkSync, writeFileSync } from "node:fs"
import {
closeSync,
type fsyncSync as FsyncSync,
openSync,
renameSync,
unlinkSync,
writeFileSync,
} from "node:fs"
export function writeFileAtomically(filePath: string, content: string): void {
const tempPath = `${filePath}.tmp`
writeFileSync(tempPath, content, "utf-8")
import { tolerantFsyncSync } from "./tolerant-fsync"
export function writeFileAtomically(
filePath: string,
content: string,
deps: { fsyncSync?: typeof FsyncSync } = {},
): void {
const tempPath = `${filePath}.tmp`
writeFileSync(tempPath, content, "utf-8")
const tempFileDescriptor = openSync(tempPath, "r")
try {
fsyncSync(tempFileDescriptor)
tolerantFsyncSync(tempFileDescriptor, `writeFileAtomically:${filePath}`, deps.fsyncSync)
} finally {
closeSync(tempFileDescriptor)
}