PlugOS refactor and other tweaks (#631)
* Prep for in-process plug loading (e.g. for CF workers, Deno Deploy) * Prototype of fixed in-process loading plugs * Fix: buttons not to scroll with content * Better positioning of modal especially on mobile * Move query caching outside query * Fix annoying mouse behavior when filter box appears * Page navigator search tweaks
This commit is contained in:
@@ -16,12 +16,7 @@ Deno.test("Run a plugos endpoint server", async () => {
|
||||
tempDir,
|
||||
);
|
||||
|
||||
await system.load(
|
||||
new URL(`file://${workerPath}`),
|
||||
"test",
|
||||
0,
|
||||
createSandbox,
|
||||
);
|
||||
await system.load("test", createSandbox(new URL(`file://${workerPath}`)));
|
||||
|
||||
const app = new Hono();
|
||||
const port = 3123;
|
||||
|
||||
@@ -70,7 +70,7 @@ export class EventHook implements Hook<EventHookT> {
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(
|
||||
`Error dispatching event ${eventName} to plug ${plug.name}: ${e.message}`,
|
||||
`Error dispatching event ${eventName} to ${plug.name}.${name}: ${e.message}`,
|
||||
);
|
||||
}
|
||||
})());
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import { Hook, Manifest } from "../types.ts";
|
||||
import { System } from "../system.ts";
|
||||
import { fullQueueName } from "../lib/mq_util.ts";
|
||||
import { MQMessage } from "$sb/types.ts";
|
||||
import { MessageQueue } from "../lib/mq.ts";
|
||||
import { throttle } from "$sb/lib/async.ts";
|
||||
@@ -61,7 +60,7 @@ export class MQHook implements Hook<MQHookT> {
|
||||
}
|
||||
const subscriptions = functionDef.mqSubscriptions;
|
||||
for (const subscriptionDef of subscriptions) {
|
||||
const queue = fullQueueName(plug.name!, subscriptionDef.queue);
|
||||
const queue = subscriptionDef.queue;
|
||||
// console.log("Subscribing to queue", queue);
|
||||
this.subscriptions.push(
|
||||
this.mq.subscribe(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assertEquals } from "https://deno.land/std@0.165.0/testing/asserts.ts";
|
||||
import { PrefixedKvPrimitives } from "./prefixed_kv_primitives.ts";
|
||||
|
||||
async function test(db: KvPrimitives) {
|
||||
const datastore = new DataStore(new PrefixedKvPrimitives(db, ["ds"]), false, {
|
||||
const datastore = new DataStore(new PrefixedKvPrimitives(db, ["ds"]), {
|
||||
count: (arr: any[]) => arr.length,
|
||||
});
|
||||
await datastore.set(["user", "peter"], { name: "Peter" });
|
||||
|
||||
+1
-26
@@ -2,18 +2,13 @@ import { applyQueryNoFilterKV, evalQueryExpression } from "$sb/lib/query.ts";
|
||||
import { FunctionMap, KV, KvKey, KvQuery } from "$sb/types.ts";
|
||||
import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts";
|
||||
import { KvPrimitives } from "./kv_primitives.ts";
|
||||
import { LimitedMap } from "../../common/limited_map.ts";
|
||||
|
||||
/**
|
||||
* This is the data store class you'll actually want to use, wrapping the primitives
|
||||
* in a more user-friendly way
|
||||
*/
|
||||
export class DataStore {
|
||||
private cache = new LimitedMap<any>(20);
|
||||
|
||||
constructor(
|
||||
readonly kv: KvPrimitives,
|
||||
private enableCache = false,
|
||||
private functionMap: FunctionMap = builtinFunctions,
|
||||
) {
|
||||
}
|
||||
@@ -63,21 +58,6 @@ export class DataStore {
|
||||
}
|
||||
|
||||
async query<T = any>(query: KvQuery): Promise<KV<T>[]> {
|
||||
let cacheKey: string | undefined;
|
||||
const cacheSecs = query.cacheSecs;
|
||||
// Should we do caching?
|
||||
if (cacheSecs && this.enableCache) {
|
||||
// Remove the cacheSecs from the query
|
||||
query = { ...query, cacheSecs: undefined };
|
||||
console.log("Going to cache query", query);
|
||||
cacheKey = JSON.stringify(query);
|
||||
const cachedResult = this.cache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
// Let's use the cached result
|
||||
return cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
const results: KV<T>[] = [];
|
||||
let itemCount = 0;
|
||||
// Accumulate results
|
||||
@@ -104,12 +84,7 @@ export class DataStore {
|
||||
}
|
||||
}
|
||||
// Apply order by, limit, and select
|
||||
const finalResult = applyQueryNoFilterKV(query, results, this.functionMap);
|
||||
if (cacheKey) {
|
||||
// Store in the cache
|
||||
this.cache.set(cacheKey, finalResult, cacheSecs! * 1000);
|
||||
}
|
||||
return finalResult;
|
||||
return applyQueryNoFilterKV(query, results, this.functionMap);
|
||||
}
|
||||
|
||||
async queryDelete(query: KvQuery): Promise<void> {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Adds a plug name to a queue name if it doesn't already have one.
|
||||
export function fullQueueName(plugName: string, queueName: string) {
|
||||
if (queueName.includes(".")) {
|
||||
return queueName;
|
||||
}
|
||||
return plugName + "." + queueName;
|
||||
}
|
||||
+1
-2
@@ -21,7 +21,6 @@ export class Plug<HookT> {
|
||||
|
||||
constructor(
|
||||
private system: System<HookT>,
|
||||
public workerUrl: URL | undefined,
|
||||
readonly name: string,
|
||||
private hash: number,
|
||||
private sandboxFactory: SandboxFactory<HookT>,
|
||||
@@ -44,7 +43,7 @@ export class Plug<HookT> {
|
||||
|
||||
// Invoke a syscall
|
||||
syscall(name: string, args: any[]): Promise<any> {
|
||||
return this.system.syscallWithContext({ plug: this }, name, args);
|
||||
return this.system.syscall({ plug: this.name }, name, args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+33
-11
@@ -1,20 +1,25 @@
|
||||
import { createSandbox } from "./sandboxes/deno_worker_sandbox.ts";
|
||||
import { System } from "./system.ts";
|
||||
import { assertEquals } from "../test_deps.ts";
|
||||
import { assert, assertEquals } from "../test_deps.ts";
|
||||
import { compileManifest } from "./compile.ts";
|
||||
import { esbuild } from "./deps.ts";
|
||||
import {
|
||||
createSandbox as createNoSandbox,
|
||||
runWithSystemLock,
|
||||
} from "./sandboxes/no_sandbox.ts";
|
||||
import { sleep } from "$sb/lib/async.ts";
|
||||
import { SysCallMapping } from "./system.ts";
|
||||
|
||||
Deno.test("Run a deno sandbox", async () => {
|
||||
const system = new System("server");
|
||||
system.registerSyscalls([], {
|
||||
addNumbers: (_ctx, a, b) => {
|
||||
console.log("This is the context", _ctx.plug.name);
|
||||
return a + b;
|
||||
},
|
||||
failingSyscall: () => {
|
||||
throw new Error("#fail");
|
||||
},
|
||||
});
|
||||
} as SysCallMapping);
|
||||
system.registerSyscalls(["restricted"], {
|
||||
restrictedSyscall: () => {
|
||||
return "restricted";
|
||||
@@ -34,10 +39,8 @@ Deno.test("Run a deno sandbox", async () => {
|
||||
);
|
||||
|
||||
const plug = await system.load(
|
||||
new URL(`file://${workerPath}`),
|
||||
"test",
|
||||
0,
|
||||
createSandbox,
|
||||
createSandbox(new URL(`file://${workerPath}`)),
|
||||
);
|
||||
|
||||
assertEquals({
|
||||
@@ -52,12 +55,31 @@ Deno.test("Run a deno sandbox", async () => {
|
||||
`file://${workerPath}`
|
||||
);
|
||||
|
||||
const plug2 = await system.loadNoSandbox("test", plugExport);
|
||||
const plug2 = await system.load("test", createNoSandbox(plugExport));
|
||||
|
||||
assertEquals({
|
||||
addedNumbers: 3,
|
||||
yamlMessage: "hello: world\n",
|
||||
}, await plug2.invoke("boot", []));
|
||||
let running = false;
|
||||
await Promise.all([
|
||||
runWithSystemLock(system, async () => {
|
||||
console.log("Starting first run");
|
||||
running = true;
|
||||
await sleep(5);
|
||||
assertEquals({
|
||||
addedNumbers: 3,
|
||||
yamlMessage: "hello: world\n",
|
||||
}, await plug2.invoke("boot", []));
|
||||
console.log("Done first run");
|
||||
running = false;
|
||||
}),
|
||||
runWithSystemLock(system, async () => {
|
||||
assert(!running);
|
||||
console.log("Starting second run");
|
||||
assertEquals({
|
||||
addedNumbers: 3,
|
||||
yamlMessage: "hello: world\n",
|
||||
}, await plug2.invoke("boot", []));
|
||||
console.log("Done second run");
|
||||
}),
|
||||
]);
|
||||
|
||||
await system.unloadAll();
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { WorkerSandbox } from "./worker_sandbox.ts";
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import type { SandboxFactory } 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,
|
||||
export function createSandbox<HookT>(workerUrl: URL): SandboxFactory<HookT> {
|
||||
return (plug) =>
|
||||
new WorkerSandbox(plug, workerUrl, {
|
||||
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);
|
||||
// Have to do this because the "deno" option is not standard and doesn't typecheck yet
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,38 @@ import { PromiseQueue } from "$sb/lib/async.ts";
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import { Manifest } from "../types.ts";
|
||||
import { System } from "../system.ts";
|
||||
import { SandboxFactory } from "./sandbox.ts";
|
||||
|
||||
/**
|
||||
* This implements a "no sandbox" sandbox that actually runs code the main thread, without any isolation.
|
||||
* This is useful for (often serverless) environments like CloudFlare workers and Deno Deploy that do not support workers.
|
||||
* Since these environments often also don't allow dynamic loading (or even eval'ing) of code, plug code needs to be
|
||||
* imported as a regular ESM module (which is possible).
|
||||
*
|
||||
* To make this work, a global `syscall` function needs to be injected into the global scope.
|
||||
* Since a syscall relies on a System, we need to track the active System in a global variable.
|
||||
* The issue with this is that it means that only a single System can be active at a given time per JS process.
|
||||
* To enforce this, we have a runWithSystemLock function that can be used to run code in a System-locked context, effectively queuing the execution of tasks sequentially.
|
||||
* This isn't great, but it's the best we can do.
|
||||
*
|
||||
* Luckily, in the only contexts in which you need to run plugs this way are serverless, where code will be
|
||||
* run in a bunch of isolates with hopefully low parallelism of requests per isolate.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A type representing the `plug` export of a plug, used via e.g. `import { plug } from "./some.plug.js`
|
||||
* Values of this type are passed into the `noSandboxFactory` function when called on a system.load
|
||||
*/
|
||||
export type PlugExport = {
|
||||
manifest: Manifest<any>;
|
||||
functionMapping: Record<string, (...args: any[]) => any>;
|
||||
};
|
||||
|
||||
// The global variable tracking the currently active system (if any)
|
||||
let activeSystem:
|
||||
| System<any>
|
||||
| undefined;
|
||||
|
||||
// We need to hard inject the syscall function into the global scope
|
||||
declare global {
|
||||
@@ -9,60 +41,76 @@ declare global {
|
||||
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");
|
||||
if (!activeSystem) {
|
||||
throw new Error(`No currently active system, can't invoke syscall ${name}`);
|
||||
}
|
||||
console.log("Calling syscall", name, args);
|
||||
return activePlug.syscall(name, args);
|
||||
// Invoke syscall with no active plug set (because we don't know which plug is invoking the syscall)
|
||||
return activeSystem.syscall({}, name, args);
|
||||
};
|
||||
|
||||
// Global sequential task queue for running tasks in a System-locked context
|
||||
const taskQueue = new PromiseQueue();
|
||||
|
||||
/**
|
||||
* Schedules a task to run in a System-locked context
|
||||
* in effect this will ensure only one such context is active at a given time allowing for no parallelism
|
||||
* @param system to activate while running the task
|
||||
* @param task callback to run
|
||||
* @returns the result of the task once it completes
|
||||
*/
|
||||
export function runWithSystemLock(
|
||||
system: System<any>,
|
||||
task: () => Promise<any>,
|
||||
): Promise<any> {
|
||||
return taskQueue.runInQueue(async () => {
|
||||
// Set the global active system, which is used by the syscall function
|
||||
activeSystem = system;
|
||||
try {
|
||||
// Run the logic, note putting the await here is crucial to make sure the `finally` block runs at the right time
|
||||
return await task();
|
||||
} finally {
|
||||
// And then reset the global active system whether the thing blew up or not
|
||||
activeSystem = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a no-sandbox sandbox that runs code in the main thread
|
||||
*/
|
||||
export class NoSandbox<HookT> implements Sandbox<HookT> {
|
||||
manifest?: Manifest<HookT> | undefined;
|
||||
manifest: Manifest<HookT>;
|
||||
|
||||
constructor(
|
||||
private plug: Plug<HookT>,
|
||||
private plugExport: PlugExport<HookT>,
|
||||
readonly plug: Plug<HookT>,
|
||||
readonly plugExport: PlugExport,
|
||||
) {
|
||||
this.manifest = plugExport.manifest;
|
||||
plug.manifest = this.manifest;
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
// Nothing to do
|
||||
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;
|
||||
}
|
||||
});
|
||||
const fn = this.plugExport.functionMapping[name];
|
||||
if (!fn) {
|
||||
throw new Error(`Function not defined: ${name}`);
|
||||
}
|
||||
return Promise.resolve(fn(...args));
|
||||
}
|
||||
|
||||
stop() {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
export function noSandboxFactory<HookT>(
|
||||
plugExport: PlugExport<HookT>,
|
||||
): (plug: Plug<HookT>) => Sandbox<HookT> {
|
||||
return (plug: Plug<HookT>) => new NoSandbox(plug, plugExport);
|
||||
export function createSandbox<HookT>(
|
||||
plugExport: PlugExport,
|
||||
): SandboxFactory<HookT> {
|
||||
return (plug: Plug<any>) => new NoSandbox(plug, plugExport);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkerSandbox } from "./worker_sandbox.ts";
|
||||
import type { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import type { SandboxFactory } from "./sandbox.ts";
|
||||
|
||||
export function createSandbox<HookT>(plug: Plug<HookT>): Sandbox<HookT> {
|
||||
return new WorkerSandbox(plug);
|
||||
export function createSandbox<HookT>(workerUrl: URL): SandboxFactory<HookT> {
|
||||
return (plug: Plug<HookT>) => new WorkerSandbox(plug, workerUrl);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
|
||||
|
||||
constructor(
|
||||
readonly plug: Plug<HookT>,
|
||||
public workerUrl: URL,
|
||||
private workerOptions = {},
|
||||
) {
|
||||
}
|
||||
@@ -35,7 +36,7 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
|
||||
console.warn("Double init of sandbox, ignoring");
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.worker = new Worker(this.plug.workerUrl!, {
|
||||
this.worker = new Worker(this.workerUrl, {
|
||||
...this.workerOptions,
|
||||
type: "module",
|
||||
});
|
||||
|
||||
@@ -2,11 +2,8 @@ import { SysCallMapping, System } from "../system.ts";
|
||||
|
||||
export default function assetSyscalls(system: System<any>): SysCallMapping {
|
||||
return {
|
||||
"asset.readAsset": (
|
||||
ctx,
|
||||
name: string,
|
||||
): string => {
|
||||
return system.loadedPlugs.get(ctx.plug.name!)!.assets!.readFileAsDataUrl(
|
||||
"asset.readAsset": (_ctx, plugName: string, name: string): string => {
|
||||
return system.loadedPlugs.get(plugName)!.assets!.readFileAsDataUrl(
|
||||
name,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,75 +1,47 @@
|
||||
import { KV, KvKey, KvQuery } from "$sb/types.ts";
|
||||
import type { DataStore } from "../lib/datastore.ts";
|
||||
import type { SyscallContext, SysCallMapping } from "../system.ts";
|
||||
import type { SysCallMapping } from "../system.ts";
|
||||
|
||||
/**
|
||||
* Exposes the datastore API to plugs, but scoping everything to a prefix based on the plug's name
|
||||
* @param ds the datastore to wrap
|
||||
* @param prefix prefix to scope all keys to to which the plug name will be appended
|
||||
*/
|
||||
export function dataStoreSyscalls(
|
||||
ds: DataStore,
|
||||
prefix: KvKey = ["ds"],
|
||||
): SysCallMapping {
|
||||
export function dataStoreSyscalls(ds: DataStore): SysCallMapping {
|
||||
return {
|
||||
"datastore.delete": (ctx, key: KvKey) => {
|
||||
return ds.delete(applyPrefix(ctx, key));
|
||||
"datastore.delete": (_ctx, key: KvKey) => {
|
||||
return ds.delete(key);
|
||||
},
|
||||
|
||||
"datastore.set": (ctx, key: KvKey, value: any) => {
|
||||
return ds.set(applyPrefix(ctx, key), value);
|
||||
"datastore.set": (_ctx, key: KvKey, value: any) => {
|
||||
return ds.set(key, value);
|
||||
},
|
||||
|
||||
"datastore.batchSet": (ctx, kvs: KV[]) => {
|
||||
return ds.batchSet(
|
||||
kvs.map((kv) => ({ key: applyPrefix(ctx, kv.key), value: kv.value })),
|
||||
);
|
||||
"datastore.batchSet": (_ctx, kvs: KV[]) => {
|
||||
return ds.batchSet(kvs);
|
||||
},
|
||||
|
||||
"datastore.batchDelete": (ctx, keys: KvKey[]) => {
|
||||
return ds.batchDelete(keys.map((k) => applyPrefix(ctx, k)));
|
||||
"datastore.batchDelete": (_ctx, keys: KvKey[]) => {
|
||||
return ds.batchDelete(keys);
|
||||
},
|
||||
|
||||
"datastore.batchGet": (
|
||||
ctx,
|
||||
_ctx,
|
||||
keys: KvKey[],
|
||||
): Promise<(any | undefined)[]> => {
|
||||
return ds.batchGet(keys.map((k) => applyPrefix(ctx, k)));
|
||||
return ds.batchGet(keys);
|
||||
},
|
||||
|
||||
"datastore.get": (ctx, key: KvKey): Promise<any | null> => {
|
||||
return ds.get(applyPrefix(ctx, key));
|
||||
"datastore.get": (_ctx, key: KvKey): Promise<any | null> => {
|
||||
return ds.get(key);
|
||||
},
|
||||
|
||||
"datastore.query": async (
|
||||
ctx,
|
||||
query: KvQuery,
|
||||
): Promise<KV[]> => {
|
||||
return (await ds.query({
|
||||
...query,
|
||||
prefix: applyPrefix(ctx, query.prefix),
|
||||
})).map((kv) => ({
|
||||
key: stripPrefix(kv.key),
|
||||
value: kv.value,
|
||||
}));
|
||||
"datastore.query": async (_ctx, query: KvQuery): Promise<KV[]> => {
|
||||
return (await ds.query(query));
|
||||
},
|
||||
|
||||
"datastore.queryDelete": (
|
||||
ctx,
|
||||
query: KvQuery,
|
||||
): Promise<void> => {
|
||||
return ds.queryDelete({
|
||||
...query,
|
||||
prefix: applyPrefix(ctx, query.prefix),
|
||||
});
|
||||
"datastore.queryDelete": (_ctx, query: KvQuery): Promise<void> => {
|
||||
return ds.queryDelete(query);
|
||||
},
|
||||
};
|
||||
|
||||
function applyPrefix(ctx: SyscallContext, key?: KvKey): KvKey {
|
||||
return [...prefix, ctx.plug.name!, ...(key ? key : [])];
|
||||
}
|
||||
|
||||
function stripPrefix(key: KvKey): KvKey {
|
||||
return key.slice(prefix.length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,13 @@ import { assert } from "../../test_deps.ts";
|
||||
import { path } from "../deps.ts";
|
||||
import fileSystemSyscalls from "./fs.deno.ts";
|
||||
|
||||
const fakeCtx = {} as any;
|
||||
|
||||
Deno.test("Test FS operations", async () => {
|
||||
const thisFolder = path.resolve(
|
||||
path.dirname(new URL(import.meta.url).pathname),
|
||||
);
|
||||
const syscalls = fileSystemSyscalls(thisFolder);
|
||||
const allFiles: FileMeta[] = await syscalls["fs.listFiles"](
|
||||
fakeCtx,
|
||||
{},
|
||||
thisFolder,
|
||||
true,
|
||||
);
|
||||
|
||||
+10
-11
@@ -1,25 +1,24 @@
|
||||
import { SysCallMapping } from "../system.ts";
|
||||
import { fullQueueName } from "../lib/mq_util.ts";
|
||||
import { MessageQueue } from "../lib/mq.ts";
|
||||
|
||||
export function mqSyscalls(
|
||||
mq: MessageQueue,
|
||||
): SysCallMapping {
|
||||
return {
|
||||
"mq.send": (ctx, queue: string, body: any) => {
|
||||
return mq.send(fullQueueName(ctx.plug.name!, queue), body);
|
||||
"mq.send": (_ctx, queue: string, body: any) => {
|
||||
return mq.send(queue, body);
|
||||
},
|
||||
"mq.batchSend": (ctx, queue: string, bodies: any[]) => {
|
||||
return mq.batchSend(fullQueueName(ctx.plug.name!, queue), bodies);
|
||||
"mq.batchSend": (_ctx, queue: string, bodies: any[]) => {
|
||||
return mq.batchSend(queue, bodies);
|
||||
},
|
||||
"mq.ack": (ctx, queue: string, id: string) => {
|
||||
return mq.ack(fullQueueName(ctx.plug.name!, queue), id);
|
||||
"mq.ack": (_ctx, queue: string, id: string) => {
|
||||
return mq.ack(queue, id);
|
||||
},
|
||||
"mq.batchAck": (ctx, queue: string, ids: string[]) => {
|
||||
return mq.batchAck(fullQueueName(ctx.plug.name!, queue), ids);
|
||||
"mq.batchAck": (_ctx, queue: string, ids: string[]) => {
|
||||
return mq.batchAck(queue, ids);
|
||||
},
|
||||
"mq.getQueueStats": (ctx, queue: string) => {
|
||||
return mq.getQueueStats(fullQueueName(ctx.plug.name!, queue));
|
||||
"mq.getQueueStats": (_ctx, queue: string) => {
|
||||
return mq.getQueueStats(queue);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { SyscallContext, SysCallMapping } from "../system.ts";
|
||||
|
||||
export function proxySyscalls(
|
||||
names: string[],
|
||||
transportCall: (
|
||||
ctx: SyscallContext,
|
||||
name: string,
|
||||
...args: any[]
|
||||
) => Promise<any>,
|
||||
): SysCallMapping {
|
||||
const syscalls: SysCallMapping = {};
|
||||
|
||||
for (const name of names) {
|
||||
syscalls[name] = (ctx, ...args: any[]) => {
|
||||
return transportCall(ctx, name, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
return syscalls;
|
||||
}
|
||||
+21
-62
@@ -3,7 +3,6 @@ import { EventEmitter } from "./event.ts";
|
||||
import type { SandboxFactory } from "./sandboxes/sandbox.ts";
|
||||
import { Plug } from "./plug.ts";
|
||||
import { InMemoryManifestCache, ManifestCache } from "./manifest_cache.ts";
|
||||
import { noSandboxFactory, PlugExport } from "./sandboxes/no_sandbox.ts";
|
||||
|
||||
export interface SysCallMapping {
|
||||
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
|
||||
@@ -16,11 +15,12 @@ export type SystemEvents<HookT> = {
|
||||
|
||||
// Passed to every syscall, allows to pass in additional context that the syscall may use
|
||||
export type SyscallContext = {
|
||||
plug: Plug<any>;
|
||||
// This is the plug that is invoking the syscall,
|
||||
// which may be undefined where this cannot be determined (e.g. when running in a NoSandbox)
|
||||
plug?: string;
|
||||
};
|
||||
|
||||
type SyscallSignature = (
|
||||
ctx: SyscallContext,
|
||||
...args: any[]
|
||||
) => Promise<any> | any;
|
||||
|
||||
@@ -75,7 +75,11 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
}
|
||||
}
|
||||
|
||||
syscallWithContext(
|
||||
localSyscall(name: string, args: any): Promise<any> {
|
||||
return this.syscall({}, name, args);
|
||||
}
|
||||
|
||||
syscall(
|
||||
ctx: SyscallContext,
|
||||
name: string,
|
||||
args: any[],
|
||||
@@ -84,36 +88,29 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
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) {
|
||||
// Only when running in a plug context do we check permissions
|
||||
const plug = this.loadedPlugs.get(ctx.plug!);
|
||||
if (!plug) {
|
||||
throw new Error(
|
||||
`Plug ${ctx.plug} not found while attempting to invoke ${name}}`,
|
||||
);
|
||||
}
|
||||
if (!ctx.plug.grantedPermissions.includes(permission)) {
|
||||
throw Error(`Missing permission '${permission}' for syscall ${name}`);
|
||||
for (const permission of syscall.requiredPermissions) {
|
||||
if (!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<any> {
|
||||
return this.syscallWithContext(
|
||||
{ plug: this.plugs.get(contextPlugName)! },
|
||||
syscallName,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
async load(
|
||||
workerUrl: URL,
|
||||
name: string,
|
||||
hash: number,
|
||||
sandboxFactory: SandboxFactory<HookT>,
|
||||
hash = -1,
|
||||
): Promise<Plug<HookT>> {
|
||||
const plug = new Plug(this, workerUrl, name, hash, sandboxFactory);
|
||||
const plug = new Plug(this, name, hash, sandboxFactory);
|
||||
|
||||
// Wait for worker to boot, and pass back its manifest
|
||||
await plug.ready;
|
||||
@@ -139,44 +136,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
return plug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a plug without a sandbox, which means it will run in the same context as the caller
|
||||
* @param name
|
||||
* @param plugExport extracted via e.g. `import { plug } from "./some.plug.js`
|
||||
* @returns Plug instance
|
||||
*/
|
||||
async loadNoSandbox(
|
||||
name: string,
|
||||
plugExport: PlugExport<HookT>,
|
||||
): Promise<Plug<HookT>> {
|
||||
const plug = new Plug(
|
||||
this,
|
||||
undefined,
|
||||
name,
|
||||
-1,
|
||||
noSandboxFactory(plugExport),
|
||||
);
|
||||
|
||||
const manifest = plugExport.manifest;
|
||||
|
||||
// Validate the manifest
|
||||
let errors: string[] = [];
|
||||
for (const feature of this.enabledHooks) {
|
||||
errors = [...errors, ...feature.validateManifest(plug.manifest!)];
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid manifest: ${errors.join(", ")}`);
|
||||
}
|
||||
if (this.plugs.has(manifest.name)) {
|
||||
this.unload(manifest.name);
|
||||
}
|
||||
console.log("Activated plug without sandbox", manifest.name);
|
||||
this.plugs.set(manifest.name, plug);
|
||||
|
||||
await this.emit("plugLoaded", plug);
|
||||
return plug;
|
||||
}
|
||||
|
||||
unload(name: string) {
|
||||
const plug = this.plugs.get(name);
|
||||
if (!plug) {
|
||||
|
||||
@@ -9,6 +9,20 @@ declare global {
|
||||
function syscall(name: string, ...args: any[]): Promise<any>;
|
||||
}
|
||||
|
||||
// Are we running in a (web) worker?
|
||||
|
||||
// Determines if we're running in a web worker environment (Deno or browser)
|
||||
// - in a browser's main threads, typeof window is "object"
|
||||
// - in a browser's worker threads, typeof window === "undefined"
|
||||
// - in Deno's main thread typeof window === "object"
|
||||
// - in Deno's workers typeof window === "undefined
|
||||
// - in Cloudflare workers typeof window === "undefined", but typeof globalThis.WebSocketPair is defined
|
||||
const runningAsWebWorker = typeof window === "undefined" &&
|
||||
// @ts-ignore: globalThis
|
||||
typeof globalThis.WebSocketPair === "undefined";
|
||||
|
||||
// console.log("Running as web worker:", runningAsWebWorker);
|
||||
|
||||
if (typeof Deno === "undefined") {
|
||||
// @ts-ignore: Deno hack
|
||||
self.Deno = {
|
||||
@@ -35,13 +49,11 @@ const pendingRequests = new Map<
|
||||
|
||||
let syscallReqId = 0;
|
||||
|
||||
const workerMode = typeof window === "undefined";
|
||||
|
||||
function workerPostMessage(msg: ControllerMessage) {
|
||||
self.postMessage(msg);
|
||||
}
|
||||
|
||||
if (workerMode) {
|
||||
if (runningAsWebWorker) {
|
||||
globalThis.syscall = async (name: string, ...args: any[]) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
syscallReqId++;
|
||||
@@ -61,7 +73,7 @@ export function setupMessageListener(
|
||||
functionMapping: Record<string, Function>,
|
||||
manifest: any,
|
||||
) {
|
||||
if (!workerMode) {
|
||||
if (!runningAsWebWorker) {
|
||||
// Don't do any of this stuff if this is not a web worker
|
||||
// This caters to the NoSandbox run mode
|
||||
return;
|
||||
@@ -163,10 +175,10 @@ export async function sandboxFetch(
|
||||
return syscall("sandboxFetch.fetch", reqInfo, options);
|
||||
}
|
||||
|
||||
// @ts-ignore: monkey patching fetch
|
||||
globalThis.nativeFetch = globalThis.fetch;
|
||||
// Monkey patch fetch()
|
||||
|
||||
export function monkeyPatchFetch() {
|
||||
globalThis.nativeFetch = globalThis.fetch;
|
||||
// @ts-ignore: monkey patching fetch
|
||||
globalThis.fetch = async function (
|
||||
reqInfo: RequestInfo,
|
||||
@@ -192,4 +204,6 @@ export function monkeyPatchFetch() {
|
||||
};
|
||||
}
|
||||
|
||||
monkeyPatchFetch();
|
||||
if (runningAsWebWorker) {
|
||||
monkeyPatchFetch();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user