Skip to content

Commit e25db3a

Browse files
Fix provider cache atomic write temp path collisions (#2291)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>
1 parent b8305af commit e25db3a

5 files changed

Lines changed: 55 additions & 42 deletions

File tree

apps/server/src/atomicWrite.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Effect, FileSystem, Path } from "effect";
2+
import * as Random from "effect/Random";
3+
4+
export const writeFileStringAtomically = (input: {
5+
readonly filePath: string;
6+
readonly contents: string;
7+
}) =>
8+
Effect.scoped(
9+
Effect.gen(function* () {
10+
const fs = yield* FileSystem.FileSystem;
11+
const path = yield* Path.Path;
12+
const tempFileId = yield* Random.nextUUIDv4;
13+
const targetDirectory = path.dirname(input.filePath);
14+
15+
yield* fs.makeDirectory(targetDirectory, { recursive: true });
16+
const tempDirectory = yield* fs.makeTempDirectoryScoped({
17+
directory: targetDirectory,
18+
prefix: `${path.basename(input.filePath)}.`,
19+
});
20+
const tempPath = path.join(tempDirectory, `${tempFileId}.tmp`);
21+
22+
yield* fs.writeFileString(tempPath, input.contents);
23+
yield* fs.rename(tempPath, input.filePath);
24+
}),
25+
);

apps/server/src/keybindings.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
} from "effect";
4747
import * as Semaphore from "effect/Semaphore";
4848
import { ServerConfig } from "./config.ts";
49+
import { writeFileStringAtomically } from "./atomicWrite.ts";
4950
import { fromLenientJson } from "@t3tools/shared/schemaJson";
5051

5152
type WhenToken =
@@ -670,14 +671,17 @@ const makeKeybindings = Effect.gen(function* () {
670671
});
671672

672673
const writeConfigAtomically = (rules: readonly KeybindingRule[]) => {
673-
const tempPath = `${keybindingsConfigPath}.${process.pid}.${Date.now()}.tmp`;
674-
675674
return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe(
676675
Effect.map((encoded) => `${encoded}\n`),
677-
Effect.tap(() => fs.makeDirectory(path.dirname(keybindingsConfigPath), { recursive: true })),
678-
Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)),
679-
Effect.flatMap(() => fs.rename(tempPath, keybindingsConfigPath)),
680-
Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))),
676+
Effect.flatMap((encoded) =>
677+
writeFileStringAtomically({
678+
filePath: keybindingsConfigPath,
679+
contents: encoded,
680+
}).pipe(
681+
Effect.provideService(FileSystem.FileSystem, fs),
682+
Effect.provideService(Path.Path, path),
683+
),
684+
),
681685
Effect.mapError(
682686
(cause) =>
683687
new KeybindingsConfigError({

apps/server/src/provider/providerStatusCache.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as nodePath from "node:path";
22
import { type ServerProvider, ServerProvider as ServerProviderSchema } from "@t3tools/contracts";
3-
import { Cause, Effect, FileSystem, Path, Schema } from "effect";
3+
import { Cause, Effect, FileSystem, Schema } from "effect";
4+
5+
import { writeFileStringAtomically } from "../atomicWrite.ts";
46

57
export const PROVIDER_CACHE_IDS = [
68
"codex",
@@ -96,22 +98,8 @@ export const readProviderStatusCache = (filePath: string) =>
9698
export const writeProviderStatusCache = (input: {
9799
readonly filePath: string;
98100
readonly provider: ServerProvider;
99-
}) => {
100-
const tempPath = `${input.filePath}.${process.pid}.${Date.now()}.tmp`;
101-
return Effect.gen(function* () {
102-
const fs = yield* FileSystem.FileSystem;
103-
const path = yield* Path.Path;
104-
const encoded = `${JSON.stringify(input.provider, null, 2)}\n`;
105-
106-
yield* fs.makeDirectory(path.dirname(input.filePath), { recursive: true });
107-
yield* fs.writeFileString(tempPath, encoded);
108-
yield* fs.rename(tempPath, input.filePath);
109-
}).pipe(
110-
Effect.ensuring(
111-
Effect.gen(function* () {
112-
const fs = yield* FileSystem.FileSystem;
113-
yield* fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }));
114-
}),
115-
),
116-
);
117-
};
101+
}) =>
102+
writeFileStringAtomically({
103+
filePath: input.filePath,
104+
contents: `${JSON.stringify(input.provider, null, 2)}\n`,
105+
});

apps/server/src/serverRuntimeState.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Effect, FileSystem, Option, Path, Schema } from "effect";
1+
import { Effect, FileSystem, Option, Schema } from "effect";
22

3+
import { writeFileStringAtomically } from "./atomicWrite.ts";
34
import { type ServerConfigShape } from "./config.ts";
45
import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
56

@@ -42,15 +43,9 @@ export const persistServerRuntimeState = (input: {
4243
readonly path: string;
4344
readonly state: PersistedServerRuntimeState;
4445
}) =>
45-
Effect.gen(function* () {
46-
const fs = yield* FileSystem.FileSystem;
47-
const pathService = yield* Path.Path;
48-
const tempPath = `${input.path}.${process.pid}.${Date.now()}.tmp`;
49-
return yield* fs.makeDirectory(pathService.dirname(input.path), { recursive: true }).pipe(
50-
Effect.flatMap(() => fs.writeFileString(tempPath, `${JSON.stringify(input.state)}\n`)),
51-
Effect.flatMap(() => fs.rename(tempPath, input.path)),
52-
Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))),
53-
);
46+
writeFileStringAtomically({
47+
filePath: input.path,
48+
contents: `${JSON.stringify(input.state)}\n`,
5449
});
5550

5651
export const clearPersistedServerRuntimeState = (path: string) =>

apps/server/src/serverSettings.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
Cause,
4040
} from "effect";
4141
import * as Semaphore from "effect/Semaphore";
42+
import { writeFileStringAtomically } from "./atomicWrite.ts";
4243
import { ServerConfig } from "./config.ts";
4344
import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
4445
import { fromLenientJson } from "@t3tools/shared/schemaJson";
@@ -233,14 +234,14 @@ const makeServerSettings = Effect.gen(function* () {
233234
const getSettingsFromCache = Cache.get(settingsCache, cacheKey);
234235

235236
const writeSettingsAtomically = (settings: ServerSettings) => {
236-
const tempPath = `${settingsPath}.${process.pid}.${Date.now()}.tmp`;
237237
const sparseSettings = stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {};
238238

239-
return Effect.succeed(`${JSON.stringify(sparseSettings, null, 2)}\n`).pipe(
240-
Effect.tap(() => fs.makeDirectory(pathService.dirname(settingsPath), { recursive: true })),
241-
Effect.tap((encoded) => fs.writeFileString(tempPath, encoded)),
242-
Effect.flatMap(() => fs.rename(tempPath, settingsPath)),
243-
Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore({ log: true }))),
239+
return writeFileStringAtomically({
240+
filePath: settingsPath,
241+
contents: `${JSON.stringify(sparseSettings, null, 2)}\n`,
242+
}).pipe(
243+
Effect.provideService(FileSystem.FileSystem, fs),
244+
Effect.provideService(Path.Path, pathService),
244245
Effect.mapError(
245246
(cause) =>
246247
new ServerSettingsError({

0 commit comments

Comments
 (0)