fix(hwpod): 使用 YAML 数据库和健康超时

This commit is contained in:
root
2026-07-21 03:17:11 +02:00
parent 3c61bee644
commit fe46bfcaaa
2 changed files with 12 additions and 5 deletions
+7 -3
View File
@@ -29,11 +29,12 @@ async function start(service: ServiceName, stateDir: string, stateFile: string,
await mkdir(stateDir, { recursive: true });
const endpoints = serviceEndpoints(service, env);
const command = serviceCommand(service);
const healthTimeoutMs = positiveIntegerEnv(env, "HWPOD_HEALTH_TIMEOUT_MS");
const logFd = openSync(logFile, "w");
const child = spawn(command[0], command.slice(1), { cwd: service === "web" ? path.join(cwd, "web/hwlab-cloud-web") : cwd, env: serviceEnv(service, env), detached: true, stdio: ["ignore", logFd, logFd] });
closeSync(logFd);
child.unref();
const state = { service, pid: child.pid, status: "starting", command, cwd, logFile, endpoints, healthUrl: endpoints.probe.url, startedAt: new Date().toISOString() };
const state = { service, pid: child.pid, status: "starting", command, cwd, logFile, endpoints, healthUrl: endpoints.probe.url, healthTimeoutMs, startedAt: new Date().toISOString() };
await writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
return { ok: true, operation: `hwpod.service.${service}.start`, ...state, nextCommand: `hwlab-cli hwpod service ${service} status` };
}
@@ -49,7 +50,9 @@ async function stop(service: ServiceName, stateFile: string, logFile: string) {
async function status(service: ServiceName, stateFile: string, logFile: string) {
const state = await readState(stateFile);
const processRunning = Boolean(state?.pid && alive(state.pid));
const health = processRunning && state?.healthUrl ? await probeHealth(state.healthUrl) : { ok: false, status: null, error: processRunning ? "health-url-not-declared" : "process-not-running" };
const health = processRunning && state?.healthUrl && state?.healthTimeoutMs
? await probeHealth(state.healthUrl, state.healthTimeoutMs)
: { ok: false, status: null, error: processRunning ? "health-config-not-declared" : "process-not-running" };
const running = processRunning && health.ok;
return { ok: running, operation: `hwpod.service.${service}.status`, service, status: running ? "running" : processRunning ? "degraded" : "stopped", pid: processRunning ? state.pid : null, endpoints: state?.endpoints ?? null, health, stateFile, logFile, nextCommand: `hwlab-cli hwpod service ${service} logs` };
}
@@ -83,8 +86,9 @@ function endpoint(host: string, port: string, pathname: string) { const numericP
function validPort(value: string) { const port = Number(value); if (!Number.isInteger(port) || port < 1 || port > 65535) throw codedError("invalid_service_port", `invalid HWPOD service port: ${value}`); return port; }
function validService(value: string): ServiceName { if (value === "api" || value === "worker" || value === "web") return value; throw codedError("invalid_service", "HWPOD service must be api, worker, or web"); }
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("native_exposure_config_required", `${key} is required from YAML-first HWPOD native development config`); return value; }
function positiveIntegerEnv(env: Record<string, string | undefined>, key: string) { const value = Number(requiredEnv(env, key)); if (!Number.isSafeInteger(value) || value <= 0) throw codedError("native_exposure_config_invalid", `${key} must be a positive integer`); return value; }
function fixedHttpsOrigin(value: string, key: string) { try { const parsed = new URL(value); if (parsed.protocol === "https:" && parsed.pathname === "/" && !parsed.search && !parsed.hash && !parsed.port) return parsed.origin; } catch {} throw codedError("native_exposure_config_invalid", `${key} must be a pathless HTTPS origin`); }
async function readState(file: string) { return JSON.parse(await readFile(file, "utf8").catch(() => "null")); }
function alive(pid: number) { try { process.kill(pid, 0); return true; } catch { return false; } }
async function probeHealth(url: string) { try { const response = await fetch(url, { signal: AbortSignal.timeout(1000) }); return { ok: response.ok, status: response.status, url }; } catch (error) { return { ok: false, status: null, url, error: error instanceof Error ? error.message : String(error) }; } }
async function probeHealth(url: string, timeoutMs: number) { try { const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); return { ok: response.ok, status: response.status, url }; } catch (error) { return { ok: false, status: null, url, error: error instanceof Error ? error.message : String(error) }; } }
function codedError(code: string, message: string, details?: unknown) { return Object.assign(new Error(message), { code, details }); }