Space System refactor
This commit is contained in:
parent
80a2d4e58a
commit
eff0277be0
33
cmd/invokeFunction.ts
Normal file
33
cmd/invokeFunction.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { SpaceSystem } from "../server/space_system.ts";
|
||||||
|
|
||||||
|
import assetBundle from "../dist/asset_bundle.json" assert { type: "json" };
|
||||||
|
import { path } from "../plugos/deps.ts";
|
||||||
|
import { AssetBundle, AssetJson } from "../plugos/asset_bundle/bundle.ts";
|
||||||
|
|
||||||
|
export async function invokeFunction(
|
||||||
|
options: any,
|
||||||
|
pagesPath: string,
|
||||||
|
functionName: string,
|
||||||
|
...args: string[]
|
||||||
|
) {
|
||||||
|
console.log("Going to invoke funciton", functionName, "with args", args);
|
||||||
|
const spaceSystem = new SpaceSystem(
|
||||||
|
new AssetBundle(assetBundle as AssetJson),
|
||||||
|
pagesPath,
|
||||||
|
path.join(pagesPath, options.db),
|
||||||
|
);
|
||||||
|
|
||||||
|
await spaceSystem.start();
|
||||||
|
|
||||||
|
const [plugName, funcName] = functionName.split(".");
|
||||||
|
|
||||||
|
const plug = spaceSystem.system.loadedPlugs.get(plugName);
|
||||||
|
|
||||||
|
if (!plug) {
|
||||||
|
console.error("Plug not found", plugName);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await plug.invoke(funcName, args);
|
||||||
|
Deno.exit(0);
|
||||||
|
}
|
@ -14,7 +14,7 @@ import {
|
|||||||
storeSyscalls,
|
storeSyscalls,
|
||||||
} from "../plugos/syscalls/store.deno.ts";
|
} from "../plugos/syscalls/store.deno.ts";
|
||||||
import { System } from "../plugos/system.ts";
|
import { System } from "../plugos/system.ts";
|
||||||
import { Manifest, SilverBulletHooks } from "../common/manifest.ts";
|
import { SilverBulletHooks } from "../common/manifest.ts";
|
||||||
import { loadMarkdownExtensions } from "../common/markdown_ext.ts";
|
import { loadMarkdownExtensions } from "../common/markdown_ext.ts";
|
||||||
import buildMarkdown from "../common/parser.ts";
|
import buildMarkdown from "../common/parser.ts";
|
||||||
import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts";
|
import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts";
|
||||||
@ -29,15 +29,12 @@ import {
|
|||||||
} from "../server/syscalls/index.ts";
|
} from "../server/syscalls/index.ts";
|
||||||
import spaceSyscalls from "../server/syscalls/space.ts";
|
import spaceSyscalls from "../server/syscalls/space.ts";
|
||||||
|
|
||||||
import { Command } from "https://deno.land/x/cliffy@v0.25.2/command/command.ts";
|
|
||||||
|
|
||||||
import assetBundle from "../dist/asset_bundle.json" assert { type: "json" };
|
import assetBundle from "../dist/asset_bundle.json" assert { type: "json" };
|
||||||
import { AssetBundle, AssetJson } from "../plugos/asset_bundle/bundle.ts";
|
import { AssetBundle, AssetJson } from "../plugos/asset_bundle/bundle.ts";
|
||||||
import { path } from "../server/deps.ts";
|
import { path } from "../server/deps.ts";
|
||||||
import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts";
|
import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts";
|
||||||
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
||||||
import assetSyscalls from "../plugos/syscalls/asset.ts";
|
import assetSyscalls from "../plugos/syscalls/asset.ts";
|
||||||
import { faBullseye } from "https://esm.sh/v96/@fortawesome/free-solid-svg-icons@6.2.0/index.d.ts";
|
|
||||||
|
|
||||||
export async function publishCommand(options: {
|
export async function publishCommand(options: {
|
||||||
index: boolean;
|
index: boolean;
|
||||||
|
@ -12,11 +12,14 @@ export function serveCommand(options: any, folder: string) {
|
|||||||
port,
|
port,
|
||||||
"serving pages from",
|
"serving pages from",
|
||||||
pagesPath,
|
pagesPath,
|
||||||
|
"with db file",
|
||||||
|
options.db,
|
||||||
);
|
);
|
||||||
|
|
||||||
const httpServer = new HttpServer({
|
const httpServer = new HttpServer({
|
||||||
port: port,
|
port: port,
|
||||||
pagesPath: pagesPath,
|
pagesPath: pagesPath,
|
||||||
|
dbPath: path.join(pagesPath, options.db),
|
||||||
assetBundle: new AssetBundle(assetBundle as AssetJson),
|
assetBundle: new AssetBundle(assetBundle as AssetJson),
|
||||||
password: options.password,
|
password: options.password,
|
||||||
});
|
});
|
||||||
|
@ -1,164 +1,41 @@
|
|||||||
import { Application, path, Router } from "./deps.ts";
|
import { Application, path, Router } from "./deps.ts";
|
||||||
import { Manifest, SilverBulletHooks } from "../common/manifest.ts";
|
import { Manifest } from "../common/manifest.ts";
|
||||||
import { loadMarkdownExtensions } from "../common/markdown_ext.ts";
|
|
||||||
import buildMarkdown from "../common/parser.ts";
|
|
||||||
import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts";
|
|
||||||
import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts";
|
|
||||||
import { Space } from "../common/spaces/space.ts";
|
|
||||||
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||||
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
|
|
||||||
import { parseYamlSettings } from "../common/util.ts";
|
|
||||||
import { createSandbox } from "../plugos/environments/deno_sandbox.ts";
|
|
||||||
import { EndpointHook } from "../plugos/hooks/endpoint.ts";
|
import { EndpointHook } from "../plugos/hooks/endpoint.ts";
|
||||||
import { EventHook } from "../plugos/hooks/event.ts";
|
|
||||||
import { DenoCronHook } from "../plugos/hooks/cron.deno.ts";
|
|
||||||
import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts";
|
|
||||||
import { eventSyscalls } from "../plugos/syscalls/event.ts";
|
|
||||||
import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts";
|
|
||||||
import {
|
|
||||||
ensureFTSTable,
|
|
||||||
fullTextSearchSyscalls,
|
|
||||||
} from "../plugos/syscalls/fulltext.sqlite.ts";
|
|
||||||
import sandboxSyscalls from "../plugos/syscalls/sandbox.ts";
|
|
||||||
import shellSyscalls from "../plugos/syscalls/shell.deno.ts";
|
|
||||||
import {
|
|
||||||
ensureTable as ensureStoreTable,
|
|
||||||
storeSyscalls,
|
|
||||||
} from "../plugos/syscalls/store.deno.ts";
|
|
||||||
import { SysCallMapping, System } from "../plugos/system.ts";
|
|
||||||
import { PageNamespaceHook } from "./hooks/page_namespace.ts";
|
|
||||||
import { PlugSpacePrimitives } from "./hooks/plug_space_primitives.ts";
|
|
||||||
import {
|
|
||||||
ensureTable as ensureIndexTable,
|
|
||||||
pageIndexSyscalls,
|
|
||||||
} from "./syscalls/index.ts";
|
|
||||||
import spaceSyscalls from "./syscalls/space.ts";
|
|
||||||
import { systemSyscalls } from "./syscalls/system.ts";
|
|
||||||
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
|
||||||
import assetSyscalls from "../plugos/syscalls/asset.ts";
|
|
||||||
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
||||||
import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts";
|
import { SpaceSystem } from "./space_system.ts";
|
||||||
import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts";
|
import { parseYamlSettings } from "../common/util.ts";
|
||||||
|
|
||||||
export type ServerOptions = {
|
export type ServerOptions = {
|
||||||
port: number;
|
port: number;
|
||||||
pagesPath: string;
|
pagesPath: string;
|
||||||
|
dbPath: string;
|
||||||
assetBundle: AssetBundle;
|
assetBundle: AssetBundle;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const indexRequiredKey = "$spaceIndexed";
|
|
||||||
const staticLastModified = new Date().toUTCString();
|
const staticLastModified = new Date().toUTCString();
|
||||||
|
|
||||||
export class HttpServer {
|
export class HttpServer {
|
||||||
app: Application;
|
app: Application;
|
||||||
system: System<SilverBulletHooks>;
|
systemBoot: SpaceSystem;
|
||||||
private space: Space;
|
|
||||||
private eventHook: EventHook;
|
|
||||||
private db: AsyncSQLite;
|
|
||||||
private port: number;
|
private port: number;
|
||||||
password?: string;
|
password?: string;
|
||||||
settings: { [key: string]: any } = {};
|
settings: { [key: string]: any } = {};
|
||||||
spacePrimitives: SpacePrimitives;
|
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
globalModules: Manifest;
|
|
||||||
assetBundle: AssetBundle;
|
|
||||||
indexSyscalls: SysCallMapping;
|
|
||||||
|
|
||||||
constructor(options: ServerOptions) {
|
constructor(options: ServerOptions) {
|
||||||
this.port = options.port;
|
this.port = options.port;
|
||||||
this.app = new Application(); //{ serverConstructor: FlashServer });
|
this.app = new Application(); //{ serverConstructor: FlashServer });
|
||||||
this.assetBundle = options.assetBundle;
|
|
||||||
this.password = options.password;
|
this.password = options.password;
|
||||||
|
this.systemBoot = new SpaceSystem(
|
||||||
this.globalModules = JSON.parse(
|
options.assetBundle,
|
||||||
this.assetBundle.readTextFileSync(`web/global.plug.json`),
|
options.pagesPath,
|
||||||
|
options.dbPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set up the PlugOS System
|
|
||||||
this.system = new System<SilverBulletHooks>("server");
|
|
||||||
|
|
||||||
// Instantiate the event bus hook
|
|
||||||
this.eventHook = new EventHook();
|
|
||||||
this.system.addHook(this.eventHook);
|
|
||||||
|
|
||||||
// And the page namespace hook
|
|
||||||
const namespaceHook = new PageNamespaceHook();
|
|
||||||
this.system.addHook(namespaceHook);
|
|
||||||
|
|
||||||
// The database used for persistence (SQLite)
|
|
||||||
this.db = new AsyncSQLite(path.join(options.pagesPath, "data.db"));
|
|
||||||
this.db.init().catch((e) => {
|
|
||||||
console.error("Error initializing database", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.indexSyscalls = pageIndexSyscalls(this.db);
|
|
||||||
|
|
||||||
// The space
|
|
||||||
try {
|
|
||||||
this.spacePrimitives = new FileMetaSpacePrimitives(
|
|
||||||
new AssetBundlePlugSpacePrimitives(
|
|
||||||
new EventedSpacePrimitives(
|
|
||||||
new PlugSpacePrimitives(
|
|
||||||
new DiskSpacePrimitives(options.pagesPath),
|
|
||||||
namespaceHook,
|
|
||||||
"server",
|
|
||||||
),
|
|
||||||
this.eventHook,
|
|
||||||
),
|
|
||||||
this.assetBundle,
|
|
||||||
),
|
|
||||||
this.indexSyscalls,
|
|
||||||
);
|
|
||||||
this.space = new Space(this.spacePrimitives);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e instanceof Deno.errors.NotFound) {
|
|
||||||
console.error("Pages folder", options.pagesPath, "not found");
|
|
||||||
} else {
|
|
||||||
console.error(e.message);
|
|
||||||
}
|
|
||||||
Deno.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The cron hook
|
|
||||||
this.system.addHook(new DenoCronHook());
|
|
||||||
|
|
||||||
// Register syscalls available on the server side
|
|
||||||
this.system.registerSyscalls(
|
|
||||||
[],
|
|
||||||
this.indexSyscalls,
|
|
||||||
storeSyscalls(this.db, "store"),
|
|
||||||
fullTextSearchSyscalls(this.db, "fts"),
|
|
||||||
spaceSyscalls(this.space),
|
|
||||||
eventSyscalls(this.eventHook),
|
|
||||||
markdownSyscalls(buildMarkdown([])),
|
|
||||||
esbuildSyscalls([this.globalModules]),
|
|
||||||
systemSyscalls(this, this.system),
|
|
||||||
sandboxSyscalls(this.system),
|
|
||||||
assetSyscalls(this.system),
|
|
||||||
// jwtSyscalls(),
|
|
||||||
);
|
|
||||||
// Danger zone
|
|
||||||
this.system.registerSyscalls(["shell"], shellSyscalls(options.pagesPath));
|
|
||||||
this.system.registerSyscalls(["fs"], fileSystemSyscalls("/"));
|
|
||||||
|
|
||||||
// Register the HTTP endpoint hook (with "/_/<plug-name>"" prefix, hardcoded for now)
|
|
||||||
this.system.addHook(new EndpointHook(this.app, "/_"));
|
|
||||||
|
|
||||||
this.system.on({
|
|
||||||
sandboxInitialized: async (sandbox) => {
|
|
||||||
for (
|
|
||||||
const [modName, code] of Object.entries(
|
|
||||||
this.globalModules.dependencies!,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await sandbox.loadDependency(modName, code as string);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Second, for loading plug JSON files with absolute or relative (from CWD) paths
|
// Second, for loading plug JSON files with absolute or relative (from CWD) paths
|
||||||
this.eventHook.addLocalListener(
|
this.systemBoot.eventHook.addLocalListener(
|
||||||
"get-plug:file",
|
"get-plug:file",
|
||||||
async (plugPath: string): Promise<Manifest> => {
|
async (plugPath: string): Promise<Manifest> => {
|
||||||
const resolvedPath = path.resolve(plugPath);
|
const resolvedPath = path.resolve(plugPath);
|
||||||
@ -175,57 +52,17 @@ export class HttpServer {
|
|||||||
|
|
||||||
// Rescan disk every 5s to detect any out-of-process file changes
|
// Rescan disk every 5s to detect any out-of-process file changes
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.space.updatePageList().catch(console.error);
|
this.systemBoot.space.updatePageList().catch(console.error);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
|
||||||
|
|
||||||
rebuildMdExtensions() {
|
// Register the HTTP endpoint hook (with "/_/<plug-name>"" prefix, hardcoded for now)
|
||||||
this.system.registerSyscalls(
|
this.systemBoot.system.addHook(new EndpointHook(this.app, "/_"));
|
||||||
[],
|
|
||||||
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async reloadPlugs() {
|
|
||||||
await this.space.updatePageList();
|
|
||||||
|
|
||||||
const allPlugs = await this.space.listPlugs();
|
|
||||||
|
|
||||||
console.log("Loading plugs", allPlugs);
|
|
||||||
await Promise.all((await this.space.listPlugs()).map(async (plugName) => {
|
|
||||||
const { data } = await this.space.readAttachment(plugName, "string");
|
|
||||||
await this.system.load(JSON.parse(data as string), createSandbox);
|
|
||||||
}));
|
|
||||||
this.rebuildMdExtensions();
|
|
||||||
|
|
||||||
const corePlug = this.system.loadedPlugs.get("core");
|
|
||||||
if (!corePlug) {
|
|
||||||
console.error("Something went very wrong, 'core' plug not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do we need to reindex this space?
|
|
||||||
if (
|
|
||||||
!(await this.system.localSyscall("core", "store.get", [indexRequiredKey]))
|
|
||||||
) {
|
|
||||||
console.log("Now reindexing space...");
|
|
||||||
await corePlug.invoke("reindexSpace", []);
|
|
||||||
await this.system.localSyscall("core", "store.set", [
|
|
||||||
indexRequiredKey,
|
|
||||||
true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
await ensureIndexTable(this.db);
|
await this.systemBoot.start();
|
||||||
await ensureStoreTable(this.db, "store");
|
await this.systemBoot.ensureSpaceIndex();
|
||||||
await ensureFTSTable(this.db, "fts");
|
|
||||||
await this.ensureAndLoadSettings();
|
await this.ensureAndLoadSettings();
|
||||||
|
|
||||||
// Load plugs
|
|
||||||
this.reloadPlugs().catch(console.error);
|
|
||||||
|
|
||||||
// Serve static files (javascript, css, html)
|
// Serve static files (javascript, css, html)
|
||||||
this.app.use(async ({ request, response }, next) => {
|
this.app.use(async ({ request, response }, next) => {
|
||||||
if (request.url.pathname === "/") {
|
if (request.url.pathname === "/") {
|
||||||
@ -234,7 +71,7 @@ export class HttpServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
response.headers.set("Content-type", "text/html");
|
response.headers.set("Content-type", "text/html");
|
||||||
response.body = this.assetBundle.readTextFileSync(
|
response.body = this.systemBoot.assetBundle.readTextFileSync(
|
||||||
"web/index.html",
|
"web/index.html",
|
||||||
);
|
);
|
||||||
response.headers.set("Last-Modified", staticLastModified);
|
response.headers.set("Last-Modified", staticLastModified);
|
||||||
@ -243,7 +80,7 @@ export class HttpServer {
|
|||||||
try {
|
try {
|
||||||
const assetName = `web${request.url.pathname}`;
|
const assetName = `web${request.url.pathname}`;
|
||||||
if (
|
if (
|
||||||
this.assetBundle.has(assetName) &&
|
this.systemBoot.assetBundle.has(assetName) &&
|
||||||
request.headers.get("If-Modified-Since") === staticLastModified
|
request.headers.get("If-Modified-Since") === staticLastModified
|
||||||
) {
|
) {
|
||||||
response.status = 304;
|
response.status = 304;
|
||||||
@ -252,9 +89,9 @@ export class HttpServer {
|
|||||||
response.status = 200;
|
response.status = 200;
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
"Content-type",
|
"Content-type",
|
||||||
this.assetBundle.getMimeType(assetName),
|
this.systemBoot.assetBundle.getMimeType(assetName),
|
||||||
);
|
);
|
||||||
const data = this.assetBundle.readFileSync(
|
const data = this.systemBoot.assetBundle.readFileSync(
|
||||||
assetName,
|
assetName,
|
||||||
);
|
);
|
||||||
response.headers.set("Cache-Control", "no-cache");
|
response.headers.set("Cache-Control", "no-cache");
|
||||||
@ -270,7 +107,7 @@ export class HttpServer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Pages API
|
// Pages API
|
||||||
const fsRouter = this.buildFsRouter(this.spacePrimitives);
|
const fsRouter = this.buildFsRouter(this.systemBoot.spacePrimitives);
|
||||||
this.app.use(fsRouter.routes());
|
this.app.use(fsRouter.routes());
|
||||||
this.app.use(fsRouter.allowedMethods());
|
this.app.use(fsRouter.allowedMethods());
|
||||||
|
|
||||||
@ -282,7 +119,7 @@ export class HttpServer {
|
|||||||
// Fallback, serve index.html
|
// Fallback, serve index.html
|
||||||
this.app.use((ctx) => {
|
this.app.use((ctx) => {
|
||||||
ctx.response.headers.set("Content-type", "text/html");
|
ctx.response.headers.set("Content-type", "text/html");
|
||||||
ctx.response.body = this.assetBundle.readTextFileSync(
|
ctx.response.body = this.systemBoot.assetBundle.readTextFileSync(
|
||||||
"web/index.html",
|
"web/index.html",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -296,7 +133,34 @@ export class HttpServer {
|
|||||||
console.log(
|
console.log(
|
||||||
`Silver Bullet is now running: http://localhost:${this.port}`,
|
`Silver Bullet is now running: http://localhost:${this.port}`,
|
||||||
);
|
);
|
||||||
console.log("--------------");
|
}
|
||||||
|
|
||||||
|
async ensureAndLoadSettings() {
|
||||||
|
const space = this.systemBoot.space;
|
||||||
|
try {
|
||||||
|
await space.getPageMeta("SETTINGS");
|
||||||
|
} catch {
|
||||||
|
await space.writePage(
|
||||||
|
"SETTINGS",
|
||||||
|
this.systemBoot.assetBundle.readTextFileSync("SETTINGS_template.md"),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text: settingsText } = await space.readPage("SETTINGS");
|
||||||
|
const settings = parseYamlSettings(settingsText);
|
||||||
|
if (!settings.indexPage) {
|
||||||
|
settings.indexPage = "index";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await space.getPageMeta(settings.indexPage);
|
||||||
|
} catch {
|
||||||
|
await space.writePage(
|
||||||
|
settings.indexPage,
|
||||||
|
`Welcome to your new space!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addPasswordAuth(r: Router) {
|
private addPasswordAuth(r: Router) {
|
||||||
@ -413,6 +277,7 @@ export class HttpServer {
|
|||||||
private buildPlugRouter(): Router {
|
private buildPlugRouter(): Router {
|
||||||
const plugRouter = new Router();
|
const plugRouter = new Router();
|
||||||
this.addPasswordAuth(plugRouter);
|
this.addPasswordAuth(plugRouter);
|
||||||
|
const system = this.systemBoot.system;
|
||||||
|
|
||||||
plugRouter.post(
|
plugRouter.post(
|
||||||
"/:plug/syscall/:name",
|
"/:plug/syscall/:name",
|
||||||
@ -421,14 +286,14 @@ export class HttpServer {
|
|||||||
const plugName = ctx.params.plug;
|
const plugName = ctx.params.plug;
|
||||||
const args = await ctx.request.body().value;
|
const args = await ctx.request.body().value;
|
||||||
console.log("Got args", args, "for", name, "in", plugName);
|
console.log("Got args", args, "for", name, "in", plugName);
|
||||||
const plug = this.system.loadedPlugs.get(plugName);
|
const plug = system.loadedPlugs.get(plugName);
|
||||||
if (!plug) {
|
if (!plug) {
|
||||||
ctx.response.status = 404;
|
ctx.response.status = 404;
|
||||||
ctx.response.body = `Plug ${plugName} not found`;
|
ctx.response.body = `Plug ${plugName} not found`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.system.syscallWithContext(
|
const result = await system.syscallWithContext(
|
||||||
{ plug },
|
{ plug },
|
||||||
name,
|
name,
|
||||||
args,
|
args,
|
||||||
@ -450,7 +315,7 @@ export class HttpServer {
|
|||||||
const name = ctx.params.name;
|
const name = ctx.params.name;
|
||||||
const plugName = ctx.params.plug;
|
const plugName = ctx.params.plug;
|
||||||
const args = await ctx.request.body().value;
|
const args = await ctx.request.body().value;
|
||||||
const plug = this.system.loadedPlugs.get(plugName);
|
const plug = system.loadedPlugs.get(plugName);
|
||||||
if (!plug) {
|
if (!plug) {
|
||||||
ctx.response.status = 404;
|
ctx.response.status = 404;
|
||||||
ctx.response.body = `Plug ${plugName} not found`;
|
ctx.response.body = `Plug ${plugName} not found`;
|
||||||
@ -471,37 +336,11 @@ export class HttpServer {
|
|||||||
return new Router().use("/plug", plugRouter.routes());
|
return new Router().use("/plug", plugRouter.routes());
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureAndLoadSettings() {
|
|
||||||
try {
|
|
||||||
await this.space.getPageMeta("SETTINGS");
|
|
||||||
} catch {
|
|
||||||
await this.space.writePage(
|
|
||||||
"SETTINGS",
|
|
||||||
this.assetBundle.readTextFileSync("SETTINGS_template.md"),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { text: settingsText } = await this.space.readPage("SETTINGS");
|
|
||||||
this.settings = parseYamlSettings(settingsText);
|
|
||||||
if (!this.settings.indexPage) {
|
|
||||||
this.settings.indexPage = "index";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.space.getPageMeta(this.settings.indexPage);
|
|
||||||
} catch {
|
|
||||||
await this.space.writePage(
|
|
||||||
this.settings.indexPage,
|
|
||||||
`Welcome to your new space!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
|
const system = this.systemBoot.system;
|
||||||
if (this.abortController) {
|
if (this.abortController) {
|
||||||
console.log("Stopping");
|
console.log("Stopping");
|
||||||
await this.system.unloadAll();
|
await system.unloadAll();
|
||||||
console.log("Stopped plugs");
|
console.log("Stopped plugs");
|
||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
console.log("stopped server");
|
console.log("stopped server");
|
||||||
|
185
server/space_system.ts
Normal file
185
server/space_system.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { SilverBulletHooks } from "../common/manifest.ts";
|
||||||
|
import { loadMarkdownExtensions } from "../common/markdown_ext.ts";
|
||||||
|
import buildMarkdown from "../common/parser.ts";
|
||||||
|
import { DiskSpacePrimitives } from "../common/spaces/disk_space_primitives.ts";
|
||||||
|
import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts";
|
||||||
|
import { Space } from "../common/spaces/space.ts";
|
||||||
|
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||||
|
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
|
||||||
|
import { createSandbox } from "../plugos/environments/deno_sandbox.ts";
|
||||||
|
import { EventHook } from "../plugos/hooks/event.ts";
|
||||||
|
import { DenoCronHook } from "../plugos/hooks/cron.deno.ts";
|
||||||
|
import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts";
|
||||||
|
import { eventSyscalls } from "../plugos/syscalls/event.ts";
|
||||||
|
import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts";
|
||||||
|
import {
|
||||||
|
ensureFTSTable,
|
||||||
|
fullTextSearchSyscalls,
|
||||||
|
} from "../plugos/syscalls/fulltext.sqlite.ts";
|
||||||
|
import sandboxSyscalls from "../plugos/syscalls/sandbox.ts";
|
||||||
|
import shellSyscalls from "../plugos/syscalls/shell.deno.ts";
|
||||||
|
import {
|
||||||
|
ensureTable as ensureStoreTable,
|
||||||
|
storeSyscalls,
|
||||||
|
} from "../plugos/syscalls/store.deno.ts";
|
||||||
|
import { System } from "../plugos/system.ts";
|
||||||
|
import { PageNamespaceHook } from "./hooks/page_namespace.ts";
|
||||||
|
import { PlugSpacePrimitives } from "./hooks/plug_space_primitives.ts";
|
||||||
|
import {
|
||||||
|
ensureTable as ensureIndexTable,
|
||||||
|
pageIndexSyscalls,
|
||||||
|
} from "./syscalls/index.ts";
|
||||||
|
import spaceSyscalls from "./syscalls/space.ts";
|
||||||
|
import { systemSyscalls } from "./syscalls/system.ts";
|
||||||
|
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
||||||
|
import assetSyscalls from "../plugos/syscalls/asset.ts";
|
||||||
|
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
||||||
|
import { AsyncSQLite } from "../plugos/sqlite/async_sqlite.ts";
|
||||||
|
import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts";
|
||||||
|
export const indexRequiredKey = "$spaceIndexed";
|
||||||
|
|
||||||
|
// A composition of a PlugOS system attached to a Space for server-side use
|
||||||
|
export class SpaceSystem {
|
||||||
|
public system: System<SilverBulletHooks>;
|
||||||
|
public space: Space;
|
||||||
|
public eventHook: EventHook;
|
||||||
|
public spacePrimitives: SpacePrimitives;
|
||||||
|
|
||||||
|
private db: AsyncSQLite;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly assetBundle: AssetBundle,
|
||||||
|
pagesPath: string,
|
||||||
|
databasePath: string,
|
||||||
|
) {
|
||||||
|
const globalModules = JSON.parse(
|
||||||
|
assetBundle.readTextFileSync(`web/global.plug.json`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up the PlugOS System
|
||||||
|
this.system = new System<SilverBulletHooks>("server");
|
||||||
|
|
||||||
|
// Instantiate the event bus hook
|
||||||
|
this.eventHook = new EventHook();
|
||||||
|
this.system.addHook(this.eventHook);
|
||||||
|
|
||||||
|
// And the page namespace hook
|
||||||
|
const namespaceHook = new PageNamespaceHook();
|
||||||
|
this.system.addHook(namespaceHook);
|
||||||
|
|
||||||
|
// The database used for persistence (SQLite)
|
||||||
|
this.db = new AsyncSQLite(databasePath);
|
||||||
|
this.db.init().catch((e) => {
|
||||||
|
console.error("Error initializing database", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
const indexSyscalls = pageIndexSyscalls(this.db);
|
||||||
|
// The space
|
||||||
|
try {
|
||||||
|
this.spacePrimitives = new FileMetaSpacePrimitives(
|
||||||
|
new AssetBundlePlugSpacePrimitives(
|
||||||
|
new EventedSpacePrimitives(
|
||||||
|
new PlugSpacePrimitives(
|
||||||
|
new DiskSpacePrimitives(pagesPath),
|
||||||
|
namespaceHook,
|
||||||
|
"server",
|
||||||
|
),
|
||||||
|
this.eventHook,
|
||||||
|
),
|
||||||
|
assetBundle,
|
||||||
|
),
|
||||||
|
indexSyscalls,
|
||||||
|
);
|
||||||
|
this.space = new Space(this.spacePrimitives);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof Deno.errors.NotFound) {
|
||||||
|
console.error("Pages folder", pagesPath, "not found");
|
||||||
|
} else {
|
||||||
|
console.error(e.message);
|
||||||
|
}
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cron hook
|
||||||
|
this.system.addHook(new DenoCronHook());
|
||||||
|
|
||||||
|
// Register syscalls available on the server side
|
||||||
|
this.system.registerSyscalls(
|
||||||
|
[],
|
||||||
|
indexSyscalls,
|
||||||
|
storeSyscalls(this.db, "store"),
|
||||||
|
fullTextSearchSyscalls(this.db, "fts"),
|
||||||
|
spaceSyscalls(this.space),
|
||||||
|
eventSyscalls(this.eventHook),
|
||||||
|
markdownSyscalls(buildMarkdown([])),
|
||||||
|
esbuildSyscalls([globalModules]),
|
||||||
|
systemSyscalls(this.loadPlugsFromSpace.bind(this), this.system),
|
||||||
|
sandboxSyscalls(this.system),
|
||||||
|
assetSyscalls(this.system),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Danger zone, these syscalls require requesting specific permissions
|
||||||
|
this.system.registerSyscalls(["shell"], shellSyscalls(pagesPath));
|
||||||
|
this.system.registerSyscalls(["fs"], fileSystemSyscalls("/"));
|
||||||
|
|
||||||
|
this.system.on({
|
||||||
|
sandboxInitialized: async (sandbox) => {
|
||||||
|
for (
|
||||||
|
const [modName, code] of Object.entries(
|
||||||
|
globalModules.dependencies!,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await sandbox.loadDependency(modName, code as string);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads all plugs under "_plug/" in the space
|
||||||
|
async loadPlugsFromSpace() {
|
||||||
|
await this.space.updatePageList();
|
||||||
|
|
||||||
|
const allPlugs = await this.space.listPlugs();
|
||||||
|
|
||||||
|
console.log("Going to load", allPlugs.length, "plugs...");
|
||||||
|
await Promise.all(allPlugs.map(async (plugName) => {
|
||||||
|
const { data } = await this.space.readAttachment(plugName, "string");
|
||||||
|
await this.system.load(JSON.parse(data as string), createSandbox);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Re-register the markdown syscall with new markdown extensions
|
||||||
|
this.system.registerSyscalls(
|
||||||
|
[],
|
||||||
|
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the space has been indexed, and if not, does so
|
||||||
|
async ensureSpaceIndex(forceReindex = false) {
|
||||||
|
const corePlug = this.system.loadedPlugs.get("core");
|
||||||
|
if (!corePlug) {
|
||||||
|
console.error("Something went very wrong, 'core' plug not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we need to reindex this space?
|
||||||
|
if (
|
||||||
|
forceReindex ||
|
||||||
|
!(await this.system.localSyscall("core", "store.get", [indexRequiredKey]))
|
||||||
|
) {
|
||||||
|
console.log("Now reindexing space...");
|
||||||
|
await corePlug.invoke("reindexSpace", []);
|
||||||
|
await this.system.localSyscall("core", "store.set", [
|
||||||
|
indexRequiredKey,
|
||||||
|
true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await ensureIndexTable(this.db);
|
||||||
|
await ensureStoreTable(this.db, "store");
|
||||||
|
await ensureFTSTable(this.db, "fts");
|
||||||
|
await this.loadPlugsFromSpace();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import { Plug } from "../../plugos/plug.ts";
|
import { Plug } from "../../plugos/plug.ts";
|
||||||
import { SysCallMapping, System } from "../../plugos/system.ts";
|
import { SysCallMapping, System } from "../../plugos/system.ts";
|
||||||
import type { HttpServer } from "../http_server.ts";
|
|
||||||
|
|
||||||
export function systemSyscalls(
|
export function systemSyscalls(
|
||||||
httpServer: HttpServer,
|
plugReloader: () => Promise<void>,
|
||||||
system: System<any>,
|
system: System<any>,
|
||||||
): SysCallMapping {
|
): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
@ -30,7 +29,7 @@ export function systemSyscalls(
|
|||||||
return plug.invoke(name, args);
|
return plug.invoke(name, args);
|
||||||
},
|
},
|
||||||
"system.reloadPlugs": () => {
|
"system.reloadPlugs": () => {
|
||||||
return httpServer.reloadPlugs();
|
return plugReloader();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { fixCommand } from "./cmd/fix.ts";
|
|||||||
import { serveCommand } from "./cmd/server.ts";
|
import { serveCommand } from "./cmd/server.ts";
|
||||||
import { plugCompileCommand } from "./cmd/plug_compile.ts";
|
import { plugCompileCommand } from "./cmd/plug_compile.ts";
|
||||||
import { publishCommand } from "./cmd/publish.ts";
|
import { publishCommand } from "./cmd/publish.ts";
|
||||||
|
import { invokeFunction } from "./cmd/invokeFunction.ts";
|
||||||
|
|
||||||
await new Command()
|
await new Command()
|
||||||
.name("silverbullet")
|
.name("silverbullet")
|
||||||
@ -20,6 +21,9 @@ await new Command()
|
|||||||
// Main command
|
// Main command
|
||||||
.arguments("<folder:string>")
|
.arguments("<folder:string>")
|
||||||
.option("-p, --port <port:number>", "Port to listen on")
|
.option("-p, --port <port:number>", "Port to listen on")
|
||||||
|
.option("--db <dbfile:string>", "Filename for the database", {
|
||||||
|
default: "data.db",
|
||||||
|
})
|
||||||
.option("--password <password:string>", "Password for basic authentication")
|
.option("--password <password:string>", "Password for basic authentication")
|
||||||
.action(serveCommand)
|
.action(serveCommand)
|
||||||
// fix
|
// fix
|
||||||
@ -43,6 +47,13 @@ await new Command()
|
|||||||
)
|
)
|
||||||
.option("--importmap <path:string>", "Path to import map file to use")
|
.option("--importmap <path:string>", "Path to import map file to use")
|
||||||
.action(plugCompileCommand)
|
.action(plugCompileCommand)
|
||||||
|
// invokeFunction
|
||||||
|
.command("invokeFunction", "Invoke a specific plug function from the CLI")
|
||||||
|
.arguments("<path:string> <function:string> [...arguments:string]")
|
||||||
|
.option("--db <dbfile:string>", "Filename for the database", {
|
||||||
|
default: "data.db",
|
||||||
|
})
|
||||||
|
.action(invokeFunction)
|
||||||
// publish
|
// publish
|
||||||
.command("publish")
|
.command("publish")
|
||||||
.description("Publish a SilverBullet site")
|
.description("Publish a SilverBullet site")
|
||||||
|
Loading…
Reference in New Issue
Block a user