Compare commits

...

1 Commits

Author SHA1 Message Date
Dax Raad fb566141d3 fix(cli): release service startup lock 2026-07-17 22:46:09 +00:00
2 changed files with 26 additions and 11 deletions
+7 -2
View File
@@ -10,7 +10,7 @@ import { AppProcess } from "@opencode-ai/core/process"
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
import { randomBytes, randomUUID } from "node:crypto"
import path from "node:path"
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
import { Effect, Exit, FileSystem, Logger, Option, Redacted, Schedule, Schema, Scope } from "effect"
import { HttpServer } from "effect/unstable/http"
import { Env } from "./env"
import { ServiceConfig } from "./services/service-config"
@@ -38,8 +38,12 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
return yield* Effect.scoped(
Effect.gen(function* () {
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
if (serviceOptions !== undefined) {
const lockScope = serviceOptions === undefined ? undefined : yield* Scope.make()
if (lockScope !== undefined) yield* Effect.addFinalizer((exit) => Scope.close(lockScope, exit))
if (serviceOptions !== undefined && lockScope !== undefined) {
if ((yield* Service.discover(serviceOptions)) !== undefined) return yield* Effect.void
const acquired = yield* ProcessLock.acquire(serviceOptions.file + ".lock").pipe(
Effect.provideService(Scope.Scope, lockScope),
Effect.as(true),
Effect.catchTag("ProcessLockHeldError", () => Effect.succeed(false)),
)
@@ -72,6 +76,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
? undefined
: { onListen: (address) => register(address, password, instanceID, serviceOptions.file) },
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
if (lockScope !== undefined) yield* Scope.close(lockScope, Exit.void)
const url = HttpServer.formatAddress(server.address)
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
+19 -9
View File
@@ -10,6 +10,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
import { expect, test } from "bun:test"
import { Effect, Schedule, Schema } from "effect"
import fs from "node:fs/promises"
@@ -142,15 +143,23 @@ test("concurrent service processes elect one server", async () => {
expect(exited).toEqual(losers.map(() => true))
expect(winner?.exitCode).toBe(null)
expect(
await fetch(new URL("/api/health", info.url), {
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
}).then((response) => response.json()),
).toEqual({
healthy: true,
version: info.version,
pid: info.pid,
})
await Effect.runPromise(
Effect.gen(function* () {
yield* ProcessLock.acquire(registration + ".lock")
expect(winner?.exitCode).toBe(null)
expect(
yield* Effect.promise(() =>
fetch(new URL("/api/health", info.url), {
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
}).then((response) => response.json()),
),
).toEqual({
healthy: true,
version: info.version,
pid: info.pid,
})
}).pipe(Effect.scoped),
)
const blockedTemp = registration + "." + info.id + ".tmp"
await fs.mkdir(blockedTemp)
await fs.rm(registration)
@@ -172,6 +181,7 @@ test("concurrent service processes elect one server", async () => {
Bun.sleep(10_000).then(() => false),
])
expect(contenderExited).toBe(true)
expect(contender.exitCode).toBe(0)
expect((await waitForInfo(registration)).id).toBe(info.id)
} finally {
contender.kill("SIGTERM")