fix: aggregate HWPOD native service lifecycle

This commit is contained in:
root
2026-07-23 06:45:16 +02:00
parent 105526c9c6
commit 3311348f34
3 changed files with 62 additions and 4 deletions
+3 -3
View File
@@ -27,10 +27,10 @@ if (argv[0] === "tasktree") {
process.exitCode = 1;
}
} else if (argv[0] === "hwpod" && argv[1] === "service") {
const { hwpodNativeServiceCommand, hwpodNativeServicesStatus } = await import("../../src/hwpod-native-service.ts");
const { hwpodNativeServiceCommand, hwpodNativeServicesCommand } = await import("../../src/hwpod-native-service.ts");
try {
const result = argv[2] === "status" && argv[3] === undefined
? await hwpodNativeServicesStatus({ cwd: process.cwd(), env: process.env })
const result = ["start", "stop", "restart", "status", "logs"].includes(argv[2] ?? "") && argv[3] === undefined
? await hwpodNativeServicesCommand({ action: argv[2], cwd: process.cwd(), env: process.env })
: await hwpodNativeServiceCommand({ service: argv[2], action: argv[3] ?? "status", cwd: process.cwd(), env: process.env });
console.log(JSON.stringify(result, null, 2));
process.exitCode = result?.ok === false ? 1 : 0;
+13 -1
View File
@@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { hwpodNativeServicesStatus } from "./hwpod-native-service.ts";
import { hwpodNativeServicesCommand, hwpodNativeServicesStatus } from "./hwpod-native-service.ts";
test("HWPOD aggregate status reports API, worker, and Web together", async () => {
const cwd = await mkdtemp(path.join(tmpdir(), "hwpod-native-status-"));
@@ -23,3 +23,15 @@ test("HWPOD aggregate status reports API, worker, and Web together", async () =>
await rm(cwd, { recursive: true, force: true });
}
});
test("aggregate stop owns all HWPOD native services", async () => {
const cwd = await mkdtemp(path.join(tmpdir(), "hwpod-native-stop-"));
try {
const result = await hwpodNativeServicesCommand({ action: "stop", cwd, env: { HWPOD_NATIVE_SERVICE_STATE_DIR: ".state/services" } });
expect(result.ok).toBe(true);
expect(Object.keys(result.services).sort()).toEqual(["api", "web", "worker"]);
expect(result.summary.completed).toBe(3);
} finally {
await rm(cwd, { recursive: true, force: true });
}
});
+46
View File
@@ -24,6 +24,52 @@ export async function hwpodNativeServicesStatus(input: { cwd: string; env: Recor
};
}
export async function hwpodNativeServicesCommand(input: { action: string; cwd: string; env: Record<string, string | undefined> }) {
if (input.action === "status") return hwpodNativeServicesStatus(input);
if (input.action === "logs") {
const results = await Promise.all(SERVICE_NAMES.map((service) => hwpodNativeServiceCommand({ service, action: "logs", ...input })));
return aggregateResult("logs", results);
}
if (input.action === "stop") {
const results = [];
for (const service of [...SERVICE_NAMES].reverse()) results.push(await hwpodNativeServiceCommand({ service, action: "stop", ...input }));
return aggregateResult("stop", results);
}
if (input.action === "start" || input.action === "restart") {
const stopped = [];
if (input.action === "restart") {
for (const service of [...SERVICE_NAMES].reverse()) stopped.push(await hwpodNativeServiceCommand({ service, action: "stop", ...input }));
}
const started = [];
for (const service of SERVICE_NAMES) started.push(await hwpodNativeServiceCommand({ service, action: "start", ...input }));
const readiness = await waitForServicesReady(input);
return { ...aggregateResult(input.action, started), stopped, readiness, ok: readiness.ok, status: readiness.status };
}
throw codedError("unsupported_service_action", `unsupported aggregate HWPOD service action: ${input.action}`);
}
function aggregateResult(action: string, results: any[]) {
const ok = results.every((result) => result.ok !== false);
return {
ok,
operation: `hwpod.service.${action}`,
status: ok ? "completed" : "failed",
summary: { total: SERVICE_NAMES.length, completed: results.filter((result) => result.ok !== false).length, failed: results.filter((result) => result.ok === false).length },
services: Object.fromEntries(results.map((result) => [result.service, result])),
valuesPrinted: false,
};
}
async function waitForServicesReady(input: { cwd: string; env: Record<string, string | undefined> }) {
const deadline = Date.now() + 30_000;
let current = await hwpodNativeServicesStatus(input);
while (!current.ok && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 250));
current = await hwpodNativeServicesStatus(input);
}
return current;
}
export async function hwpodNativeServiceCommand(input: { service: string; action: string; cwd: string; env: Record<string, string | undefined> }) {
const service = validService(input.service);
const stateDir = path.resolve(input.cwd, requiredEnv(input.env, "HWPOD_NATIVE_SERVICE_STATE_DIR"));