fix: check managed systemd unit before is-enabled (#38819)

This commit is contained in:
Ayaan Zaidi
2026-03-07 17:10:48 +05:30
committed by Ayaan Zaidi
parent addd290f88
commit 26c9796736
3 changed files with 41 additions and 8 deletions

View File

@@ -228,6 +228,7 @@ Docs: https://docs.openclaw.ai
- Agents/reply MEDIA delivery: normalize local assistant `MEDIA:` paths before block/final delivery, keep media dedupe aligned with message-tool sends, and contain malformed media normalization failures so generated files send reliably instead of falling back to empty responses. (#38572) Thanks @obviyus.
- Sessions/bootstrap cache rollover invalidation: clear cached workspace bootstrap snapshots whenever an existing `sessionKey` rolls to a new `sessionId` across auto-reply, command, and isolated cron session resolvers, so `AGENTS.md`/`MEMORY.md`/`USER.md` updates are reloaded after daily, idle, or forced session resets instead of staying stale until gateway restart. (#38494) Thanks @LivingInDrm.
- Gateway/Telegram polling health monitor: skip stale-socket restarts for Telegram long-polling channels and thread channel identity through shared health evaluation so polling connections are not restarted on the WebSocket stale-socket heuristic. (#38395) Thanks @ql-wade and @Takhoffman.
- Daemon/systemd fresh-install probe: check for OpenClaw's managed user unit before running `systemctl --user is-enabled`, so first-time Linux installs no longer fail on generic missing-unit probe errors. (#38819) Thanks @adaHubble.
## 2026.3.2

View File

@@ -1,3 +1,4 @@
import fs from "node:fs/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
const execFileMock = vi.hoisted(() => vi.fn());
@@ -66,44 +67,65 @@ describe("systemd availability", () => {
});
describe("isSystemdServiceEnabled", () => {
const mockManagedUnitPresent = () => {
vi.spyOn(fs, "access").mockResolvedValue(undefined);
};
beforeEach(() => {
vi.restoreAllMocks();
execFileMock.mockReset();
});
it("returns false when systemctl is not present", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
mockManagedUnitPresent();
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
const err = new Error("spawn systemctl EACCES") as Error & { code?: string };
err.code = "EACCES";
cb(err, "", "");
});
const result = await isSystemdServiceEnabled({ env: {} });
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
expect(result).toBe(false);
});
it("returns false without calling systemctl when the managed unit file is missing", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
const err = new Error("missing unit") as NodeJS.ErrnoException;
err.code = "ENOENT";
vi.spyOn(fs, "access").mockRejectedValueOnce(err);
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
expect(result).toBe(false);
expect(execFileMock).not.toHaveBeenCalled();
});
it("calls systemctl is-enabled when systemctl is present", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
mockManagedUnitPresent();
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
cb(null, "enabled", "");
});
const result = await isSystemdServiceEnabled({ env: {} });
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
expect(result).toBe(true);
});
it("returns false when systemctl reports disabled", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
mockManagedUnitPresent();
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
const err = new Error("disabled") as Error & { code?: number };
err.code = 1;
cb(err, "disabled", "");
});
const result = await isSystemdServiceEnabled({ env: {} });
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
expect(result).toBe(false);
});
it("throws when systemctl is-enabled fails for non-state errors", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
mockManagedUnitPresent();
execFileMock
.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
@@ -119,13 +141,14 @@ describe("isSystemdServiceEnabled", () => {
err.code = 1;
cb(err, "", "permission denied");
});
await expect(isSystemdServiceEnabled({ env: {} })).rejects.toThrow(
"systemctl is-enabled unavailable: permission denied",
);
await expect(
isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } }),
).rejects.toThrow("systemctl is-enabled unavailable: permission denied");
});
it("returns false when systemctl is-enabled exits with code 4 (not-found)", async () => {
const { isSystemdServiceEnabled } = await import("./systemd.js");
mockManagedUnitPresent();
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
// On Ubuntu 24.04, `systemctl --user is-enabled <unit>` exits with
// code 4 and prints "not-found" to stdout when the unit doesn't exist.
@@ -135,7 +158,7 @@ describe("isSystemdServiceEnabled", () => {
err.code = 4;
cb(err, "not-found\n", "");
});
const result = await isSystemdServiceEnabled({ env: {} });
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
expect(result).toBe(false);
});
});

View File

@@ -423,7 +423,16 @@ export async function restartSystemdService({
export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Promise<boolean> {
const env = args.env ?? process.env;
const serviceName = resolveSystemdServiceName(args.env ?? {});
try {
await fs.access(resolveSystemdUnitPath(env));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
}
throw error;
}
const serviceName = resolveSystemdServiceName(env);
const unitName = `${serviceName}.service`;
const res = await execSystemctlUser(env, ["is-enabled", unitName]);
if (res.code === 0) {