diff --git a/packages/cli/src/services/service-config.ts b/packages/cli/src/services/service-config.ts index ae11239839e..ae352d08a91 100644 --- a/packages/cli/src/services/service-config.ts +++ b/packages/cli/src/services/service-config.ts @@ -2,7 +2,7 @@ import { Global } from "@opencode/util/global" import { OPENCODE_CHANNEL, OPENCODE_VERSION } from "../version" import { Hash } from "@opencode/util/hash" import { Service } from "@opencode/client/effect/service" -import { Effect, FileSystem, Option, Schema } from "effect" +import { Effect, FileSystem, Option, Schedule, Schema } from "effect" import { randomBytes } from "crypto" import path from "path" import { selfCommand } from "../util/process" @@ -178,10 +178,10 @@ function route() { const write = Effect.fn("cli.service-config.write")(function* (value: Info) { const { fs, configFile } = yield* paths - const temp = configFile + ".tmp" + const temp = configFile + "." + process.pid + ".tmp" yield* fs.makeDirectory(path.dirname(configFile), { recursive: true }) yield* fs.writeFileString(temp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 }) - yield* fs.rename(temp, configFile) + yield* fs.rename(temp, configFile).pipe(Effect.retry({ times: 20, schedule: Schedule.spaced("10 millis") })) }) export const password = Effect.fn("cli.service-config.password")(function* (value?: string) { diff --git a/packages/cli/src/services/service-registration.ts b/packages/cli/src/services/service-registration.ts index 69a00b28409..7757d60428d 100644 --- a/packages/cli/src/services/service-registration.ts +++ b/packages/cli/src/services/service-registration.ts @@ -36,7 +36,14 @@ export const register = Effect.fnUntraced(function* (options: { found.url === info.url && found.pid === info.pid && found.password === info.password - yield* fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, options.file))) + yield* fs + .writeFileString(temp, encoded, { mode: 0o600 }) + .pipe( + // Windows rename can fail with EPERM while a contender or test polls the registration file. + Effect.andThen( + fs.rename(temp, options.file).pipe(Effect.retry({ times: 20, schedule: Schedule.spaced("10 millis") })), + ), + ) yield* current.pipe( Effect.catchCause((cause) => Effect.logWarning("managed service registration check failed; shutting down", { diff --git a/packages/cli/test/service.test.ts b/packages/cli/test/service.test.ts index c1e3a2ad6ee..832f7736423 100644 --- a/packages/cli/test/service.test.ts +++ b/packages/cli/test/service.test.ts @@ -851,6 +851,7 @@ function serviceEnv(root: string) { ...process.env, BUN_RUNTIME_TRANSPILER_CACHE_PATH: transpilerCache, HOME: root, + OPENCODE_CONFIG_DIR: undefined, OPENCODE_DB: path.join(root, "opencode.db"), OPENCODE_TEST_HOME: root, XDG_CACHE_HOME: path.join(root, "cache"),