UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

142 lines • 5.22 kB
/** Host-side runner lifecycle; cancellation and actual completion are separate. * A cancellation stands for a signal: a native program the runner started * receives it, and a cancelled runner's status is 128 plus it, as a shell * reports a child the signal ended. Joining covers the runner's finally * blocks, not untracked native descendants. * The runner starts once `start` is called, so whoever tracks the handle * registers it before even a synchronous prelude of the runner runs. */ export class ProcessHandle { run; cancel; finished; cancelChildren; bind; task; /** Whether the runner has started running. */ begun = false; current; stopSignal = null; exitedCode = null; nativeRuns = new Map(); completion; constructor(info, run, cancel, finished, cancelChildren = () => undefined, bind = (_id, run) => run()) { this.run = run; this.cancel = cancel; this.finished = finished; this.cancelChildren = cancelChildren; this.bind = bind; this.current = Object.freeze(info); } /** * Start the runner. * * @param eager run it at once, up to its first wait, for a caller with * nothing left to set up; otherwise it starts on the next microtask. * Either way a runner that throws before it returns its promise fails * as one whose promise rejects. */ start(eager = false) { const work = () => { this.begun = true; return this.bind(this.current.executionId, this.run, this); }; this.task = eager ? (async () => work())() : Promise.resolve().then(work); this.completion = this.task.then((code) => this.settle(code, null, this.finished), (error) => { const aborted = error instanceof Error && error.name === 'AbortError'; if (aborted) this.current = Object.freeze({ ...this.current, cancellationRequested: true }); return this.settle(aborted ? 128 + (this.stopSignal ?? 9) : 1, aborted ? null : error instanceof Error ? error.message : String(error), this.finished); }); } get info() { return this.current; } /** The signal the runner was stopped with, null while it runs. */ get signal() { return this.stopSignal; } /** The status a program the runner started exited with on its own after the stop signal, null unless one did. */ get programCode() { return this.exitedCode; } /** * The native runs this runner has going, each under a key of its own: a * host program it started, a sandbox's run. */ get runs() { return Object.fromEntries(this.nativeRuns); } /** * Record a native run this runner started, which the process plane * publishes and a workspace that outlives this one stops. * * @param key the run's key, unique among the runner's. * @param run how another workspace reaches it. */ addRun(key, run) { this.nativeRuns.set(key, run); } /** Forget a native run once it ended, by the key it was recorded under. */ dropRun(key) { this.nativeRuns.delete(key); } /** * Record that a program the runner started handled the stop signal and * exited: a job that is that one program reports its status, as bash * reports a child that handled the signal. * * @param code the program's exit status. */ programExited(code) { this.exitedCode = code; } /** * Request cancellation without waiting for the runner to exit. A runner * that already exited has nothing to cancel, but what it started may * still run (`sh -c 'sleep 100 &'`), so its children are stopped all the * same. A KILL while the runner still stops under another signal cuts the * grace that signal gave its program short, as a second `kill -KILL` does. * * @param signal what the stop stands for, KILL (9) unless a `kill` named another. */ terminate(signal = 9) { if (this.current.state === 'exited') { this.cancelChildren(signal); return false; } if (this.current.cancellationRequested) { if (signal !== 9 || this.stopSignal === 9) return false; this.stopSignal = signal; this.cancelChildren(signal); return true; } const previous = this.current; this.current = Object.freeze({ ...this.current, state: 'stopping', cancellationRequested: true, }); this.stopSignal = signal; try { this.cancelChildren(signal); this.cancel(signal); } catch (error) { this.current = previous; this.stopSignal = null; throw error; } return true; } settle(code, failure, finished) { this.current = Object.freeze({ ...this.current, state: 'exited', exitCode: code, failure }); finished(this.current.pid); return this.current; } join() { return this.completion; } } //# sourceMappingURL=handle.js.map