mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 19:00:21 +00:00
fix: check managed systemd unit before is-enabled (#38819)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user