feat(ops): restart individual PM2 children and enrich manage UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
PM2_HTTP_NAME,
|
||||
PM2_WORKER_NAME,
|
||||
recreateChildren,
|
||||
restartPm2Process,
|
||||
stopPm2App,
|
||||
} from "./pm2-bridge.js";
|
||||
|
||||
@@ -149,6 +150,43 @@ async function waitUntilIdle(timeoutMs, lockToken) {
|
||||
return { ok: active === 0, active };
|
||||
}
|
||||
|
||||
/** @param {unknown} raw */
|
||||
function parsePmId(raw) {
|
||||
if (raw == null || raw === "") return null;
|
||||
const id = Math.floor(Number(raw));
|
||||
if (!Number.isFinite(id) || id < 0) return Number.NaN;
|
||||
return id;
|
||||
}
|
||||
|
||||
async function handleProcessRestart(pmId, reply) {
|
||||
const lock = tryAcquireOpsLock(`process-restart:${process.pid}`);
|
||||
if (!lock.ok) {
|
||||
return reply.code(409).send({ error: lock.error, holder: lock.holder });
|
||||
}
|
||||
|
||||
try {
|
||||
const restarted = await restartPm2Process(pmId);
|
||||
return reply.send({ ok: true, ...restarted, status: await buildStatus() });
|
||||
} catch (err) {
|
||||
const code = err && typeof err === "object" ? err.code : undefined;
|
||||
if (code === "BAD_REQUEST") {
|
||||
return reply.code(400).send({ error: "pmId is required" });
|
||||
}
|
||||
if (code === "NOT_FOUND") {
|
||||
return reply.code(404).send({ error: "process not found" });
|
||||
}
|
||||
if (code === "FORBIDDEN") {
|
||||
return reply.code(403).send({ error: "process is not a JerapahFlow child" });
|
||||
}
|
||||
log.error({ err, pmId }, "process restart failed");
|
||||
return reply.code(500).send({
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
} finally {
|
||||
releaseOpsLock(lock.token);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildStatus() {
|
||||
const state = readControlState();
|
||||
const children = await describeChildren();
|
||||
@@ -258,9 +296,18 @@ server.post(
|
||||
"/ops/restart",
|
||||
{ onRequest: [server.authenticate, server.requireAdmin] },
|
||||
async (req, reply) => {
|
||||
const body = /** @type {{ force?: boolean, timeoutMs?: number }} */ (
|
||||
const body = /** @type {{ force?: boolean, timeoutMs?: number, pmId?: number }} */ (
|
||||
req.body ?? {}
|
||||
);
|
||||
|
||||
const pmId = parsePmId(body.pmId);
|
||||
if (pmId != null) {
|
||||
if (Number.isNaN(pmId)) {
|
||||
return reply.code(400).send({ error: "pmId is required" });
|
||||
}
|
||||
return handleProcessRestart(pmId, reply);
|
||||
}
|
||||
|
||||
const force = Boolean(body.force);
|
||||
const timeoutMs = Math.min(
|
||||
Math.max(Number(body.timeoutMs) || DRAIN_DEFAULT_MS, 1_000),
|
||||
@@ -342,6 +389,19 @@ server.post(
|
||||
},
|
||||
);
|
||||
|
||||
server.post(
|
||||
"/ops/process/restart",
|
||||
{ onRequest: [server.authenticate, server.requireAdmin] },
|
||||
async (req, reply) => {
|
||||
const body = /** @type {{ pmId?: number }} */ (req.body ?? {});
|
||||
const pmId = parsePmId(body.pmId);
|
||||
if (pmId == null || Number.isNaN(pmId)) {
|
||||
return reply.code(400).send({ error: "pmId is required" });
|
||||
}
|
||||
return handleProcessRestart(pmId, reply);
|
||||
},
|
||||
);
|
||||
|
||||
server.post(
|
||||
"/ops/scale",
|
||||
{ onRequest: [server.authenticate, server.requireAdmin] },
|
||||
|
||||
Reference in New Issue
Block a user