2022-10-10 12:50:21 +00:00
|
|
|
import type { LogLevel } from "./environments/custom_logger.ts";
|
2022-05-09 12:59:12 +00:00
|
|
|
import {
|
|
|
|
ControllerMessage,
|
|
|
|
WorkerLike,
|
|
|
|
WorkerMessage,
|
2022-10-10 12:50:21 +00:00
|
|
|
} from "./environments/worker.ts";
|
|
|
|
import { Plug } from "./plug.ts";
|
2022-03-25 11:03:06 +00:00
|
|
|
|
|
|
|
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox;
|
2022-03-23 14:41:12 +00:00
|
|
|
|
2022-05-09 12:59:12 +00:00
|
|
|
export type LogEntry = {
|
|
|
|
level: LogLevel;
|
|
|
|
message: string;
|
|
|
|
date: number;
|
|
|
|
};
|
|
|
|
|
2022-03-23 14:41:12 +00:00
|
|
|
export class Sandbox {
|
|
|
|
protected worker: WorkerLike;
|
|
|
|
protected reqId = 0;
|
|
|
|
protected outstandingInits = new Map<string, () => void>();
|
2022-05-13 12:36:26 +00:00
|
|
|
protected outstandingDependencyInits = new Map<string, () => void>();
|
2022-03-23 14:41:12 +00:00
|
|
|
protected outstandingInvocations = new Map<
|
|
|
|
number,
|
|
|
|
{ resolve: (result: any) => void; reject: (e: any) => void }
|
|
|
|
>();
|
|
|
|
protected loadedFunctions = new Set<string>();
|
2022-03-25 11:03:06 +00:00
|
|
|
protected plug: Plug<any>;
|
2022-05-09 12:59:12 +00:00
|
|
|
public logBuffer: LogEntry[] = [];
|
|
|
|
public maxLogBufferSize = 100;
|
2022-03-23 14:41:12 +00:00
|
|
|
|
2022-03-25 11:03:06 +00:00
|
|
|
constructor(plug: Plug<any>, worker: WorkerLike) {
|
2022-03-23 14:41:12 +00:00
|
|
|
worker.onMessage = this.onMessage.bind(this);
|
|
|
|
this.worker = worker;
|
2022-03-25 11:03:06 +00:00
|
|
|
this.plug = plug;
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
isLoaded(name: string) {
|
|
|
|
return this.loadedFunctions.has(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
async load(name: string, code: string): Promise<void> {
|
|
|
|
await this.worker.ready;
|
2022-10-15 17:02:56 +00:00
|
|
|
const outstandingInit = this.outstandingInits.get(name);
|
2022-03-28 06:51:24 +00:00
|
|
|
if (outstandingInit) {
|
|
|
|
// Load already in progress, let's wait for it...
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.outstandingInits.set(name, () => {
|
|
|
|
outstandingInit!();
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
this.worker.postMessage({
|
|
|
|
type: "load",
|
|
|
|
name: name,
|
|
|
|
code: code,
|
|
|
|
} as WorkerMessage);
|
|
|
|
return new Promise((resolve) => {
|
2022-03-28 06:51:24 +00:00
|
|
|
this.outstandingInits.set(name, () => {
|
|
|
|
this.loadedFunctions.add(name);
|
|
|
|
this.outstandingInits.delete(name);
|
|
|
|
resolve();
|
|
|
|
});
|
2022-03-23 14:41:12 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-10-10 12:50:21 +00:00
|
|
|
loadDependency(name: string, code: string): Promise<void> {
|
2022-05-13 12:36:26 +00:00
|
|
|
// console.log("Loading dependency", name);
|
|
|
|
this.worker.postMessage({
|
|
|
|
type: "load-dependency",
|
|
|
|
name: name,
|
|
|
|
code: code,
|
|
|
|
} as WorkerMessage);
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
// console.log("Loaded dependency", name);
|
|
|
|
this.outstandingDependencyInits.set(name, () => {
|
|
|
|
this.outstandingDependencyInits.delete(name);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-23 14:41:12 +00:00
|
|
|
async onMessage(data: ControllerMessage) {
|
|
|
|
switch (data.type) {
|
2022-10-15 17:02:56 +00:00
|
|
|
case "inited": {
|
|
|
|
const initCb = this.outstandingInits.get(data.name!);
|
2022-03-23 14:41:12 +00:00
|
|
|
initCb && initCb();
|
|
|
|
this.outstandingInits.delete(data.name!);
|
|
|
|
break;
|
2022-10-15 17:02:56 +00:00
|
|
|
}
|
|
|
|
case "dependency-inited": {
|
|
|
|
const depInitCb = this.outstandingDependencyInits.get(data.name!);
|
2022-05-13 12:36:26 +00:00
|
|
|
depInitCb && depInitCb();
|
|
|
|
this.outstandingDependencyInits.delete(data.name!);
|
|
|
|
break;
|
2022-10-15 17:02:56 +00:00
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
case "syscall":
|
|
|
|
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.postMessage({
|
|
|
|
type: "syscall-response",
|
|
|
|
id: data.id,
|
|
|
|
result: result,
|
|
|
|
} as WorkerMessage);
|
|
|
|
} catch (e: any) {
|
2022-03-28 13:25:05 +00:00
|
|
|
// console.error("Syscall fail", e);
|
2022-03-23 14:41:12 +00:00
|
|
|
this.worker.postMessage({
|
|
|
|
type: "syscall-response",
|
|
|
|
id: data.id,
|
|
|
|
error: e.message,
|
|
|
|
} as WorkerMessage);
|
|
|
|
}
|
|
|
|
break;
|
2022-10-15 17:02:56 +00:00
|
|
|
case "result": {
|
|
|
|
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(
|
2022-10-10 12:50:21 +00:00
|
|
|
new Error(`${data.error}\nStack trace: ${data.stack}`),
|
2022-05-13 15:05:52 +00:00
|
|
|
);
|
2022-03-23 14:41:12 +00:00
|
|
|
} else {
|
|
|
|
resultCbs && resultCbs.resolve(data.result);
|
|
|
|
}
|
|
|
|
break;
|
2022-10-15 17:02:56 +00:00
|
|
|
}
|
|
|
|
case "log": {
|
2023-01-14 17:51:00 +00:00
|
|
|
this.log(data.level!, data.message!);
|
2022-05-09 12:59:12 +00:00
|
|
|
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-01-14 17:51:00 +00:00
|
|
|
log(level: string, ...messageBits: any[]) {
|
|
|
|
const message = messageBits.map((a) => "" + a).join(" ");
|
|
|
|
this.logBuffer.push({
|
|
|
|
message,
|
|
|
|
level: level as LogLevel,
|
|
|
|
date: Date.now(),
|
|
|
|
});
|
|
|
|
if (this.logBuffer.length > this.maxLogBufferSize) {
|
|
|
|
this.logBuffer.shift();
|
|
|
|
}
|
|
|
|
console.log(`[Sandbox ${level}]`, message);
|
|
|
|
}
|
|
|
|
|
2022-10-15 17:02:56 +00:00
|
|
|
invoke(name: string, args: any[]): Promise<any> {
|
2022-03-23 14:41:12 +00:00
|
|
|
this.reqId++;
|
|
|
|
this.worker.postMessage({
|
|
|
|
type: "invoke",
|
|
|
|
id: this.reqId,
|
|
|
|
name,
|
|
|
|
args,
|
|
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.outstandingInvocations.set(this.reqId, { resolve, reject });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
this.worker.terminate();
|
|
|
|
}
|
|
|
|
}
|