fix(daemon): address clanker review findings for kickstart restart

Bug 1 (high): replace fixed sleep 1 with caller-PID polling in both
kickstart and start-after-exit handoff modes. The helper now waits until
kill -0 $caller_pid fails before issuing launchctl kickstart -k.

Bug 2 (medium): gate enable+bootstrap fallback on isLaunchctlNotLoaded().
Only attempt re-registration when kickstart -k fails because the job is
absent; all other kickstart failures now re-throw the original error.

Follows up on 3c0fd3dffe.
Fixes #43311, #43406, #43035, #43049
This commit is contained in:
Robin Waslander
2026-03-12 02:16:24 +01:00
committed by GitHub
parent b7a37c2023
commit 841ee24340
11 changed files with 137 additions and 27 deletions

View File

@@ -65,7 +65,7 @@ describe("runServiceRestart config pre-flight (#35862)", () => {
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({ environment: {} });
service.restart.mockResolvedValue(undefined);
service.restart.mockResolvedValue({ outcome: "completed" });
vi.unstubAllEnvs();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
@@ -163,7 +163,7 @@ describe("runServiceStart config pre-flight (#35862)", () => {
service.isLoaded.mockClear();
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
service.restart.mockResolvedValue(undefined);
service.restart.mockResolvedValue({ outcome: "completed" });
});
it("aborts start when config is invalid", async () => {

View File

@@ -64,7 +64,7 @@ describe("runServiceRestart token drift", () => {
service.readCommand.mockResolvedValue({
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
});
service.restart.mockResolvedValue(undefined);
service.restart.mockResolvedValue({ outcome: "completed" });
vi.unstubAllEnvs();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
@@ -176,4 +176,24 @@ describe("runServiceRestart token drift", () => {
expect(payload.result).toBe("restarted");
expect(payload.message).toContain("unmanaged process");
});
it("skips restart health checks when restart is only scheduled", async () => {
const postRestartCheck = vi.fn(async () => {});
service.restart.mockResolvedValue({ outcome: "scheduled" });
const result = await runServiceRestart({
serviceNoun: "Gateway",
service,
renderStartHints: () => [],
opts: { json: true },
postRestartCheck,
});
expect(result).toBe(true);
expect(postRestartCheck).not.toHaveBeenCalled();
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { result?: string; message?: string };
expect(payload.result).toBe("scheduled");
expect(payload.message).toBe("restart scheduled, gateway will restart momentarily");
});
});

View File

@@ -3,6 +3,7 @@ import { readBestEffortConfig, readConfigFileSnapshot } from "../../config/confi
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 type { GatewayService } from "../../daemon/service.js";
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
@@ -402,8 +403,23 @@ export async function runServiceRestart(params: {
}
try {
let restartResult: GatewayServiceRestartResult = { outcome: "completed" };
if (loaded) {
await params.service.restart({ env: process.env, stdout });
restartResult = await params.service.restart({ env: process.env, stdout });
}
if (restartResult.outcome === "scheduled") {
const message = `restart scheduled, ${params.serviceNoun.toLowerCase()} will restart momentarily`;
emit({
ok: true,
result: "scheduled",
message,
service: buildDaemonServiceSnapshot(params.service, loaded),
warnings: warnings.length ? warnings : undefined,
});
if (!json) {
defaultRuntime.log(message);
}
return true;
}
if (params.postRestartCheck) {
await params.postRestartCheck({ json, stdout, warnings, fail });