2023-05-23 18:53:53 +00:00
|
|
|
import { Manifest } from "./types.ts";
|
|
|
|
import { ControllerMessage, WorkerMessage } from "./protocol.ts";
|
2022-10-10 12:50:21 +00:00
|
|
|
import { Plug } from "./plug.ts";
|
2022-03-25 11:03:06 +00:00
|
|
|
|
2023-05-23 18:53:53 +00:00
|
|
|
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox<HookT>;
|
2022-03-23 14:41:12 +00:00
|
|
|
|
2023-05-23 18:53:53 +00:00
|
|
|
/**
|
|
|
|
* Represents a "safe" execution environment for plug code
|
|
|
|
* Effectively this wraps a web worker, the reason to have this split from Plugs is to allow plugs to manage multiple sandboxes, e.g. for performance in the future
|
|
|
|
*/
|
|
|
|
export class Sandbox<HookT> {
|
2023-12-06 17:44:48 +00:00
|
|
|
private worker?: Worker;
|
2023-05-23 18:53:53 +00:00
|
|
|
private reqId = 0;
|
|
|
|
private outstandingInvocations = new Map<
|
2022-03-23 14:41:12 +00:00
|
|
|
number,
|
|
|
|
{ resolve: (result: any) => void; reject: (e: any) => void }
|
|
|
|
>();
|
|
|
|
|
2023-12-06 17:44:48 +00:00
|
|
|
// public ready: Promise<void>;
|
2023-05-23 18:53:53 +00:00
|
|
|
public manifest?: Manifest<HookT>;
|
2022-03-23 14:41:12 +00:00
|
|
|
|
2023-05-23 18:53:53 +00:00
|
|
|
constructor(
|
|
|
|
readonly plug: Plug<HookT>,
|
2023-12-06 17:44:48 +00:00
|
|
|
private workerOptions = {},
|
2023-05-23 18:53:53 +00:00
|
|
|
) {
|
2023-12-06 17:44:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should only invoked lazily (either by invoke, or by a ManifestCache to load the manifest)
|
|
|
|
*/
|
|
|
|
init(): Promise<void> {
|
|
|
|
console.log("Booting up worker for", this.plug.name);
|
|
|
|
if (this.worker) {
|
|
|
|
// Should not happen
|
|
|
|
console.warn("Double init of sandbox");
|
|
|
|
}
|
|
|
|
this.worker = new Worker(this.plug.workerUrl, {
|
|
|
|
...this.workerOptions,
|
2023-05-23 18:53:53 +00:00
|
|
|
type: "module",
|
2022-03-23 14:41:12 +00:00
|
|
|
});
|
2023-12-06 17:44:48 +00:00
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.worker!.onmessage = (ev) => {
|
2023-05-23 18:53:53 +00:00
|
|
|
if (ev.data.type === "manifest") {
|
|
|
|
this.manifest = ev.data.manifest;
|
|
|
|
resolve();
|
|
|
|
return;
|
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
|
2023-05-23 18:53:53 +00:00
|
|
|
this.onMessage(ev.data);
|
|
|
|
};
|
2022-05-13 12:36:26 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-23 14:41:12 +00:00
|
|
|
async onMessage(data: ControllerMessage) {
|
|
|
|
switch (data.type) {
|
2023-05-23 18:53:53 +00:00
|
|
|
case "sys":
|
2022-03-23 14:41:12 +00:00
|
|
|
try {
|
2022-10-15 17:02:56 +00:00
|
|
|
const result = await this.plug.syscall(data.name!, data.args!);
|
2022-03-23 14:41:12 +00:00
|
|
|
|
2023-12-06 17:44:48 +00:00
|
|
|
this.worker!.postMessage({
|
2023-05-23 18:53:53 +00:00
|
|
|
type: "sysr",
|
2022-03-23 14:41:12 +00:00
|
|
|
id: data.id,
|
|
|
|
result: result,
|
|
|
|
} as WorkerMessage);
|
|
|
|
} catch (e: any) {
|
2022-03-28 13:25:05 +00:00
|
|
|
// console.error("Syscall fail", e);
|
2023-12-06 17:44:48 +00:00
|
|
|
this.worker!.postMessage({
|
2023-05-23 18:53:53 +00:00
|
|
|
type: "sysr",
|
2022-03-23 14:41:12 +00:00
|
|
|
id: data.id,
|
|
|
|
error: e.message,
|
|
|
|
} as WorkerMessage);
|
|
|
|
}
|
|
|
|
break;
|
2023-05-23 18:53:53 +00:00
|
|
|
case "invr": {
|
2022-10-15 17:02:56 +00:00
|
|
|
const resultCbs = this.outstandingInvocations.get(data.id!);
|
2022-03-23 14:41:12 +00:00
|
|
|
this.outstandingInvocations.delete(data.id!);
|
|
|
|
if (data.error) {
|
2022-05-13 15:05:52 +00:00
|
|
|
resultCbs &&
|
2023-05-23 18:53:53 +00:00
|
|
|
resultCbs.reject(new Error(data.error));
|
2022-03-23 14:41:12 +00:00
|
|
|
} else {
|
|
|
|
resultCbs && resultCbs.resolve(data.result);
|
|
|
|
}
|
|
|
|
break;
|
2022-10-15 17:02:56 +00:00
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
default:
|
|
|
|
console.error("Unknown message type", data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-06 17:44:48 +00:00
|
|
|
async invoke(name: string, args: any[]): Promise<any> {
|
|
|
|
if (!this.worker) {
|
|
|
|
// Lazy initialization
|
|
|
|
await this.init();
|
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
this.reqId++;
|
2023-12-06 17:44:48 +00:00
|
|
|
this.worker!.postMessage({
|
2023-05-23 18:53:53 +00:00
|
|
|
type: "inv",
|
2022-03-23 14:41:12 +00:00
|
|
|
id: this.reqId,
|
|
|
|
name,
|
|
|
|
args,
|
2023-05-23 18:53:53 +00:00
|
|
|
} as WorkerMessage);
|
2022-03-23 14:41:12 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.outstandingInvocations.set(this.reqId, { resolve, reject });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
2023-12-06 17:44:48 +00:00
|
|
|
if (this.worker) {
|
|
|
|
this.worker.terminate();
|
|
|
|
this.worker = undefined;
|
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
|
|
|
}
|