refactor(daemon): share systemd service action flow

This commit is contained in:
Peter Steinberger
2026-02-18 18:19:19 +00:00
parent 63403d47d9
commit 9fd810e3a6
2 changed files with 85 additions and 16 deletions

View File

@@ -11,7 +11,9 @@ import { parseSystemdExecStart } from "./systemd-unit.js";
import {
isSystemdUserServiceAvailable,
parseSystemdShow,
restartSystemdService,
resolveSystemdUserUnitPath,
stopSystemdService,
} from "./systemd.js";
describe("systemd availability", () => {
@@ -151,3 +153,58 @@ describe("parseSystemdExecStart", () => {
]);
});
});
describe("systemd service control", () => {
beforeEach(() => {
execFileMock.mockReset();
});
it("stops the resolved user unit", async () => {
execFileMock
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["--user", "stop", "openclaw-gateway.service"]);
cb(null, "", "");
});
const write = vi.fn();
const stdout = { write } as unknown as NodeJS.WritableStream;
await stopSystemdService({ stdout, env: {} });
expect(write).toHaveBeenCalledTimes(1);
expect(String(write.mock.calls[0]?.[0])).toContain("Stopped systemd service");
});
it("restarts a profile-specific user unit", async () => {
execFileMock
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["--user", "restart", "openclaw-gateway-work.service"]);
cb(null, "", "");
});
const write = vi.fn();
const stdout = { write } as unknown as NodeJS.WritableStream;
await restartSystemdService({ stdout, env: { OPENCLAW_PROFILE: "work" } });
expect(write).toHaveBeenCalledTimes(1);
expect(String(write.mock.calls[0]?.[0])).toContain("Restarted systemd service");
});
it("surfaces stop failures with systemctl detail", async () => {
execFileMock
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
.mockImplementationOnce((_cmd, _args, _opts, cb) => {
const err = new Error("stop failed") as Error & { code?: number };
err.code = 1;
cb(err, "", "permission denied");
});
await expect(
stopSystemdService({
stdout: { write: vi.fn() } as unknown as NodeJS.WritableStream,
env: {},
}),
).rejects.toThrow("systemctl stop failed: permission denied");
});
});

View File

@@ -253,6 +253,22 @@ export async function uninstallSystemdService({
}
}
async function runSystemdServiceAction(params: {
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
action: "stop" | "restart";
label: string;
}) {
await assertSystemdAvailable();
const serviceName = resolveSystemdServiceName(params.env ?? {});
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", params.action, unitName]);
if (res.code !== 0) {
throw new Error(`systemctl ${params.action} failed: ${res.stderr || res.stdout}`.trim());
}
params.stdout.write(`${formatLine(params.label, unitName)}\n`);
}
export async function stopSystemdService({
stdout,
env,
@@ -260,14 +276,12 @@ export async function stopSystemdService({
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
}): Promise<void> {
await assertSystemdAvailable();
const serviceName = resolveSystemdServiceName(env ?? {});
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", "stop", unitName]);
if (res.code !== 0) {
throw new Error(`systemctl stop failed: ${res.stderr || res.stdout}`.trim());
}
stdout.write(`${formatLine("Stopped systemd service", unitName)}\n`);
await runSystemdServiceAction({
stdout,
env,
action: "stop",
label: "Stopped systemd service",
});
}
export async function restartSystemdService({
@@ -277,14 +291,12 @@ export async function restartSystemdService({
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
}): Promise<void> {
await assertSystemdAvailable();
const serviceName = resolveSystemdServiceName(env ?? {});
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", "restart", unitName]);
if (res.code !== 0) {
throw new Error(`systemctl restart failed: ${res.stderr || res.stdout}`.trim());
}
stdout.write(`${formatLine("Restarted systemd service", unitName)}\n`);
await runSystemdServiceAction({
stdout,
env,
action: "restart",
label: "Restarted systemd service",
});
}
export async function isSystemdServiceEnabled(args: {