WIP: CLI running of plugs

This commit is contained in:
Zef Hemel
2023-08-04 18:56:55 +02:00
parent de3e385017
commit 3464af0252
35 changed files with 738 additions and 136 deletions
+38
View File
@@ -0,0 +1,38 @@
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { compileManifest } from "../plugos/compile.ts";
import { esbuild } from "../plugos/deps.ts";
import { runPlug } from "./plug_run.ts";
import assets from "../dist/plug_asset_bundle.json" assert {
type: "json",
};
import { assertEquals } from "../test_deps.ts";
import { path } from "../common/deps.ts";
Deno.test("Test plug run", async () => {
// const tempDir = await Deno.makeTempDir();
const assetBundle = new AssetBundle(assets);
const testFolder = path.dirname(new URL(import.meta.url).pathname);
const testSpaceFolder = path.join(testFolder, "test_space");
const plugFolder = path.join(testSpaceFolder, "_plug");
await Deno.mkdir(plugFolder, { recursive: true });
await compileManifest(
path.join(testFolder, "test.plug.yaml"),
plugFolder,
);
assertEquals(
await runPlug(
testSpaceFolder,
"test.run",
[],
assetBundle,
),
"Hello",
);
// await Deno.remove(tempDir, { recursive: true });
esbuild.stop();
});
+145
View File
@@ -0,0 +1,145 @@
import { path } from "../common/deps.ts";
import { PlugNamespaceHook } from "../common/hooks/plug_namespace.ts";
import { SilverBulletHooks } from "../common/manifest.ts";
import { loadMarkdownExtensions } from "../common/markdown_parser/markdown_ext.ts";
import buildMarkdown from "../common/markdown_parser/parser.ts";
import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts";
import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts";
import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts";
import { PlugSpacePrimitives } from "../common/spaces/plug_space_primitives.ts";
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { createSandbox } from "../plugos/environments/deno_sandbox.ts";
import { CronHook } from "../plugos/hooks/cron.ts";
import { EventHook } from "../plugos/hooks/event.ts";
import { JSONKVStore } from "../plugos/lib/kv_store.json_file.ts";
import assetSyscalls from "../plugos/syscalls/asset.ts";
import { eventSyscalls } from "../plugos/syscalls/event.ts";
import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts";
import { shellSyscalls } from "../plugos/syscalls/shell.deno.ts";
import { storeSyscalls } from "../plugos/syscalls/store.ts";
import { System } from "../plugos/system.ts";
import { Space } from "../web/space.ts";
import { debugSyscalls } from "../web/syscalls/debug.ts";
import { markdownSyscalls } from "../web/syscalls/markdown.ts";
import { systemSyscalls } from "../web/syscalls/system.ts";
import { yamlSyscalls } from "../web/syscalls/yaml.ts";
import { pageIndexSyscalls } from "./syscalls/index.ts";
import { spaceSyscalls } from "./syscalls/space.ts";
export async function runPlug(
spacePath: string,
functionName: string,
args: string[] = [],
builtinAssetBundle: AssetBundle,
indexFirst = false,
) {
spacePath = path.resolve(spacePath);
const system = new System<SilverBulletHooks>("cli");
// Event hook
const eventHook = new EventHook();
system.addHook(eventHook);
// Cron hook
const cronHook = new CronHook(system);
system.addHook(cronHook);
const pageIndexCalls = pageIndexSyscalls("run.db");
// TODO: Add endpoint
const plugNamespaceHook = new PlugNamespaceHook();
system.addHook(plugNamespaceHook);
const spacePrimitives = new FileMetaSpacePrimitives(
new EventedSpacePrimitives(
new PlugSpacePrimitives(
new DiskSpacePrimitives(spacePath),
plugNamespaceHook,
),
eventHook,
),
pageIndexCalls,
);
const kvStore = new JSONKVStore();
const space = new Space(spacePrimitives, kvStore);
// Add syscalls
system.registerSyscalls(
[],
eventSyscalls(eventHook),
spaceSyscalls(space),
assetSyscalls(system),
yamlSyscalls(),
storeSyscalls(kvStore),
systemSyscalls(undefined as any, system),
pageIndexCalls,
debugSyscalls(),
markdownSyscalls(buildMarkdown([])), // Will later be replaced with markdown extensions
);
// Syscalls that require some additional permissions
system.registerSyscalls(
["fetch"],
sandboxFetchSyscalls(),
);
system.registerSyscalls(
["shell"],
shellSyscalls("."),
);
await loadPlugsFromAssetBundle(system, builtinAssetBundle);
for (let plugPath of await space.listPlugs()) {
plugPath = path.resolve(spacePath, plugPath);
await system.load(
new URL(`file://${plugPath}`),
createSandbox,
);
}
// Load markdown syscalls based on all new syntax (if any)
system.registerSyscalls(
[],
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(system))),
);
if (indexFirst) {
await system.loadedPlugs.get("core")!.invoke("reindexSpace", []);
}
const [plugName, funcName] = functionName.split(".");
const plug = system.loadedPlugs.get(plugName);
if (!plug) {
throw new Error(`Plug ${plugName} not found`);
}
const result = await plug.invoke(funcName, args);
await system.unloadAll();
await pageIndexCalls["index.close"]({} as any);
return result;
}
async function loadPlugsFromAssetBundle(
system: System<any>,
assetBundle: AssetBundle,
) {
const tempDir = await Deno.makeTempDir();
try {
for (const filePath of assetBundle.listFiles()) {
if (filePath.endsWith(".plug.js")) {
const plugPath = path.join(tempDir, filePath);
await Deno.mkdir(path.dirname(plugPath), { recursive: true });
await Deno.writeFile(plugPath, assetBundle.readFileSync(filePath));
await system.load(
new URL(`file://${plugPath}`),
createSandbox,
);
}
}
} finally {
await Deno.remove(tempDir, { recursive: true });
}
}
+7
View File
@@ -0,0 +1,7 @@
import { index } from "$sb/silverbullet-syscall/mod.ts";
export async function run() {
console.log("Hello from plug_test.ts");
console.log(await index.queryPrefix(`tag:`));
return "Hello";
}
+34
View File
@@ -0,0 +1,34 @@
import { assertEquals } from "../../test_deps.ts";
import { pageIndexSyscalls } from "./index.ts";
Deno.test("Test KV index", async () => {
const ctx: any = {};
const calls = pageIndexSyscalls();
await calls["index.set"](ctx, "page", "test", "value");
assertEquals(await calls["index.get"](ctx, "page", "test"), "value");
await calls["index.delete"](ctx, "page", "test");
assertEquals(await calls["index.get"](ctx, "page", "test"), null);
await calls["index.batchSet"](ctx, "page", [{
key: "attr:test",
value: "value",
}, {
key: "attr:test2",
value: "value2",
}, { key: "random", value: "value3" }]);
await calls["index.batchSet"](ctx, "page2", [{
key: "attr:test",
value: "value",
}, {
key: "attr:test2",
value: "value2",
}, { key: "random", value: "value3" }]);
let results = await calls["index.queryPrefix"](ctx, "attr:");
assertEquals(results.length, 4);
await calls["index.clearPageIndexForPage"](ctx, "page");
results = await calls["index.queryPrefix"](ctx, "attr:");
assertEquals(results.length, 2);
await calls["index.clearPageIndex"](ctx);
results = await calls["index.queryPrefix"](ctx, "");
assertEquals(results.length, 0);
await calls["index.close"](ctx);
});
+103
View File
@@ -0,0 +1,103 @@
/// <reference lib="deno.unstable" />
import type { SysCallMapping } from "../../plugos/system.ts";
type Item = {
page: string;
key: string;
value: any;
};
export type KV = {
key: string;
value: any;
};
// Keyspace:
// ["index", page, key] -> value
// ["indexByKey", key, page] -> value
/**
* Implements the index syscalls using Deno's KV store.
* @param dbFile
* @returns
*/
export function pageIndexSyscalls(dbFile?: string): SysCallMapping {
const kv = Deno.openKv(dbFile);
const apiObj: SysCallMapping = {
"index.set": async (_ctx, page: string, key: string, value: any) => {
const res = await (await kv).atomic()
.set(["index", page, key], value)
.set(["indexByKey", key, page], value)
.commit();
if (!res.ok) {
throw res;
}
},
"index.batchSet": async (_ctx, page: string, kvs: KV[]) => {
// await items.bulkPut(kvs);
for (const { key, value } of kvs) {
await apiObj["index.set"](_ctx, page, key, value);
}
},
"index.delete": async (_ctx, page: string, key: string) => {
const res = await (await kv).atomic()
.delete(["index", page, key])
.delete(["indexByKey", key, page])
.commit();
if (!res.ok) {
throw res;
}
},
"index.get": async (_ctx, page: string, key: string) => {
return (await (await kv).get(["index", page, key])).value;
},
"index.queryPrefix": async (_ctx, prefix: string) => {
const results: { key: string; page: string; value: any }[] = [];
for await (
const result of (await kv).list({
start: ["indexByKey", prefix],
end: [
"indexByKey",
prefix.slice(0, -1) +
// This is a hack to get the next character in the ASCII table (e.g. "a" -> "b")
String.fromCharCode(prefix.charCodeAt(prefix.length - 1) + 1),
],
})
) {
results.push({
key: result.key[1] as string,
page: result.key[2] as string,
value: result.value,
});
}
return results;
},
"index.clearPageIndexForPage": async (ctx, page: string) => {
await apiObj["index.deletePrefixForPage"](ctx, page, "");
},
"index.deletePrefixForPage": async (_ctx, page: string, prefix: string) => {
for await (
const result of (await kv).list({
start: ["index", page, prefix],
end: ["index", page, prefix + "~"],
})
) {
await apiObj["index.delete"](_ctx, page, result.key[2]);
}
},
"index.clearPageIndex": async (ctx) => {
for await (
const result of (await kv).list({
prefix: ["index"],
})
) {
await apiObj["index.delete"](ctx, result.key[1], result.key[2]);
}
},
"index.close": async () => {
(await kv).close();
},
};
return apiObj;
}
+61
View File
@@ -0,0 +1,61 @@
import { SysCallMapping } from "../../plugos/system.ts";
import type { Space } from "../../web/space.ts";
import { AttachmentMeta, PageMeta } from "../../web/types.ts";
/**
* Almost the same as web/syscalls/space.ts except leaving out client-specific stuff
*/
export function spaceSyscalls(space: Space): SysCallMapping {
return {
"space.listPages": (): Promise<PageMeta[]> => {
return space.fetchPageList();
},
"space.readPage": async (
_ctx,
name: string,
): Promise<string> => {
return (await space.readPage(name)).text;
},
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
return space.getPageMeta(name);
},
"space.writePage": (
_ctx,
name: string,
text: string,
): Promise<PageMeta> => {
return space.writePage(name, text);
},
"space.deletePage": async (_ctx, name: string) => {
await space.deletePage(name);
},
"space.listPlugs": (): Promise<string[]> => {
return space.listPlugs();
},
"space.listAttachments": async (): Promise<AttachmentMeta[]> => {
return await space.fetchAttachmentList();
},
"space.readAttachment": async (
_ctx,
name: string,
): Promise<Uint8Array> => {
return (await space.readAttachment(name)).data;
},
"space.getAttachmentMeta": async (
_ctx,
name: string,
): Promise<AttachmentMeta> => {
return await space.getAttachmentMeta(name);
},
"space.writeAttachment": (
_ctx,
name: string,
data: Uint8Array,
): Promise<AttachmentMeta> => {
return space.writeAttachment(name, data);
},
"space.deleteAttachment": async (_ctx, name: string) => {
await space.deleteAttachment(name);
},
};
}
+6
View File
@@ -0,0 +1,6 @@
name: test
requiredPermissions:
- shell
functions:
run:
path: plug_test.ts:run