mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix: gateway restart avoids false failure when systemd probe is unavailable (#123069)
* fix(cli): avoid false gateway restart failures Allow restart to proceed when the loaded-state probe is unavailable but an installed service definition identifies the native manager. Native restart errors and Gateway health checks remain authoritative. * fix(daemon): detect system-scope restart definitions Move installed-definition detection into the platform service adapter so gateway restart can recover from an unavailable loaded-state probe for both user- and system-scope systemd units.
This commit is contained in:
committed by
GitHub
parent
af377128b4
commit
5ed9bc2bd2
@@ -268,6 +268,33 @@ describe("runServiceRestart token drift", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("restarts an installed system-scope service when its loaded-state probe is unavailable", async () => {
|
||||
service.isLoaded.mockRejectedValue(
|
||||
new Error(
|
||||
"systemctl is-enabled unavailable: Command failed during launch or output capture (EACCES)",
|
||||
),
|
||||
);
|
||||
service.readCommand.mockResolvedValue(null);
|
||||
const hasInstalledDefinition = vi.fn(async () => true);
|
||||
const postRestartCheck = vi.fn(async () => {});
|
||||
|
||||
await expect(
|
||||
runServiceRestart({
|
||||
...createServiceRunArgs(),
|
||||
service: { ...service, hasInstalledDefinition } as GatewayService,
|
||||
postRestartCheck,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(hasInstalledDefinition).toHaveBeenCalledWith({ env: process.env });
|
||||
expect(service.restart).toHaveBeenCalledTimes(1);
|
||||
expect(postRestartCheck).toHaveBeenCalledTimes(1);
|
||||
expect(readJsonLog<{ ok?: boolean; result?: string }>()).toMatchObject({
|
||||
ok: true,
|
||||
result: "restarted",
|
||||
});
|
||||
});
|
||||
|
||||
it("aborts loaded-service mutation when the service guard rejects", async () => {
|
||||
const repairLoadedService = vi.fn();
|
||||
|
||||
|
||||
@@ -124,11 +124,22 @@ async function resolveServiceLoadedOrFail(params: {
|
||||
serviceNoun: string;
|
||||
service: GatewayService;
|
||||
fail: ReturnType<typeof createDaemonActionContext>["fail"];
|
||||
acceptInstalledDefinition?: boolean;
|
||||
}): Promise<boolean | null> {
|
||||
// Returning null keeps failure emission centralized in the caller's action context.
|
||||
try {
|
||||
return await params.service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
if (params.acceptInstalledDefinition) {
|
||||
// The adapter owns platform-specific install discovery; systemd spans
|
||||
// user, system, marker-owned, and dueling definitions.
|
||||
const installed = params.service.hasInstalledDefinition
|
||||
? await params.service.hasInstalledDefinition({ env: process.env }).catch(() => false)
|
||||
: Boolean(await params.service.readCommand(process.env).catch(() => null));
|
||||
if (installed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
params.fail(`${params.serviceNoun} service check failed: ${String(err)}`);
|
||||
return null;
|
||||
}
|
||||
@@ -514,6 +525,7 @@ export async function runServiceRestart(params: {
|
||||
serviceNoun: params.serviceNoun,
|
||||
service: params.service,
|
||||
fail,
|
||||
acceptInstalledDefinition: true,
|
||||
});
|
||||
if (loaded === null) {
|
||||
return false;
|
||||
@@ -679,21 +691,11 @@ export async function runServiceRestart(params: {
|
||||
}
|
||||
}
|
||||
}
|
||||
let restarted = loaded;
|
||||
if (loaded) {
|
||||
try {
|
||||
restarted = await params.service.isLoaded({ env: process.env });
|
||||
} catch {
|
||||
restarted = true;
|
||||
}
|
||||
} else if (recoveredLoadedState !== null) {
|
||||
restarted = recoveredLoadedState;
|
||||
}
|
||||
emit({
|
||||
ok: true,
|
||||
result: "restarted",
|
||||
message: handledRecovery?.message ?? handledRepair?.message,
|
||||
service: buildDaemonServiceSnapshot(params.service, restarted),
|
||||
service: buildDaemonServiceSnapshot(params.service, loaded || recoveredLoadedState === true),
|
||||
warnings: warnings.length ? warnings : undefined,
|
||||
});
|
||||
const actionMessage = handledRecovery?.message ?? handledRepair?.message;
|
||||
|
||||
@@ -26,6 +26,7 @@ function withNodeInstallEnv(args: GatewayServiceInstallArgs): GatewayServiceInst
|
||||
/** Returns a service controller bound to node-host labels across all platforms. */
|
||||
export function resolveNodeService(): GatewayService {
|
||||
const base = resolveGatewayService();
|
||||
const hasInstalledDefinition = base.hasInstalledDefinition;
|
||||
return {
|
||||
...base,
|
||||
stage: (args) => base.stage(withNodeInstallEnv(args)),
|
||||
@@ -39,6 +40,9 @@ export function resolveNodeService(): GatewayService {
|
||||
// wedged service manager instead of hanging the whole status command.
|
||||
return base.isLoaded({ env: withNodeServiceEnv(args.env ?? {}), timeoutMs: args.timeoutMs });
|
||||
},
|
||||
hasInstalledDefinition: hasInstalledDefinition
|
||||
? (args) => hasInstalledDefinition({ ...args, env: withNodeServiceEnv(args.env ?? {}) })
|
||||
: undefined,
|
||||
readCommand: (env) => base.readCommand(withNodeServiceEnv(env)),
|
||||
readRuntime: (env, opts) => base.readRuntime(withNodeServiceEnv(env), opts),
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ import type {
|
||||
GatewayServiceState,
|
||||
} from "./service-types.js";
|
||||
import {
|
||||
findInstalledSystemdGatewayScope,
|
||||
installSystemdService,
|
||||
isSystemdServiceEnabled,
|
||||
readSystemdServiceExecStart,
|
||||
@@ -84,6 +85,7 @@ export type GatewayService = {
|
||||
restart: (args: GatewayServiceControlArgs) => Promise<GatewayServiceRestartResult>;
|
||||
isLoaded: (args: GatewayServiceEnvArgs) => Promise<boolean>;
|
||||
isEnabled?: (args: GatewayServiceEnvArgs) => Promise<boolean>;
|
||||
hasInstalledDefinition?: (args: GatewayServiceEnvArgs) => Promise<boolean>;
|
||||
readCommand: (env: GatewayServiceEnv) => Promise<GatewayServiceCommandConfig | null>;
|
||||
readRuntime: (
|
||||
env: GatewayServiceEnv,
|
||||
@@ -354,6 +356,8 @@ const GATEWAY_SERVICE_REGISTRY: Record<SupportedGatewayServicePlatform, GatewayS
|
||||
stop: stopSystemdService,
|
||||
restart: restartSystemdService,
|
||||
isLoaded: isSystemdServiceEnabled,
|
||||
hasInstalledDefinition: async ({ env }) =>
|
||||
(await findInstalledSystemdGatewayScope(env ?? process.env)) !== null,
|
||||
readCommand: readSystemdServiceExecStart,
|
||||
readRuntime: readSystemdServiceRuntime,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user