1
0
silverbullet/plugos/sandboxes/worker_sandbox.ts

128 lines
3.5 KiB
TypeScript
Raw Normal View History

2024-01-14 12:38:39 +00:00
import { Manifest } from "../types.ts";
import { ControllerMessage, WorkerMessage } from "../protocol.ts";
import { Plug } from "../plug.ts";
import { AssetBundle, AssetJson } from "../asset_bundle/bundle.ts";
import { Sandbox } from "./sandbox.ts";
2022-03-23 14:41:12 +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
*/
2024-01-14 12:38:39 +00:00
export class WorkerSandbox<HookT> implements Sandbox<HookT> {
private worker?: Worker;
private reqId = 0;
private outstandingInvocations = new Map<
2022-03-23 14:41:12 +00:00
number,
{ resolve: (result: any) => void; reject: (e: any) => void }
>();
// public ready: Promise<void>;
public manifest?: Manifest<HookT>;
2022-03-23 14:41:12 +00:00
constructor(
readonly plug: Plug<HookT>,
public workerUrl: URL,
private workerOptions = {},
) {
}
/**
* 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) {
2023-12-10 14:18:34 +00:00
// Race condition
console.warn("Double init of sandbox, ignoring");
return Promise.resolve();
}
this.worker = new Worker(this.workerUrl, {
...this.workerOptions,
type: "module",
2022-03-23 14:41:12 +00:00
});
return new Promise((resolve) => {
this.worker!.onmessage = (ev) => {
if (ev.data.type === "manifest") {
this.manifest = ev.data.manifest;
2023-12-09 17:08:46 +00:00
// Set manifest in the plug
this.plug.manifest = this.manifest;
// Set assets in the plug
this.plug.assets = new AssetBundle(
this.manifest?.assets ? this.manifest.assets as AssetJson : {},
);
return resolve();
}
2022-03-23 14:41:12 +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) {
if (!this.worker) {
console.warn("Received message for terminated worker, ignoring");
return;
}
2022-03-23 14:41:12 +00:00
switch (data.type) {
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
this.worker && this.worker!.postMessage({
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);
this.worker && this.worker!.postMessage({
type: "sysr",
2022-03-23 14:41:12 +00:00
id: data.id,
error: e.message,
} as WorkerMessage);
}
break;
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 &&
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);
}
}
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++;
this.worker!.postMessage({
type: "inv",
2022-03-23 14:41:12 +00:00
id: this.reqId,
name,
args,
} as WorkerMessage);
2022-03-23 14:41:12 +00:00
return new Promise((resolve, reject) => {
this.outstandingInvocations.set(this.reqId, { resolve, reject });
});
}
stop() {
if (this.worker) {
this.worker.terminate();
this.worker = undefined;
}
2022-03-23 14:41:12 +00:00
}
}