2022-03-23 14:41:12 +00:00
|
|
|
import { Feature, Manifest, RuntimeEnvironment } from "./types";
|
|
|
|
import { EventEmitter } from "../common/event";
|
2022-03-25 11:03:06 +00:00
|
|
|
import { SandboxFactory } from "./sandbox";
|
2022-03-23 14:41:12 +00:00
|
|
|
import { Plug } from "./plug";
|
|
|
|
|
2022-03-24 09:48:56 +00:00
|
|
|
export interface SysCallMapping {
|
2022-03-25 11:03:06 +00:00
|
|
|
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export type SystemJSON<HookT> = { [key: string]: Manifest<HookT> };
|
2022-03-24 09:48:56 +00:00
|
|
|
|
2022-03-23 14:41:12 +00:00
|
|
|
export type SystemEvents<HookT> = {
|
|
|
|
plugLoaded: (name: string, plug: Plug<HookT>) => void;
|
|
|
|
plugUnloaded: (name: string, plug: Plug<HookT>) => void;
|
|
|
|
};
|
|
|
|
|
2022-03-25 11:03:06 +00:00
|
|
|
type SyscallContext = {
|
|
|
|
plug: Plug<any> | null;
|
|
|
|
};
|
|
|
|
|
|
|
|
type SyscallSignature = (
|
|
|
|
ctx: SyscallContext,
|
|
|
|
...args: any[]
|
|
|
|
) => Promise<any> | any;
|
|
|
|
|
|
|
|
type Syscall = {
|
|
|
|
requiredPermissions: string[];
|
|
|
|
callback: SyscallSignature;
|
|
|
|
};
|
|
|
|
|
2022-03-23 14:41:12 +00:00
|
|
|
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|
|
|
protected plugs = new Map<string, Plug<HookT>>();
|
2022-03-25 11:03:06 +00:00
|
|
|
protected registeredSyscalls = new Map<string, Syscall>();
|
2022-03-23 14:41:12 +00:00
|
|
|
protected enabledFeatures = new Set<Feature<HookT>>();
|
|
|
|
|
|
|
|
readonly runtimeEnv: RuntimeEnvironment;
|
|
|
|
|
|
|
|
constructor(env: RuntimeEnvironment) {
|
|
|
|
super();
|
|
|
|
this.runtimeEnv = env;
|
|
|
|
}
|
|
|
|
|
|
|
|
addFeature(feature: Feature<HookT>) {
|
|
|
|
this.enabledFeatures.add(feature);
|
|
|
|
feature.apply(this);
|
|
|
|
}
|
|
|
|
|
2022-03-25 11:03:06 +00:00
|
|
|
registerSyscalls(
|
|
|
|
namespace: string,
|
|
|
|
requiredCapabilities: string[],
|
|
|
|
...registrationObjects: SysCallMapping[]
|
|
|
|
) {
|
2022-03-23 14:41:12 +00:00
|
|
|
for (const registrationObject of registrationObjects) {
|
2022-03-25 11:03:06 +00:00
|
|
|
for (let [name, callback] of Object.entries(registrationObject)) {
|
|
|
|
const callName = namespace ? `${namespace}.${name}` : name;
|
|
|
|
this.registeredSyscalls.set(callName, {
|
|
|
|
requiredPermissions: requiredCapabilities,
|
|
|
|
callback,
|
|
|
|
});
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-25 11:03:06 +00:00
|
|
|
async syscallWithContext(
|
|
|
|
ctx: SyscallContext,
|
|
|
|
name: string,
|
|
|
|
args: any[]
|
|
|
|
): Promise<any> {
|
|
|
|
const syscall = this.registeredSyscalls.get(name);
|
|
|
|
if (!syscall) {
|
2022-03-23 14:41:12 +00:00
|
|
|
throw Error(`Unregistered syscall ${name}`);
|
|
|
|
}
|
2022-03-25 11:03:06 +00:00
|
|
|
for (const permission of syscall.requiredPermissions) {
|
|
|
|
if (!ctx.plug) {
|
|
|
|
throw Error(`Syscall ${name} requires permission and no plug is set`);
|
|
|
|
}
|
|
|
|
if (!ctx.plug.grantedPermissions.includes(permission)) {
|
|
|
|
throw Error(`Missing permission '${permission}' for syscall ${name}`);
|
|
|
|
}
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
2022-03-25 11:03:06 +00:00
|
|
|
return Promise.resolve(syscall.callback(ctx, ...args));
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async load(
|
|
|
|
name: string,
|
|
|
|
manifest: Manifest<HookT>,
|
2022-03-25 11:03:06 +00:00
|
|
|
sandboxFactory: SandboxFactory<HookT>
|
2022-03-23 14:41:12 +00:00
|
|
|
): Promise<Plug<HookT>> {
|
|
|
|
if (this.plugs.has(name)) {
|
|
|
|
await this.unload(name);
|
|
|
|
}
|
|
|
|
// Validate
|
|
|
|
let errors: string[] = [];
|
|
|
|
for (const feature of this.enabledFeatures) {
|
|
|
|
errors = [...errors, ...feature.validateManifest(manifest)];
|
|
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
|
|
throw new Error(`Invalid manifest: ${errors.join(", ")}`);
|
|
|
|
}
|
|
|
|
// Ok, let's load this thing!
|
2022-03-25 11:03:06 +00:00
|
|
|
const plug = new Plug(this, name, sandboxFactory);
|
2022-03-23 14:41:12 +00:00
|
|
|
await plug.load(manifest);
|
|
|
|
this.plugs.set(name, plug);
|
|
|
|
this.emit("plugLoaded", name, plug);
|
|
|
|
return plug;
|
|
|
|
}
|
|
|
|
|
|
|
|
async unload(name: string) {
|
|
|
|
const plug = this.plugs.get(name);
|
|
|
|
if (!plug) {
|
|
|
|
throw Error(`Plug ${name} not found`);
|
|
|
|
}
|
|
|
|
await plug.stop();
|
|
|
|
this.emit("plugUnloaded", name, plug);
|
|
|
|
this.plugs.delete(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
get loadedPlugs(): Map<string, Plug<HookT>> {
|
|
|
|
return this.plugs;
|
|
|
|
}
|
|
|
|
|
|
|
|
toJSON(): SystemJSON<HookT> {
|
|
|
|
let plugJSON: { [key: string]: Manifest<HookT> } = {};
|
|
|
|
for (let [name, plug] of this.plugs) {
|
|
|
|
if (!plug.manifest) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
plugJSON[name] = plug.manifest;
|
|
|
|
}
|
|
|
|
return plugJSON;
|
|
|
|
}
|
|
|
|
|
|
|
|
async replaceAllFromJSON(
|
|
|
|
json: SystemJSON<HookT>,
|
2022-03-25 11:03:06 +00:00
|
|
|
sandboxFactory: SandboxFactory<HookT>
|
2022-03-23 14:41:12 +00:00
|
|
|
) {
|
|
|
|
await this.unloadAll();
|
|
|
|
for (let [name, manifest] of Object.entries(json)) {
|
|
|
|
console.log("Loading plug", name);
|
2022-03-25 11:03:06 +00:00
|
|
|
await this.load(name, manifest, sandboxFactory);
|
2022-03-23 14:41:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async unloadAll(): Promise<void[]> {
|
|
|
|
return Promise.all(
|
|
|
|
Array.from(this.plugs.keys()).map(this.unload.bind(this))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|