Plug sandbox rework
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { WorkerSandbox } from "./worker_sandbox.ts";
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
|
||||
// Uses Deno's permissions to lock the worker down significantly
|
||||
export function createSandbox<HookT>(plug: Plug<HookT>): Sandbox<HookT> {
|
||||
return new WorkerSandbox(plug, {
|
||||
deno: {
|
||||
permissions: {
|
||||
// Allow network access
|
||||
net: true,
|
||||
// This is required for console logging to work, apparently?
|
||||
env: true,
|
||||
// No talking to native code
|
||||
ffi: false,
|
||||
// No invocation of shell commands
|
||||
run: false,
|
||||
// No read access to the file system
|
||||
read: false,
|
||||
// No write access to the file system
|
||||
write: false,
|
||||
},
|
||||
},
|
||||
// Have to do this because the "deno" option is not standard and doesn't typecheck yet
|
||||
} as any);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { PromiseQueue } from "$sb/lib/async.ts";
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import { Manifest } from "../types.ts";
|
||||
|
||||
// We need to hard inject the syscall function into the global scope
|
||||
declare global {
|
||||
interface globalThis {
|
||||
syscall(name: string, ...args: any[]): Promise<any>;
|
||||
}
|
||||
}
|
||||
|
||||
export type PlugExport<HookT> = {
|
||||
manifest: Manifest<HookT>;
|
||||
functionMapping: Record<string, Function>;
|
||||
};
|
||||
|
||||
const functionQueue = new PromiseQueue();
|
||||
|
||||
let activePlug: Plug<any> | undefined;
|
||||
|
||||
// @ts-ignore: globalThis
|
||||
globalThis.syscall = (name: string, ...args: any[]): Promise<any> => {
|
||||
if (!activePlug) {
|
||||
throw new Error("No active plug");
|
||||
}
|
||||
console.log("Calling syscall", name, args);
|
||||
return activePlug.syscall(name, args);
|
||||
};
|
||||
|
||||
export class NoSandbox<HookT> implements Sandbox<HookT> {
|
||||
manifest?: Manifest<HookT> | undefined;
|
||||
constructor(
|
||||
private plug: Plug<HookT>,
|
||||
private plugExport: PlugExport<HookT>,
|
||||
) {
|
||||
this.manifest = plugExport.manifest;
|
||||
plug.manifest = this.manifest;
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
invoke(name: string, args: any[]): Promise<any> {
|
||||
activePlug = this.plug;
|
||||
return functionQueue.runInQueue(async () => {
|
||||
try {
|
||||
const fn = this.plugExport.functionMapping[name];
|
||||
if (!fn) {
|
||||
throw new Error(`Function not loaded: ${name}`);
|
||||
}
|
||||
return await fn(...args);
|
||||
} finally {
|
||||
activePlug = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
}
|
||||
}
|
||||
|
||||
export function noSandboxFactory<HookT>(
|
||||
plugExport: PlugExport<HookT>,
|
||||
): (plug: Plug<HookT>) => Sandbox<HookT> {
|
||||
return (plug: Plug<HookT>) => new NoSandbox(plug, plugExport);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Manifest } from "../types.ts";
|
||||
|
||||
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox<HookT>;
|
||||
|
||||
export interface Sandbox<HookT> {
|
||||
manifest?: Manifest<HookT>;
|
||||
init(): Promise<void>;
|
||||
invoke(name: string, args: any[]): Promise<any>;
|
||||
stop(): void;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { WorkerSandbox } from "./worker_sandbox.ts";
|
||||
import type { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
|
||||
export function createSandbox<HookT>(plug: Plug<HookT>): Sandbox<HookT> {
|
||||
return new WorkerSandbox(plug);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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 WorkerSandbox<HookT> implements Sandbox<HookT> {
|
||||
private worker?: Worker;
|
||||
private reqId = 0;
|
||||
private outstandingInvocations = new Map<
|
||||
number,
|
||||
{ resolve: (result: any) => void; reject: (e: any) => void }
|
||||
>();
|
||||
|
||||
// public ready: Promise<void>;
|
||||
public manifest?: Manifest<HookT>;
|
||||
|
||||
constructor(
|
||||
readonly plug: Plug<HookT>,
|
||||
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) {
|
||||
// Race condition
|
||||
console.warn("Double init of sandbox, ignoring");
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.worker = new Worker(this.plug.workerUrl!, {
|
||||
...this.workerOptions,
|
||||
type: "module",
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.worker!.onmessage = (ev) => {
|
||||
if (ev.data.type === "manifest") {
|
||||
this.manifest = ev.data.manifest;
|
||||
// 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();
|
||||
}
|
||||
|
||||
this.onMessage(ev.data);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async onMessage(data: ControllerMessage) {
|
||||
if (!this.worker) {
|
||||
console.warn("Received message for terminated worker, ignoring");
|
||||
return;
|
||||
}
|
||||
switch (data.type) {
|
||||
case "sys":
|
||||
try {
|
||||
const result = await this.plug.syscall(data.name!, data.args!);
|
||||
|
||||
this.worker && this.worker!.postMessage({
|
||||
type: "sysr",
|
||||
id: data.id,
|
||||
result: result,
|
||||
} as WorkerMessage);
|
||||
} catch (e: any) {
|
||||
// console.error("Syscall fail", e);
|
||||
this.worker && this.worker!.postMessage({
|
||||
type: "sysr",
|
||||
id: data.id,
|
||||
error: e.message,
|
||||
} as WorkerMessage);
|
||||
}
|
||||
break;
|
||||
case "invr": {
|
||||
const resultCbs = this.outstandingInvocations.get(data.id!);
|
||||
this.outstandingInvocations.delete(data.id!);
|
||||
if (data.error) {
|
||||
resultCbs &&
|
||||
resultCbs.reject(new Error(data.error));
|
||||
} else {
|
||||
resultCbs && resultCbs.resolve(data.result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error("Unknown message type", data);
|
||||
}
|
||||
}
|
||||
|
||||
async invoke(name: string, args: any[]): Promise<any> {
|
||||
if (!this.worker) {
|
||||
// Lazy initialization
|
||||
await this.init();
|
||||
}
|
||||
this.reqId++;
|
||||
this.worker!.postMessage({
|
||||
type: "inv",
|
||||
id: this.reqId,
|
||||
name,
|
||||
args,
|
||||
} as WorkerMessage);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.outstandingInvocations.set(this.reqId, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.worker) {
|
||||
this.worker.terminate();
|
||||
this.worker = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user