diff --git a/.dockerignore b/.dockerignore index 09292fa2..ed763eb8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,7 @@ !**/components.json !app/** +!apps/agent/** !packages/** !public/** diff --git a/Dockerfile b/Dockerfile index 2feb66be..ee1bd691 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,8 @@ COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr COPY ./package.json ./bun.lock ./ COPY ./packages/core/package.json ./packages/core/package.json +COPY ./packages/contracts/package.json ./packages/contracts/package.json +COPY ./apps/agent/package.json ./apps/agent/package.json RUN bun install --frozen-lockfile --ignore-scripts @@ -86,11 +88,14 @@ WORKDIR /app COPY ./package.json ./bun.lock ./ COPY ./packages/core/package.json ./packages/core/package.json +COPY ./packages/contracts/package.json ./packages/contracts/package.json +COPY ./apps/agent/package.json ./apps/agent/package.json RUN bun install --frozen-lockfile COPY . . RUN bun run build +RUN bun build apps/agent/src/index.ts --outfile .output/agent/index.mjs --target bun FROM base AS production diff --git a/app/server/modules/lifecycle/__tests__/shutdown.test.ts b/app/server/modules/lifecycle/__tests__/shutdown.test.ts new file mode 100644 index 00000000..7625c890 --- /dev/null +++ b/app/server/modules/lifecycle/__tests__/shutdown.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; +import { Scheduler } from "../../../core/scheduler"; +import * as backendModule from "../../backends/backend"; +import type { VolumeBackend } from "../../backends/backend"; +import * as bootstrapModule from "../bootstrap"; +import { createTestVolume } from "~/test/helpers/volume"; + +const loadShutdownModule = async () => { + const moduleUrl = new URL("../shutdown.ts", import.meta.url); + moduleUrl.searchParams.set("test", crypto.randomUUID()); + return import(moduleUrl.href); +}; + +afterEach(() => { + mock.restore(); +}); + +describe("shutdown", () => { + test("stops the agent runtime before unmounting mounted volumes", async () => { + const events: string[] = []; + const stopScheduler = mock(async () => { + events.push("scheduler.stop"); + }); + const stopApplicationRuntime = mock(async () => { + events.push("agents.stop"); + }); + const unmountVolume = mock(async () => { + events.push("backend.unmount"); + return { status: "unmounted" as const }; + }); + + await createTestVolume({ + name: "Shutdown test volume", + config: { + backend: "directory", + path: "/Applications", + }, + status: "mounted", + }); + + spyOn(Scheduler, "stop").mockImplementation(stopScheduler); + spyOn(bootstrapModule, "stopApplicationRuntime").mockImplementation(stopApplicationRuntime); + spyOn(backendModule, "createVolumeBackend").mockImplementation( + () => + ({ + mount: async () => ({ status: "mounted" as const }), + unmount: unmountVolume, + checkHealth: async () => ({ status: "mounted" as const }), + }) satisfies VolumeBackend, + ); + + const { shutdown } = await loadShutdownModule(); + + await shutdown(); + + expect(events).toEqual(["scheduler.stop", "agents.stop", "backend.unmount"]); + }); +}); diff --git a/app/server/modules/lifecycle/bootstrap.ts b/app/server/modules/lifecycle/bootstrap.ts index bb8f44ea..791ebce8 100644 --- a/app/server/modules/lifecycle/bootstrap.ts +++ b/app/server/modules/lifecycle/bootstrap.ts @@ -1,4 +1,5 @@ import { runDbMigrations } from "../../db/db"; +import { agentManager, spawnLocalAgent, stopAgentRuntime } from "../agents/agents-manager"; import { runMigrations } from "./migrations"; import { startup } from "./startup"; @@ -7,6 +8,8 @@ let bootstrapPromise: Promise | undefined; const runBootstrap = async () => { await runDbMigrations(); await runMigrations(); + agentManager.start(); + await spawnLocalAgent(); await startup(); }; @@ -22,3 +25,11 @@ export const bootstrapApplication = async () => { throw err; } }; + +export const stopApplicationRuntime = async () => { + try { + await stopAgentRuntime(); + } finally { + bootstrapPromise = undefined; + } +}; diff --git a/app/server/modules/lifecycle/shutdown.ts b/app/server/modules/lifecycle/shutdown.ts index 3bb76073..4deff2af 100644 --- a/app/server/modules/lifecycle/shutdown.ts +++ b/app/server/modules/lifecycle/shutdown.ts @@ -2,9 +2,11 @@ import { Scheduler } from "../../core/scheduler"; import { db } from "../../db/db"; import { logger } from "@zerobyte/core/node"; import { createVolumeBackend } from "../backends/backend"; +import { stopApplicationRuntime } from "./bootstrap"; export const shutdown = async () => { await Scheduler.stop(); + await stopApplicationRuntime(); const volumes = await db.query.volumesTable.findMany({ where: { status: "mounted" }, diff --git a/app/server/plugins/bootstrap.ts b/app/server/plugins/bootstrap.ts index 4dbc7c9e..ede70123 100644 --- a/app/server/plugins/bootstrap.ts +++ b/app/server/plugins/bootstrap.ts @@ -1,9 +1,11 @@ import { definePlugin } from "nitro"; -import { bootstrapApplication } from "../modules/lifecycle/bootstrap"; +import { bootstrapApplication, stopApplicationRuntime } from "../modules/lifecycle/bootstrap"; import { logger } from "@zerobyte/core/node"; import { toMessage } from "../utils/errors"; -export default definePlugin(async () => { +export default definePlugin(async (nitroApp) => { + nitroApp.hooks.hook("close", stopApplicationRuntime); + await bootstrapApplication().catch((err) => { logger.error(`Bootstrap failed: ${toMessage(err)}`); process.exit(1);