From 9fd810e3a6da58d3ac819f3962151b482862c6cf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 18:19:19 +0000 Subject: [PATCH] refactor(daemon): share systemd service action flow --- src/daemon/systemd.test.ts | 57 ++++++++++++++++++++++++++++++++++++++ src/daemon/systemd.ts | 44 ++++++++++++++++++----------- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index 5e9e09deed3..5a375e57162 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -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"); + }); +}); diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index 7d91e6b32ca..23e035e1d2a 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -253,6 +253,22 @@ export async function uninstallSystemdService({ } } +async function runSystemdServiceAction(params: { + stdout: NodeJS.WritableStream; + env?: Record; + 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; }): Promise { - 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; }): Promise { - 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: {