@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
142 lines • 5.22 kB
JavaScript
/** 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