mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 19:32:27 +00:00
refactor: centralize daemon service start state flow
This commit is contained in:
@@ -205,9 +205,38 @@ describe("runServiceRestart token drift", () => {
|
||||
opts: { json: true },
|
||||
});
|
||||
|
||||
expect(service.isLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(service.isLoaded).toHaveBeenCalled();
|
||||
const payload = readJsonLog<{ result?: string; message?: string }>();
|
||||
expect(payload.result).toBe("scheduled");
|
||||
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
|
||||
});
|
||||
|
||||
it("fails start when restarting a stopped installed service errors", async () => {
|
||||
service.isLoaded.mockResolvedValue(false);
|
||||
service.restart.mockRejectedValue(new Error("launchctl kickstart failed: permission denied"));
|
||||
|
||||
await expect(runServiceStart(createServiceRunArgs())).rejects.toThrow("__exit__:1");
|
||||
|
||||
const payload = readJsonLog<{ ok?: boolean; error?: string }>();
|
||||
expect(payload.ok).toBe(false);
|
||||
expect(payload.error).toContain("launchctl kickstart failed: permission denied");
|
||||
});
|
||||
|
||||
it("falls back to not-loaded hints when start finds no install artifacts", async () => {
|
||||
service.isLoaded.mockResolvedValue(false);
|
||||
service.readCommand.mockResolvedValue(null);
|
||||
|
||||
await runServiceStart({
|
||||
serviceNoun: "Gateway",
|
||||
service,
|
||||
renderStartHints: () => ["openclaw gateway install"],
|
||||
opts: { json: true },
|
||||
});
|
||||
|
||||
const payload = readJsonLog<{ ok?: boolean; result?: string; hints?: string[] }>();
|
||||
expect(payload.ok).toBe(true);
|
||||
expect(payload.result).toBe("not-loaded");
|
||||
expect(payload.hints).toEqual(["openclaw gateway install"]);
|
||||
expect(service.restart).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { formatConfigIssueLines } from "../../config/issue-format.js";
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { checkTokenDrift } from "../../daemon/service-audit.js";
|
||||
import type { GatewayServiceRestartResult } from "../../daemon/service-types.js";
|
||||
import { describeGatewayServiceRestart } from "../../daemon/service.js";
|
||||
import { describeGatewayServiceRestart, startGatewayService } from "../../daemon/service.js";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
|
||||
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
|
||||
@@ -77,6 +77,17 @@ function createActionIO(params: { action: DaemonAction; json: boolean }) {
|
||||
return { stdout, emit, fail };
|
||||
}
|
||||
|
||||
function emitActionMessage(params: {
|
||||
json: boolean;
|
||||
emit: ReturnType<typeof createActionIO>["emit"];
|
||||
payload: Omit<DaemonActionResponse, "action">;
|
||||
}) {
|
||||
params.emit(params.payload);
|
||||
if (!params.json && params.payload.message) {
|
||||
defaultRuntime.log(params.payload.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleServiceNotLoaded(params: {
|
||||
serviceNoun: string;
|
||||
service: GatewayService;
|
||||
@@ -200,12 +211,13 @@ export async function runServiceStart(params: {
|
||||
const json = Boolean(params.opts?.json);
|
||||
const { stdout, emit, fail } = createActionIO({ action: "start", json });
|
||||
|
||||
const loaded = await resolveServiceLoadedOrFail({
|
||||
serviceNoun: params.serviceNoun,
|
||||
service: params.service,
|
||||
fail,
|
||||
});
|
||||
if (loaded === null) {
|
||||
if (
|
||||
(await resolveServiceLoadedOrFail({
|
||||
serviceNoun: params.serviceNoun,
|
||||
service: params.service,
|
||||
fail,
|
||||
})) === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Pre-flight config validation (#35862) — run for both loaded and not-loaded
|
||||
@@ -219,71 +231,45 @@ export async function runServiceStart(params: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!loaded) {
|
||||
// Service was stopped (e.g. `gateway stop` booted out the LaunchAgent).
|
||||
// Attempt a restart, which handles re-bootstrapping the service. Without
|
||||
// this, `start` after `stop` just prints hints and does nothing (#53878).
|
||||
try {
|
||||
const restartResult = await params.service.restart({ env: process.env, stdout });
|
||||
const restartStatus = describeGatewayServiceRestart(params.serviceNoun, restartResult);
|
||||
const postLoaded = await params.service.isLoaded({ env: process.env }).catch(() => true);
|
||||
emit({
|
||||
ok: true,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, postLoaded),
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(restartStatus.message);
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
// Bootstrap failed (e.g. plist was deleted, not just booted out).
|
||||
// Fall through to the not-loaded hints.
|
||||
try {
|
||||
const startResult = await startGatewayService(params.service, { env: process.env, stdout });
|
||||
if (startResult.outcome === "missing-install") {
|
||||
await handleServiceNotLoaded({
|
||||
serviceNoun: params.serviceNoun,
|
||||
service: params.service,
|
||||
loaded,
|
||||
loaded: startResult.state.loaded,
|
||||
renderStartHints: params.renderStartHints,
|
||||
json,
|
||||
emit,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const restartResult = await params.service.restart({ env: process.env, stdout });
|
||||
const restartStatus = describeGatewayServiceRestart(params.serviceNoun, restartResult);
|
||||
if (restartStatus.scheduled) {
|
||||
emit({
|
||||
ok: true,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, loaded),
|
||||
if (startResult.outcome === "scheduled") {
|
||||
const restartStatus = describeGatewayServiceRestart(params.serviceNoun, {
|
||||
outcome: "scheduled",
|
||||
});
|
||||
emitActionMessage({
|
||||
json,
|
||||
emit,
|
||||
payload: {
|
||||
ok: true,
|
||||
result: "scheduled",
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, startResult.state.loaded),
|
||||
},
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(restartStatus.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "started",
|
||||
service: buildDaemonServiceSnapshot(params.service, startResult.state.loaded),
|
||||
});
|
||||
} catch (err) {
|
||||
const hints = params.renderStartHints();
|
||||
fail(`${params.serviceNoun} start failed: ${String(err)}`, hints);
|
||||
return;
|
||||
}
|
||||
|
||||
let started = true;
|
||||
try {
|
||||
started = await params.service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
started = true;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "started",
|
||||
service: buildDaemonServiceSnapshot(params.service, started),
|
||||
});
|
||||
}
|
||||
|
||||
export async function runServiceStop(params: {
|
||||
@@ -371,16 +357,17 @@ export async function runServiceRestart(params: {
|
||||
restartStatus: ReturnType<typeof describeGatewayServiceRestart>,
|
||||
serviceLoaded: boolean,
|
||||
) => {
|
||||
emit({
|
||||
ok: true,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, serviceLoaded),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
emitActionMessage({
|
||||
json,
|
||||
emit,
|
||||
payload: {
|
||||
ok: true,
|
||||
result: restartStatus.daemonActionResult,
|
||||
message: restartStatus.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, serviceLoaded),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
},
|
||||
});
|
||||
if (!json) {
|
||||
defaultRuntime.log(restartStatus.message);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -42,9 +42,11 @@ export function resetLifecycleServiceMocks() {
|
||||
service.stage.mockClear();
|
||||
service.isLoaded.mockClear();
|
||||
service.readCommand.mockClear();
|
||||
service.readRuntime.mockClear();
|
||||
service.restart.mockClear();
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readCommand.mockResolvedValue({ programArguments: [], environment: {} });
|
||||
service.readRuntime.mockResolvedValue({ status: "running" });
|
||||
service.restart.mockResolvedValue({ outcome: "completed" });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user