import { homedir } from "node:os"; import { serveDir } from "@std/http/file-server"; import { fromFileUrl, join, resolve } from "@std/path"; import { builtinModels } from "@earendil-works/pi-ai/providers/all"; import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node"; import { piAuthStore } from "./auth.ts"; import { type Host, openBerry } from "./berry.ts"; import { createHandler } from "./http.ts"; import { denoCheck, extensionLoader } from "./loader.ts"; import { RESTART } from "./supervisor.ts"; import { extensionWeb } from "./web.ts"; const token = Deno.env.get("BERRY_TOKEN"); if (!token) { console.error("BERRY_TOKEN must be set"); Deno.exit(1); } const root = fromFileUrl(new URL("../", import.meta.url)); const directories = [ join(root, "extensions"), resolve(Deno.env.get("XDG_CONFIG_HOME") || join(homedir(), ".config"), "berry", "extensions"), ]; const credentials = piAuthStore(Deno.env.get("BERRY_AUTH")); const models = builtinModels({ credentials }); if (Deno.env.get("BERRY_DEV_MODEL")) (await import("./dev.ts")).installDevModel(models); const storage = await openNodeSqliteStorage( Deno.env.get("BERRY_DB") ?? join(root, "data", "berry.sqlite"), ); // a restart or a signal takes the process down; a check still running would hold it up const checks = new AbortController(); const host: Host = { root, extensionDirectories: directories, // the server, and the extensions it loads at startup, which it imports by name rather than statically check: async () => { const started = performance.now(); const failures = await denoCheck( root, [ "server/main.ts", ...loader.files(), ], checks.signal, ); // a restart's whole budget: this, then the stop below, then the new process saying it is listening console.log(`[berry] checked in ${Math.round(performance.now() - started)}ms`); return failures; }, restart: () => void stop(RESTART), extensions: () => loader.entries(), reload: (name) => loader.reload(name), }; const berry = await openBerry(storage, models, host); const web = extensionWeb({ cacheDirectory: join( Deno.env.get("XDG_CACHE_HOME") || join(homedir(), ".cache"), "berry", "frontends", ), }); const loader = extensionLoader({ berry, web, root, directories }); await loader.loadAll(); loader.watch(); berry.harness.resume(); const handler = createHandler({ berry, models, credentials, web, loader, host, token, // the built frontend, so one port serves everything; `deno task web` serves it with hmr instead fallback: (request) => serveDir(request, { fsRoot: join(root, "web", "dist"), quiet: true }), }); const server = Deno.serve({ hostname: Deno.env.get("BERRY_HOST") ?? "127.0.0.1", port: Number(Deno.env.get("BERRY_PORT") ?? 8787), }, handler); // from process start, so a restart's budget is this and the checked/stopped lines beside it console.log(`[berry] ready in ${Math.round(performance.now())}ms`); // once, whatever asks first: a signal or a restart; another request waits for that one let stopping: Promise | undefined; /** * How long teardown may take before the process leaves anyway: the database is a WAL, and every child watches its own * stdin, so what is still closing is left to end on its own rather than holding a restart. */ const STOP_DEADLINE_MS = 2_000; /** How long an in-flight request may take to finish before the process leaves without it. */ const REQUEST_GRACE_MS = 500; function stop(code: number): Promise { stopping ??= (async () => { checks.abort(new Error("berry is stopping")); const started = performance.now(); // where a slow shutdown goes; the only place these numbers can be seen const phases: string[] = []; const phase = async (name: string, run: () => unknown) => { const at = performance.now(); await run(); phases.push(`${name} ${Math.round(performance.now() - at)}ms`); }; const closing = (async () => { await phase("sockets", () => handler.closeSockets()); // a request of its own, such as a model refresh from a slow provider, ends with the process rather than holding it await phase( "http", () => Promise.race([ server.shutdown(), new Promise((resolve) => setTimeout(resolve, REQUEST_GRACE_MS)), ]), ); await phase("extensions", () => loader.close()); // signals every running call, and waits for them to end; durable resumes the work when berry opens again await phase("berry", () => berry.close()); })().catch((error) => console.error("[berry] stopping:", error)); const overdue = await Promise.race([ closing.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), STOP_DEADLINE_MS)), ]); const left = ["sockets", "http", "extensions", "berry"].filter((name) => !phases.some((line) => line.startsWith(`${name} `)) ); console.log( `[berry] stopped in ${Math.round(performance.now() - started)}ms (${phases.join(", ")})` + (overdue && left.length ? `; gave up on ${left.join(", ")} after ${STOP_DEADLINE_MS}ms` : ""), ); Deno.exit(code); })(); return stopping; } for (const signal of ["SIGINT", "SIGTERM"] as const) { Deno.addSignalListener(signal, () => void stop(0)); }