fix(core): retain interrupted shell output
This commit is contained in:
parent
c8dca936b1
commit
3e81ed4985
3 changed files with 51 additions and 2 deletions
|
|
@ -35,6 +35,7 @@ type Active = {
|
|||
done: Deferred.Deferred<Info, NotFoundError>
|
||||
timeoutFiber?: Fiber.Fiber<void>
|
||||
timeout?: (duration: number) => Effect.Effect<void>
|
||||
kill?: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,6 +60,8 @@ export interface Interface {
|
|||
readonly wait: (id: Shell.ID) => Effect.Effect<Shell.Info, NotFoundError>
|
||||
// Replaces the running command's timeout from now; zero clears it.
|
||||
readonly timeout: (id: Shell.ID, duration: number) => Effect.Effect<Shell.Info, NotFoundError>
|
||||
// Stops a running command while retaining its terminal state and output.
|
||||
readonly kill: (id: Shell.ID) => Effect.Effect<Shell.Info, NotFoundError>
|
||||
readonly output: (id: Shell.ID, input?: Shell.OutputInput) => Effect.Effect<Shell.Output, NotFoundError>
|
||||
readonly remove: (id: Shell.ID) => Effect.Effect<void, NotFoundError>
|
||||
}
|
||||
|
|
@ -120,6 +123,12 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
|
|||
yield* removeSession(id)
|
||||
})
|
||||
|
||||
const kill = Effect.fn("Shell.kill")(function* (id: Shell.ID) {
|
||||
const session = yield* require(id)
|
||||
if (session.kill) yield* session.kill()
|
||||
return yield* Deferred.await(session.done)
|
||||
})
|
||||
|
||||
const list = Effect.fn("Shell.list")(function* () {
|
||||
return Array.from(sessions.values())
|
||||
.filter((session) => session.info.status === "running")
|
||||
|
|
@ -313,6 +322,8 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
|
|||
)
|
||||
})
|
||||
|
||||
session.kill = () => finish("exited", undefined, handle.kill().pipe(Effect.catch(() => Effect.void)))
|
||||
|
||||
yield* session.timeout(invocation.timeout)
|
||||
|
||||
runFork(
|
||||
|
|
@ -335,7 +346,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect(
|
|||
return session.info
|
||||
})
|
||||
|
||||
return Service.of({ name, create, list, get, wait, timeout, output, remove })
|
||||
return Service.of({ name, create, list, get, wait, timeout, kill, output, remove })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export const Plugin = {
|
|||
const run = settleShell().pipe(
|
||||
Effect.tap((output) => Deferred.succeed(settled, output)),
|
||||
Effect.map((output) => output.output),
|
||||
Effect.onInterrupt(() => shell.remove(info.id).pipe(Effect.ignore)),
|
||||
Effect.onInterrupt(() => shell.kill(info.id).pipe(Effect.ignore)),
|
||||
)
|
||||
const job = yield* runtime.job.start({
|
||||
id: context.callID,
|
||||
|
|
|
|||
|
|
@ -480,6 +480,44 @@ describe("ShellTool", () => {
|
|||
),
|
||||
)
|
||||
|
||||
it.live("retains partial output when a foreground command is interrupted", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const shell = yield* Shell.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const waiting = yield* executeTool(
|
||||
registry,
|
||||
call({ command: steadyProgressCommand }, "call-interrupt-output"),
|
||||
).pipe(Effect.forkIn(scope, { startImmediately: true }))
|
||||
|
||||
const waitForShell = (remaining = 1000): Effect.Effect<ShellSchema.ID, Error> =>
|
||||
Effect.gen(function* () {
|
||||
const job = yield* jobs.get("call-interrupt-output")
|
||||
const shellID = job?.metadata?.shellID
|
||||
if (typeof shellID === "string") return ShellSchema.ID.make(shellID)
|
||||
if (remaining <= 0) return yield* Effect.fail(new Error("Timed out waiting for foreground shell"))
|
||||
yield* Effect.promise(() => Bun.sleep(1))
|
||||
return yield* waitForShell(remaining - 1)
|
||||
})
|
||||
const id = yield* waitForShell()
|
||||
yield* Effect.sleep(Duration.millis(100))
|
||||
yield* Fiber.interrupt(waiting)
|
||||
|
||||
expect((yield* shell.get(id)).status).toBe("exited")
|
||||
expect((yield* shell.output(id)).output).toContain("steady")
|
||||
yield* shell.remove(id)
|
||||
}),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("returns the shell id for a background command", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue