feat: add gateway config/update restart flow
This commit is contained in:
@@ -6,10 +6,16 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../../config/config.js";
|
||||
import { buildConfigSchema } from "../../config/schema.js";
|
||||
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
|
||||
import {
|
||||
type RestartSentinelPayload,
|
||||
writeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateConfigApplyParams,
|
||||
validateConfigGetParams,
|
||||
validateConfigSchemaParams,
|
||||
validateConfigSetParams,
|
||||
@@ -102,4 +108,102 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
"config.apply": async ({ params, respond }) => {
|
||||
if (!validateConfigApplyParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid config.apply params: ${formatValidationErrors(validateConfigApplyParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rawValue = (params as { raw?: unknown }).raw;
|
||||
if (typeof rawValue !== "string") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"invalid config.apply params: raw (string) required",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const parsedRes = parseConfigJson5(rawValue);
|
||||
if (!parsedRes.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const validated = validateConfigObject(parsedRes.parsed);
|
||||
if (!validated.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
|
||||
details: { issues: validated.issues },
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await writeConfigFile(validated.config);
|
||||
|
||||
const sessionKey =
|
||||
typeof (params as { sessionKey?: unknown }).sessionKey === "string"
|
||||
? (params as { sessionKey?: string }).sessionKey?.trim() || undefined
|
||||
: undefined;
|
||||
const note =
|
||||
typeof (params as { note?: unknown }).note === "string"
|
||||
? (params as { note?: string }).note?.trim() || undefined
|
||||
: undefined;
|
||||
const restartDelayMsRaw = (params as { restartDelayMs?: unknown })
|
||||
.restartDelayMs;
|
||||
const restartDelayMs =
|
||||
typeof restartDelayMsRaw === "number" &&
|
||||
Number.isFinite(restartDelayMsRaw)
|
||||
? Math.max(0, Math.floor(restartDelayMsRaw))
|
||||
: undefined;
|
||||
|
||||
const payload: RestartSentinelPayload = {
|
||||
kind: "config-apply",
|
||||
status: "ok",
|
||||
ts: Date.now(),
|
||||
sessionKey,
|
||||
message: note ?? null,
|
||||
stats: {
|
||||
mode: "config.apply",
|
||||
root: CONFIG_PATH_CLAWDBOT,
|
||||
},
|
||||
};
|
||||
let sentinelPath: string | null = null;
|
||||
try {
|
||||
sentinelPath = await writeRestartSentinel(payload);
|
||||
} catch {
|
||||
sentinelPath = null;
|
||||
}
|
||||
const restart = scheduleGatewaySigusr1Restart({
|
||||
delayMs: restartDelayMs,
|
||||
reason: "config.apply",
|
||||
});
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
path: CONFIG_PATH_CLAWDBOT,
|
||||
config: validated.config,
|
||||
restart,
|
||||
sentinel: {
|
||||
path: sentinelPath,
|
||||
payload,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
119
src/gateway/server-methods/update.ts
Normal file
119
src/gateway/server-methods/update.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
|
||||
import {
|
||||
type RestartSentinelPayload,
|
||||
writeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import { runGatewayUpdate } from "../../infra/update-runner.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateUpdateRunParams,
|
||||
} from "../protocol/index.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
export const updateHandlers: GatewayRequestHandlers = {
|
||||
"update.run": async ({ params, respond }) => {
|
||||
if (!validateUpdateRunParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid update.run params: ${formatValidationErrors(validateUpdateRunParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const sessionKey =
|
||||
typeof (params as { sessionKey?: unknown }).sessionKey === "string"
|
||||
? (params as { sessionKey?: string }).sessionKey?.trim() || undefined
|
||||
: undefined;
|
||||
const note =
|
||||
typeof (params as { note?: unknown }).note === "string"
|
||||
? (params as { note?: string }).note?.trim() || undefined
|
||||
: undefined;
|
||||
const restartDelayMsRaw = (params as { restartDelayMs?: unknown })
|
||||
.restartDelayMs;
|
||||
const restartDelayMs =
|
||||
typeof restartDelayMsRaw === "number" &&
|
||||
Number.isFinite(restartDelayMsRaw)
|
||||
? Math.max(0, Math.floor(restartDelayMsRaw))
|
||||
: undefined;
|
||||
const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs;
|
||||
const timeoutMs =
|
||||
typeof timeoutMsRaw === "number" && Number.isFinite(timeoutMsRaw)
|
||||
? Math.max(1000, Math.floor(timeoutMsRaw))
|
||||
: undefined;
|
||||
|
||||
let result: Awaited<ReturnType<typeof runGatewayUpdate>>;
|
||||
try {
|
||||
result = await runGatewayUpdate({
|
||||
timeoutMs,
|
||||
cwd: process.cwd(),
|
||||
argv1: process.argv[1],
|
||||
});
|
||||
} catch (err) {
|
||||
result = {
|
||||
status: "error",
|
||||
mode: "unknown",
|
||||
reason: String(err),
|
||||
steps: [],
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const payload: RestartSentinelPayload = {
|
||||
kind: "update",
|
||||
status: result.status,
|
||||
ts: Date.now(),
|
||||
sessionKey,
|
||||
message: note ?? null,
|
||||
stats: {
|
||||
mode: result.mode,
|
||||
root: result.root ?? undefined,
|
||||
before: result.before ?? null,
|
||||
after: result.after ?? null,
|
||||
steps: result.steps.map((step) => ({
|
||||
name: step.name,
|
||||
command: step.command,
|
||||
cwd: step.cwd,
|
||||
durationMs: step.durationMs,
|
||||
log: {
|
||||
stdoutTail: step.stdoutTail ?? null,
|
||||
stderrTail: step.stderrTail ?? null,
|
||||
exitCode: step.exitCode ?? null,
|
||||
},
|
||||
})),
|
||||
reason: result.reason ?? null,
|
||||
durationMs: result.durationMs,
|
||||
},
|
||||
};
|
||||
|
||||
let sentinelPath: string | null = null;
|
||||
try {
|
||||
sentinelPath = await writeRestartSentinel(payload);
|
||||
} catch {
|
||||
sentinelPath = null;
|
||||
}
|
||||
|
||||
const restart = scheduleGatewaySigusr1Restart({
|
||||
delayMs: restartDelayMs,
|
||||
reason: "update.run",
|
||||
});
|
||||
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
result,
|
||||
restart,
|
||||
sentinel: {
|
||||
path: sentinelPath,
|
||||
payload,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user