fix: retry systemd unit activation after reload

This commit is contained in:
Peter Steinberger
2026-04-26 06:47:00 +01:00
parent 2b29594611
commit e4e69c5bc6
3 changed files with 77 additions and 3 deletions

View File

@@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai
browser command is sent, and reconnect stale persistent Playwright CDP
sessions for safe tab-list reads without replaying mutating browser actions.
Fixes #67728.
- Gateway/Linux: retry `systemctl --user enable` after a second daemon reload when the freshly written gateway unit is not visible yet on migrated systemd installs. Fixes #65184. Thanks @liushuaiiu.
- Telegram: preserve exact selected quote text when sending native quote replies, and retry with legacy replies if Telegram rejects quote parameters. (#71952) Thanks @rubencu.
- Plugins/CLI: preserve manifest name, description, format, and source metadata in cold `openclaw plugins list` output without importing plugin runtime. Thanks @shakkernerd.
- Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd.

View File

@@ -816,6 +816,52 @@ describe("systemd service install and uninstall", () => {
});
});
it("retries enable after reloading again when systemd cannot see the written unit yet", async () => {
await withNodeSystemdFixture(async ({ env }) => {
execFileMock
.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertUserSystemctlArgs(args, "status");
cb(null, "", "");
})
.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertUserSystemctlArgs(args, "daemon-reload");
cb(null, "", "");
})
.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertUserSystemctlArgs(args, "enable", NODE_SERVICE);
cb(
createExecFileError("enable failed"),
"",
"Unit file openclaw-node.service does not exist.",
);
})
.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertUserSystemctlArgs(args, "daemon-reload");
cb(null, "", "");
})
.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertUserSystemctlArgs(args, "enable", NODE_SERVICE);
cb(null, "", "");
})
.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertUserSystemctlArgs(args, "restart", NODE_SERVICE);
cb(null, "", "");
});
await installSystemdService({
env,
stdout: { write: vi.fn() } as unknown as NodeJS.WritableStream,
programArguments: ["/usr/bin/openclaw", "node", "run"],
workingDirectory: "/tmp",
environment: {
OPENCLAW_SYSTEMD_UNIT: "openclaw-node",
},
});
expect(execFileMock).toHaveBeenCalledTimes(6);
});
});
it("disables the OPENCLAW_SYSTEMD_UNIT override during uninstall", async () => {
await withNodeSystemdFixture(async ({ env, unitPath }) => {
await fs.mkdir(path.dirname(unitPath), { recursive: true });

View File

@@ -294,6 +294,18 @@ function isSystemdUnitNotEnabled(detail: string): boolean {
);
}
function isSystemdUnitMissingDetail(detail: string): boolean {
if (!detail) {
return false;
}
const normalized = normalizeLowercaseStringOrEmpty(detail);
return (
(normalized.includes("unit file") && normalized.includes("does not exist")) ||
normalized.includes("not-found") ||
normalized.includes("could not be found")
);
}
const isSystemctlBusUnavailable = isSystemdUserBusUnavailableDetail;
function isSystemdUserScopeUnavailable(detail: string): boolean {
@@ -541,17 +553,32 @@ export async function stageSystemdService({
async function activateSystemdService(params: { env: GatewayServiceEnv }) {
const serviceName = resolveSystemdServiceName(params.env);
const unitName = `${serviceName}.service`;
const reload = await execSystemctlUser(params.env, ["daemon-reload"]);
const reloadSystemd = async () => await execSystemctlUser(params.env, ["daemon-reload"]);
const reload = await reloadSystemd();
if (reload.code !== 0) {
throw new Error(`systemctl daemon-reload failed: ${reload.stderr || reload.stdout}`.trim());
}
const enable = await execSystemctlUser(params.env, ["enable", unitName]);
const runAfterReloadRetry = async (action: "enable" | "restart") => {
const result = await execSystemctlUser(params.env, [action, unitName]);
if (result.code === 0 || !isSystemdUnitMissingDetail(readSystemctlDetail(result))) {
return result;
}
const retryReload = await reloadSystemd();
if (retryReload.code !== 0) {
throw new Error(
`systemctl daemon-reload failed: ${retryReload.stderr || retryReload.stdout}`.trim(),
);
}
return await execSystemctlUser(params.env, [action, unitName]);
};
const enable = await runAfterReloadRetry("enable");
if (enable.code !== 0) {
throw new Error(`systemctl enable failed: ${enable.stderr || enable.stdout}`.trim());
}
const restart = await execSystemctlUser(params.env, ["restart", unitName]);
const restart = await runAfterReloadRetry("restart");
if (restart.code !== 0) {
throw new Error(`systemctl restart failed: ${restart.stderr || restart.stdout}`.trim());
}