Skip to content

Commit 26c9796

Browse files
committed
fix: check managed systemd unit before is-enabled (#38819)
1 parent addd290 commit 26c9796

File tree

3 files changed

+41
-8
lines changed

3 files changed

+41
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ Docs: https://docs.openclaw.ai
228228
- 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.
229229
- 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.
230230
- 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.
231+
- 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.
231232

232233
## 2026.3.2
233234

src/daemon/systemd.test.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from "node:fs/promises";
12
import { beforeEach, describe, expect, it, vi } from "vitest";
23

34
const execFileMock = vi.hoisted(() => vi.fn());
@@ -66,44 +67,65 @@ describe("systemd availability", () => {
6667
});
6768

6869
describe("isSystemdServiceEnabled", () => {
70+
const mockManagedUnitPresent = () => {
71+
vi.spyOn(fs, "access").mockResolvedValue(undefined);
72+
};
73+
6974
beforeEach(() => {
75+
vi.restoreAllMocks();
7076
execFileMock.mockReset();
7177
});
7278

7379
it("returns false when systemctl is not present", async () => {
7480
const { isSystemdServiceEnabled } = await import("./systemd.js");
81+
mockManagedUnitPresent();
7582
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
7683
const err = new Error("spawn systemctl EACCES") as Error & { code?: string };
7784
err.code = "EACCES";
7885
cb(err, "", "");
7986
});
80-
const result = await isSystemdServiceEnabled({ env: {} });
87+
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
88+
expect(result).toBe(false);
89+
});
90+
91+
it("returns false without calling systemctl when the managed unit file is missing", async () => {
92+
const { isSystemdServiceEnabled } = await import("./systemd.js");
93+
const err = new Error("missing unit") as NodeJS.ErrnoException;
94+
err.code = "ENOENT";
95+
vi.spyOn(fs, "access").mockRejectedValueOnce(err);
96+
97+
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
98+
8199
expect(result).toBe(false);
100+
expect(execFileMock).not.toHaveBeenCalled();
82101
});
83102

84103
it("calls systemctl is-enabled when systemctl is present", async () => {
85104
const { isSystemdServiceEnabled } = await import("./systemd.js");
105+
mockManagedUnitPresent();
86106
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
87107
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
88108
cb(null, "enabled", "");
89109
});
90-
const result = await isSystemdServiceEnabled({ env: {} });
110+
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
91111
expect(result).toBe(true);
92112
});
93113

94114
it("returns false when systemctl reports disabled", async () => {
95115
const { isSystemdServiceEnabled } = await import("./systemd.js");
116+
mockManagedUnitPresent();
96117
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
97118
const err = new Error("disabled") as Error & { code?: number };
98119
err.code = 1;
99120
cb(err, "disabled", "");
100121
});
101-
const result = await isSystemdServiceEnabled({ env: {} });
122+
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
102123
expect(result).toBe(false);
103124
});
104125

105126
it("throws when systemctl is-enabled fails for non-state errors", async () => {
106127
const { isSystemdServiceEnabled } = await import("./systemd.js");
128+
mockManagedUnitPresent();
107129
execFileMock
108130
.mockImplementationOnce((_cmd, args, _opts, cb) => {
109131
expect(args).toEqual(["--user", "is-enabled", "openclaw-gateway.service"]);
@@ -119,13 +141,14 @@ describe("isSystemdServiceEnabled", () => {
119141
err.code = 1;
120142
cb(err, "", "permission denied");
121143
});
122-
await expect(isSystemdServiceEnabled({ env: {} })).rejects.toThrow(
123-
"systemctl is-enabled unavailable: permission denied",
124-
);
144+
await expect(
145+
isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } }),
146+
).rejects.toThrow("systemctl is-enabled unavailable: permission denied");
125147
});
126148

127149
it("returns false when systemctl is-enabled exits with code 4 (not-found)", async () => {
128150
const { isSystemdServiceEnabled } = await import("./systemd.js");
151+
mockManagedUnitPresent();
129152
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
130153
// On Ubuntu 24.04, `systemctl --user is-enabled <unit>` exits with
131154
// code 4 and prints "not-found" to stdout when the unit doesn't exist.
@@ -135,7 +158,7 @@ describe("isSystemdServiceEnabled", () => {
135158
err.code = 4;
136159
cb(err, "not-found\n", "");
137160
});
138-
const result = await isSystemdServiceEnabled({ env: {} });
161+
const result = await isSystemdServiceEnabled({ env: { HOME: "/tmp/openclaw-test-home" } });
139162
expect(result).toBe(false);
140163
});
141164
});

src/daemon/systemd.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,16 @@ export async function restartSystemdService({
423423

424424
export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Promise<boolean> {
425425
const env = args.env ?? process.env;
426-
const serviceName = resolveSystemdServiceName(args.env ?? {});
426+
try {
427+
await fs.access(resolveSystemdUnitPath(env));
428+
} catch (error) {
429+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
430+
return false;
431+
}
432+
throw error;
433+
}
434+
435+
const serviceName = resolveSystemdServiceName(env);
427436
const unitName = `${serviceName}.service`;
428437
const res = await execSystemctlUser(env, ["is-enabled", unitName]);
429438
if (res.code === 0) {

0 commit comments

Comments
 (0)