import { parseArgs } from "node:util"; import { createDb, migrateToLatest } from "@umt/db"; import denoJson from "../deno.json" with { type: "json" }; import { loadConfig } from "./config.ts"; import { createApp } from "./http/app.ts"; import { EventBus } from "./http/sse.ts"; import { createLogger } from "./logger.ts"; import { runPreflight } from "./preflight.ts"; import { startWorker } from "./worker/index.ts"; // api/worker/all 是常驻角色;migrate/preflight/probe 是容器入口脚本用的一次性子命令 const Roles = ["api", "worker", "all", "migrate", "preflight", "probe"] as const; type Role = (typeof Roles)[number]; const { values } = parseArgs({ args: Deno.args, options: { role: { type: "string", default: "all" } }, strict: true }); const role = values.role as Role; if (!Roles.includes(role)) { console.error(`--role 必须是 ${Roles.join("|")}`); Deno.exit(2); } // 健康探测:只打本机回环端口,不需要完整配置(HEALTHCHECK 用,镜像里不装 curl) if (role === "probe") { const port = Deno.env.get("PORT") ?? "4380"; try { const res = await fetch(`http://127.0.0.1:${port}/api/v1/health`, { signal: AbortSignal.timeout(3000) }); const body = (await res.json()) as { data?: { status?: string } }; Deno.exit(res.ok && body.data?.status === "ok" ? 0 : 1); } catch { Deno.exit(1); } } let config: ReturnType; try { config = loadConfig(); } catch (err) { console.error((err as Error).message); Deno.exit(2); } const logger = createLogger({ level: config.LOG_LEVEL, pretty: config.NODE_ENV === "development" && Deno.stdout.isTerminal(), }); const db = createDb({ connectionString: config.DATABASE_URL }); if (role === "migrate" || role === "preflight") { let code = 0; try { if (role === "migrate") { const { results } = await migrateToLatest(db); for (const r of results ?? []) logger.info({ migration: r.migrationName, status: r.status }, "migration"); logger.info({ applied: results?.length ?? 0 }, "迁移完成"); } else { const problems = await runPreflight({ config, db }); for (const p of problems) logger.error({ check: p.check }, p.message); if (problems.length) code = 1; else logger.info("启动自检通过"); } } catch (err) { logger.error({ err }, `${role} 失败`); code = 1; } finally { await db.destroy(); } Deno.exit(code); } const bus = new EventBus(); const stops: Array<() => Promise> = []; if (role === "api" || role === "all") { const app = createApp({ config, logger, bus, db, role, version: denoJson.version, ...(config.WEB_ROOT ? { webRoot: config.WEB_ROOT } : {}), }); const ac = new AbortController(); const server = Deno.serve( { port: config.PORT, hostname: config.HOST, signal: ac.signal, onListen: ({ port }) => logger.info( { port, role, webRoot: config.WEB_ROOT ?? null }, `API 已启动 http://localhost:${port}/api/v1/health`, ), }, app.fetch, ); stops.push(async () => { // abort 后 Deno.serve 会断开 SSE 等长连接,finished 在所有连接关闭后 resolve ac.abort(); await server.finished; }); } if (role === "worker" || role === "all") { const worker = startWorker({ logger, heartbeatMs: config.WORKER_HEARTBEAT_MS, bus }); stops.push(worker.stop); } let stopping = false; async function shutdown(signal: Deno.Signal) { if (stopping) return; stopping = true; logger.info({ signal }, "正在退出"); // 兜底:10 秒内没退干净就强退,避免容器 stop 超时被 SIGKILL const force = setTimeout(() => Deno.exit(1), 10_000); await Promise.allSettled(stops.map((s) => s())); await db.destroy(); clearTimeout(force); Deno.exit(0); } Deno.addSignalListener("SIGINT", () => void shutdown("SIGINT")); Deno.addSignalListener("SIGTERM", () => void shutdown("SIGTERM"));