import { Hook, Manifest, RuntimeEnvironment } from "./types.ts"; import { EventEmitter } from "./event.ts"; import { Sandbox, SandboxFactory } from "./sandbox.ts"; import { Plug } from "./plug.ts"; export interface SysCallMapping { [key: string]: (ctx: SyscallContext, ...args: any) => Promise | any; } export type SystemJSON = Manifest[]; export type SystemEvents = { plugLoaded: (plug: Plug) => void | Promise; sandboxInitialized(sandbox: Sandbox, plug: Plug): void | Promise; plugUnloaded: (name: string) => void | Promise; }; export type SyscallContext = { plug: Plug; }; type SyscallSignature = ( ctx: SyscallContext, ...args: any[] ) => Promise | any; type Syscall = { requiredPermissions: string[]; callback: SyscallSignature; }; export class System extends EventEmitter> { protected plugs = new Map>(); protected registeredSyscalls = new Map(); protected enabledHooks = new Set>(); constructor(readonly env?: RuntimeEnvironment) { super(); } get loadedPlugs(): Map> { return this.plugs; } addHook(feature: Hook) { this.enabledHooks.add(feature); feature.apply(this); } registerSyscalls( requiredCapabilities: string[], ...registrationObjects: SysCallMapping[] ) { for (const registrationObject of registrationObjects) { for (const [name, callback] of Object.entries(registrationObject)) { this.registeredSyscalls.set(name, { requiredPermissions: requiredCapabilities, callback, }); } } } syscallWithContext( ctx: SyscallContext, name: string, args: any[], ): Promise { const syscall = this.registeredSyscalls.get(name); if (!syscall) { throw Error(`Unregistered syscall ${name}`); } 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}`); } } return Promise.resolve(syscall.callback(ctx, ...args)); } localSyscall( contextPlugName: string, syscallName: string, args: any[], ): Promise { return this.syscallWithContext( // Mock the plug { plug: { name: contextPlugName } as any }, syscallName, args, ); } async load( manifest: Manifest, sandboxFactory: SandboxFactory, ): Promise> { const name = manifest.name; if (this.plugs.has(name)) { await this.unload(name); } // Validate let errors: string[] = []; for (const feature of this.enabledHooks) { errors = [...errors, ...feature.validateManifest(manifest)]; } if (errors.length > 0) { throw new Error(`Invalid manifest: ${errors.join(", ")}`); } // Ok, let's load this thing! const plug = new Plug(this, name, sandboxFactory); console.log("Loading", name); plug.load(manifest); this.plugs.set(name, plug); await this.emit("plugLoaded", plug); return plug; } async unload(name: string) { // console.log("Unloading", name); const plug = this.plugs.get(name); if (!plug) { throw Error(`Plug ${name} not found`); } await plug.stop(); this.emit("plugUnloaded", name); this.plugs.delete(name); } toJSON(): SystemJSON { const plugJSON: Manifest[] = []; for (const [_, plug] of this.plugs) { if (!plug.manifest) { continue; } plugJSON.push(plug.manifest); } return plugJSON; } async replaceAllFromJSON( json: SystemJSON, sandboxFactory: SandboxFactory, ) { await this.unloadAll(); for (const manifest of json) { // console.log("Loading plug", manifest.name); await this.load(manifest, sandboxFactory); } } unloadAll(): Promise { return Promise.all( Array.from(this.plugs.keys()).map(this.unload.bind(this)), ); } }