feat: expose background service lifecycle (#36895)
This commit is contained in:
parent
ea89a2f619
commit
ece2b16cdf
36 changed files with 2421 additions and 293 deletions
17
packages/core/test/fixture/process-lock-worker.ts
Normal file
17
packages/core/test/fixture/process-lock-worker.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
|
||||
import { Effect, Schema } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
|
||||
const input = Schema.decodeUnknownSync(
|
||||
Schema.fromJsonString(Schema.Struct({ file: Schema.String, ready: Schema.String })),
|
||||
)(process.argv[2])
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* ProcessLock.acquire(input.file)
|
||||
yield* Effect.promise(() => fs.writeFile(input.ready, String(process.pid)))
|
||||
return yield* Effect.never
|
||||
}),
|
||||
),
|
||||
)
|
||||
71
packages/core/test/util/process-lock.test.ts
Normal file
71
packages/core/test/util/process-lock.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { expect } from "bun:test"
|
||||
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
|
||||
import { Effect } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const worker = path.join(import.meta.dir, "../fixture/process-lock-worker.ts")
|
||||
|
||||
it.live(
|
||||
"releases ownership when the scope closes",
|
||||
Effect.gen(function* () {
|
||||
const root = yield* temp("opencode-process-lock-")
|
||||
const file = path.join(root, "service.lock")
|
||||
yield* Effect.scoped(ProcessLock.acquire(file))
|
||||
yield* Effect.scoped(ProcessLock.acquire(file))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"releases ownership when the process dies",
|
||||
Effect.gen(function* () {
|
||||
const root = yield* temp("opencode-process-lock-death-")
|
||||
const file = path.join(root, "service.lock")
|
||||
const ready = path.join(root, "ready")
|
||||
const child = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
Bun.spawn([process.execPath, worker, JSON.stringify({ file, ready })], {
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
}),
|
||||
),
|
||||
(child) =>
|
||||
Effect.promise(async () => {
|
||||
kill(child)
|
||||
await child.exited
|
||||
}),
|
||||
)
|
||||
yield* Effect.promise(async () => {
|
||||
for (let attempt = 0; attempt < 100 && !(await Bun.file(ready).exists()); attempt++) await Bun.sleep(20)
|
||||
})
|
||||
expect(yield* Effect.promise(() => Bun.file(ready).exists())).toBe(true)
|
||||
|
||||
const error = yield* Effect.scoped(ProcessLock.acquire(file)).pipe(Effect.flip)
|
||||
expect(error._tag).toBe("ProcessLockHeldError")
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
process.kill(child.pid, "SIGSTOP")
|
||||
const paused = yield* Effect.scoped(ProcessLock.acquire(file)).pipe(Effect.flip)
|
||||
expect(paused._tag).toBe("ProcessLockHeldError")
|
||||
process.kill(child.pid, "SIGCONT")
|
||||
}
|
||||
|
||||
kill(child)
|
||||
yield* Effect.promise(() => child.exited)
|
||||
yield* Effect.scoped(ProcessLock.acquire(file))
|
||||
}),
|
||||
)
|
||||
|
||||
function temp(prefix: string) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), prefix))),
|
||||
(root) => Effect.promise(() => fs.rm(root, { recursive: true, force: true })),
|
||||
)
|
||||
}
|
||||
|
||||
function kill(child: Bun.Subprocess) {
|
||||
if (process.platform === "win32") return child.kill()
|
||||
return child.kill("SIGKILL")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue