fix(daemon): detect system-scope systemd gateway units on Linux

Detect OpenClaw gateway units installed in the system systemd scope, including marker-owned custom unit names such as `openclaw.service`. Route status/restart/stop through the system manager when appropriate, and show non-root users the matching `sudo systemctl ...` command instead of falling back to unmanaged process signaling.

Fixes #87577.
Thanks @yetval.

Verification:
- `node scripts/run-vitest.mjs src/daemon/systemd.test.ts src/cli/daemon-cli/lifecycle.test.ts src/daemon/inspect.test.ts src/cli/daemon-cli/lifecycle-core.test.ts src/cli/daemon-cli/status.gather.test.ts src/cli/daemon-cli/response.test.ts src/commands/doctor-gateway-daemon-flow.test.ts src/cli/update-cli/restart-helper.test.ts src/infra/outbound/message-action-runner.core-send.test.ts`
- AWS Crabbox `cbx_69f97dff5e5c`, run `run_a68431b3dad6`: exact SHA checkout, focused tests, real `/etc/systemd/system/openclaw.service` status/restart/stop proof.
This commit is contained in:
Yuval Dinodia
2026-05-31 19:52:02 +02:00
committed by GitHub
parent e014145ac1
commit b988e2f92b
5 changed files with 508 additions and 32 deletions

View File

@@ -54,6 +54,15 @@ const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(()
const loadConfig = vi.hoisted(() => vi.fn(() => ({})));
const recoverInstalledLaunchAgent = vi.hoisted(() => vi.fn());
const repairLoadedGatewayServiceForStart = vi.hoisted(() => vi.fn());
const findInstalledSystemdGatewayScope = vi.hoisted(() =>
vi.fn<() => Promise<{ scope: "user" | "system"; unitName: string; unitPath: string } | null>>(
async () => null,
),
);
const restartSystemdService = vi.hoisted(() =>
vi.fn<() => Promise<{ outcome: "completed" }>>(async () => ({ outcome: "completed" })),
);
const stopSystemdService = vi.hoisted(() => vi.fn<() => Promise<void>>(async () => {}));
function requireMockCallArg(
mockFn: { mock: { calls: unknown[][] } },
@@ -113,6 +122,12 @@ vi.mock("../../daemon/service.js", () => ({
resolveGatewayService: () => service,
}));
vi.mock("../../daemon/systemd.js", () => ({
findInstalledSystemdGatewayScope: () => findInstalledSystemdGatewayScope(),
restartSystemdService: () => restartSystemdService(),
stopSystemdService: () => stopSystemdService(),
}));
vi.mock("./launchd-recovery.js", () => ({
recoverInstalledLaunchAgent: (args: { result: "started" | "restarted" }) =>
recoverInstalledLaunchAgent(args),
@@ -208,6 +223,12 @@ describe("runDaemonRestart health checks", () => {
service.restart.mockResolvedValue({ outcome: "completed" });
runServiceStart.mockResolvedValue(undefined);
recoverInstalledLaunchAgent.mockResolvedValue(null);
findInstalledSystemdGatewayScope.mockReset();
findInstalledSystemdGatewayScope.mockResolvedValue(null);
restartSystemdService.mockReset();
restartSystemdService.mockResolvedValue({ outcome: "completed" });
stopSystemdService.mockReset();
stopSystemdService.mockResolvedValue(undefined);
runServiceRestart.mockImplementation(async (params: RestartParams) => {
const fail = (message: string, hints?: string[]) => {
@@ -607,6 +628,89 @@ describe("runDaemonRestart health checks", () => {
);
});
it("delegates system-scope restart to systemctl without unmanaged signaling when root (openclaw#87577)", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
findInstalledSystemdGatewayScope.mockResolvedValue({
scope: "system",
unitName: "openclaw.service",
unitPath: "/etc/systemd/system/openclaw.service",
});
restartSystemdService.mockResolvedValue({ outcome: "completed" });
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
mockUnmanagedRestart();
await expect(runDaemonRestart({ json: true })).resolves.toBe(true);
expect(restartSystemdService).toHaveBeenCalled();
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
expect(probeGateway).not.toHaveBeenCalled();
});
it("surfaces systemd sudo guidance and never signals when restarting a system-scope unit as non-root (openclaw#87577)", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
findInstalledSystemdGatewayScope.mockResolvedValue({
scope: "system",
unitName: "openclaw.service",
unitPath: "/etc/systemd/system/openclaw.service",
});
restartSystemdService.mockRejectedValue(
new Error(
"openclaw.service is a system-scope unit (/etc/systemd/system/openclaw.service); run `sudo systemctl restart openclaw.service` to restart it",
),
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
mockUnmanagedRestart();
await expect(runDaemonRestart({ json: true })).rejects.toThrow(
/sudo systemctl restart openclaw\.service/,
);
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
expect(probeGateway).not.toHaveBeenCalled();
});
it("delegates system-scope stop to systemctl without unmanaged signaling when root (openclaw#87577)", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
findInstalledSystemdGatewayScope.mockResolvedValue({
scope: "system",
unitName: "openclaw-gateway.service",
unitPath: "/etc/systemd/system/openclaw-gateway.service",
});
stopSystemdService.mockResolvedValue(undefined);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {
await params.onNotLoaded?.();
});
await expect(runDaemonStop({ json: true })).resolves.toBeUndefined();
expect(stopSystemdService).toHaveBeenCalled();
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
});
it("surfaces systemd sudo guidance and never signals when stopping a system-scope unit as non-root (openclaw#87577)", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
findInstalledSystemdGatewayScope.mockResolvedValue({
scope: "system",
unitName: "openclaw-gateway.service",
unitPath: "/etc/systemd/system/openclaw-gateway.service",
});
stopSystemdService.mockRejectedValue(
new Error(
"openclaw-gateway.service is a system-scope unit (/etc/systemd/system/openclaw-gateway.service); run `sudo systemctl stop openclaw-gateway.service` to stop it",
),
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4200]);
runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {
await params.onNotLoaded?.();
});
await expect(runDaemonStop({ json: true })).rejects.toThrow(
/sudo systemctl stop openclaw-gateway\.service/,
);
expect(stopSystemdService).toHaveBeenCalled();
expect(signalVerifiedGatewayPidSync).not.toHaveBeenCalled();
});
it("skips unmanaged signaling for pids that are not live gateway processes", async () => {
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
runServiceStop.mockImplementation(async (params: { onNotLoaded?: () => Promise<unknown> }) => {

View File

@@ -3,6 +3,11 @@ import { theme } from "../../../packages/terminal-core/src/theme.js";
import { isRestartEnabled } from "../../config/commands.flags.js";
import { readBestEffortConfig, resolveGatewayPort } from "../../config/config.js";
import { resolveGatewayService } from "../../daemon/service.js";
import {
findInstalledSystemdGatewayScope,
restartSystemdService,
stopSystemdService,
} from "../../daemon/systemd.js";
import { callGatewayCli } from "../../gateway/call.js";
import { probeGateway } from "../../gateway/probe.js";
import {
@@ -16,6 +21,7 @@ import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { parseDurationMs } from "../parse-duration.js";
import { recoverInstalledLaunchAgent } from "./launchd-recovery.js";
import { createNullWriter } from "./response.js";
import {
runServiceRestart,
runServiceStart,
@@ -112,7 +118,36 @@ function resolveVerifiedGatewayListenerPids(port: number): number[] {
);
}
async function handleSystemScopeSystemdGateway(
action: "stop" | "restart",
): Promise<{ result: "stopped" | "restarted"; message: string } | null> {
if (process.platform !== "linux") {
return null;
}
const installed = await findInstalledSystemdGatewayScope(process.env).catch(() => null);
if (installed?.scope !== "system") {
return null;
}
const stdout = createNullWriter();
if (action === "stop") {
await stopSystemdService({ stdout, env: process.env });
return {
result: "stopped",
message: `Gateway stopped via system-scope systemd unit ${installed.unitName}.`,
};
}
await restartSystemdService({ stdout, env: process.env });
return {
result: "restarted",
message: `Gateway restarted via system-scope systemd unit ${installed.unitName}.`,
};
}
async function stopGatewayWithoutServiceManager(port: number) {
const managed = await handleSystemScopeSystemdGateway("stop");
if (managed) {
return managed;
}
const pids = resolveVerifiedGatewayListenerPids(port);
if (pids.length === 0) {
return null;
@@ -196,6 +231,10 @@ async function restartGatewayWithoutServiceManager(
port: number,
restartIntent?: GatewayRestartIntent,
) {
const managed = await handleSystemScopeSystemdGateway("restart");
if (managed) {
return managed;
}
await assertUnmanagedGatewayRestartEnabled(port);
const pids = resolveVerifiedGatewayListenerPids(port);
if (pids.length === 0) {

View File

@@ -90,7 +90,7 @@ export function buildDaemonServiceSnapshot(service: GatewayService, loaded: bool
};
}
function createNullWriter(): Writable {
export function createNullWriter(): Writable {
return new Writable({
write(_chunk, _encoding, callback) {
callback();

View File

@@ -1,14 +1,10 @@
import type { ExecFileException, ExecFileOptionsWithStringEncoding } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { ExecFileException, ExecFileOptionsWithStringEncoding } from "node:child_process";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
type ExecFileCallback = (
error: ExecFileException | null,
stdout: string,
stderr: string,
) => void;
type ExecFileCallback = (error: ExecFileException | null, stdout: string, stderr: string) => void;
type ExecFileMock = (
command: string,
args: string[],
@@ -18,6 +14,24 @@ type ExecFileMock = (
const execFileMock = vi.hoisted(() => vi.fn<ExecFileMock>());
const existsSyncMock = vi.hoisted(() => vi.fn(() => false));
const findSystemGatewayServicesMock = vi.hoisted(() =>
vi.fn<
() => Promise<
Array<{
platform: "linux";
label: string;
detail: string;
scope: "user" | "system";
marker?: "openclaw" | "clawdbot";
legacy?: boolean;
}>
>
>(async () => []),
);
vi.mock("./inspect.js", () => ({
findSystemGatewayServices: () => findSystemGatewayServicesMock(),
}));
vi.mock("node:fs", async (importOriginal) => ({
...(await importOriginal<typeof import("node:fs")>()),
@@ -58,6 +72,7 @@ vi.mock("./exec-file.js", () => {
import { splitArgsPreservingQuotes } from "./arg-split.js";
import { parseSystemdEnvAssignments, parseSystemdExecStart } from "./systemd-unit.js";
import {
findInstalledSystemdGatewayScope,
installSystemdService,
isNonFatalSystemdInstallProbeError,
isSystemdServiceEnabled,
@@ -512,6 +527,219 @@ describe("isSystemdUnitActive", () => {
});
});
describe("system-scope gateway unit detection (openclaw#87577)", () => {
beforeEach(() => {
vi.restoreAllMocks();
execFileMock.mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
});
function mockUnitFileLayout(layout: { user?: boolean; system?: string | false }) {
vi.spyOn(fs, "access").mockImplementation(async (pathArg) => {
const p = pathLikeToString(pathArg);
if (layout.user && p.includes("/.config/systemd/user/")) {
return undefined;
}
if (typeof layout.system === "string" && p === layout.system) {
return undefined;
}
const err = new Error("ENOENT") as NodeJS.ErrnoException;
err.code = "ENOENT";
throw err;
});
}
it("findInstalledSystemdGatewayScope prefers user scope when both exist", async () => {
mockUnitFileLayout({
user: true,
system: "/etc/systemd/system/openclaw-gateway.service",
});
const result = await findInstalledSystemdGatewayScope({ HOME: TEST_MANAGED_HOME });
expect(result?.scope).toBe("user");
expect(result?.unitName).toBe(GATEWAY_SERVICE);
expect(result?.unitPath).toContain("/.config/systemd/user/openclaw-gateway.service");
});
it("findInstalledSystemdGatewayScope detects system-scope unit in /etc/systemd/system", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
const result = await findInstalledSystemdGatewayScope({ HOME: TEST_MANAGED_HOME });
expect(result).toEqual({
scope: "system",
unitName: GATEWAY_SERVICE,
unitPath: "/etc/systemd/system/openclaw-gateway.service",
});
});
it("findInstalledSystemdGatewayScope falls back to /usr/lib/systemd/system", async () => {
mockUnitFileLayout({ system: "/usr/lib/systemd/system/openclaw-gateway.service" });
const result = await findInstalledSystemdGatewayScope({ HOME: TEST_MANAGED_HOME });
expect(result?.scope).toBe("system");
expect(result?.unitPath).toBe("/usr/lib/systemd/system/openclaw-gateway.service");
});
it("findInstalledSystemdGatewayScope returns null when no unit file exists", async () => {
mockUnitFileLayout({ system: false });
findSystemGatewayServicesMock.mockResolvedValueOnce([]);
const result = await findInstalledSystemdGatewayScope({ HOME: TEST_MANAGED_HOME });
expect(result).toBeNull();
});
it("findInstalledSystemdGatewayScope falls back to marker-owned system unit with custom name", async () => {
mockUnitFileLayout({ system: false });
findSystemGatewayServicesMock.mockResolvedValueOnce([
{
platform: "linux",
label: "openclaw.service",
detail: "unit: /etc/systemd/system/openclaw.service",
scope: "system",
marker: "openclaw",
},
]);
const result = await findInstalledSystemdGatewayScope({ HOME: TEST_MANAGED_HOME });
expect(result).toEqual({
scope: "system",
unitName: "openclaw.service",
unitPath: "/etc/systemd/system/openclaw.service",
});
});
it("findInstalledSystemdGatewayScope ignores legacy clawdbot system units in the marker fallback", async () => {
mockUnitFileLayout({ system: false });
findSystemGatewayServicesMock.mockResolvedValueOnce([
{
platform: "linux",
label: "clawdbot.service",
detail: "unit: /etc/systemd/system/clawdbot.service",
scope: "system",
marker: "clawdbot",
legacy: true,
},
]);
const result = await findInstalledSystemdGatewayScope({ HOME: TEST_MANAGED_HOME });
expect(result).toBeNull();
});
it("isSystemdServiceEnabled queries the marker-owned custom system unit name", async () => {
mockUnitFileLayout({ system: false });
findSystemGatewayServicesMock.mockResolvedValueOnce([
{
platform: "linux",
label: "openclaw.service",
detail: "unit: /etc/systemd/system/openclaw.service",
scope: "system",
marker: "openclaw",
},
]);
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["is-enabled", "openclaw.service"]);
cb(null, "enabled\n", "");
});
await expect(isSystemdServiceEnabled({ env: { HOME: TEST_MANAGED_HOME } })).resolves.toBe(true);
});
it("restartSystemdService surfaces sudo guidance using the marker-owned custom unit name", async () => {
mockUnitFileLayout({ system: false });
findSystemGatewayServicesMock.mockResolvedValueOnce([
{
platform: "linux",
label: "openclaw.service",
detail: "unit: /etc/systemd/system/openclaw.service",
scope: "system",
marker: "openclaw",
},
]);
mockEffectiveUid(1000);
const { stdout, write } = createWritableStreamMock();
await expect(
restartSystemdService({ stdout, env: { HOME: TEST_MANAGED_HOME } }),
).rejects.toThrow(
/openclaw\.service is a system-scope unit \(\/etc\/systemd\/system\/openclaw\.service\); run `sudo systemctl restart openclaw\.service`/,
);
expect(execFileMock).not.toHaveBeenCalled();
expect(write).not.toHaveBeenCalled();
});
it("isSystemdServiceEnabled reports true for an enabled system-scope unit", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["is-enabled", GATEWAY_SERVICE]);
cb(null, "enabled\n", "");
});
await expect(isSystemdServiceEnabled({ env: { HOME: TEST_MANAGED_HOME } })).resolves.toBe(true);
});
it("isSystemdServiceEnabled reports false for a disabled system-scope unit", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["is-enabled", GATEWAY_SERVICE]);
cb(createExecFileError("disabled", { code: 1 }), "disabled\n", "");
});
await expect(isSystemdServiceEnabled({ env: { HOME: TEST_MANAGED_HOME } })).resolves.toBe(
false,
);
});
it("readSystemdServiceRuntime queries the system manager for system-scope units", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args[0]).toBe("show");
expect(args).not.toContain("--user");
cb(
null,
[
"Id=openclaw-gateway.service",
"ActiveState=active",
"SubState=running",
"MainPID=4242",
].join("\n"),
"",
);
});
const runtime = await readSystemdServiceRuntime({ HOME: TEST_MANAGED_HOME });
expect(runtime.status).toBe("running");
expect(runtime.pid).toBe(4242);
expect(runtime.systemd?.unit).toBe("openclaw-gateway.service");
});
it("restartSystemdService refuses to use the user manager when the unit is system-scope and the caller is not root", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
mockEffectiveUid(1000);
const { stdout, write } = createWritableStreamMock();
await expect(
restartSystemdService({ stdout, env: { HOME: TEST_MANAGED_HOME } }),
).rejects.toThrow(
/system-scope unit .* run `sudo systemctl restart openclaw-gateway\.service`/,
);
expect(execFileMock).not.toHaveBeenCalled();
expect(write).not.toHaveBeenCalled();
});
it("restartSystemdService restarts the system unit directly when running as root", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
mockEffectiveUid(0);
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["restart", GATEWAY_SERVICE]);
cb(null, "", "");
});
const { stdout, write } = createWritableStreamMock();
const result = await restartSystemdService({ stdout, env: { HOME: TEST_MANAGED_HOME } });
expect(result).toEqual({ outcome: "completed" });
expect(requireFirstWrite(write)).toContain("Restarted systemd service");
});
it("stopSystemdService surfaces sudo guidance for system-scope units without root", async () => {
mockUnitFileLayout({ system: "/etc/systemd/system/openclaw-gateway.service" });
mockEffectiveUid(1000);
const { stdout } = createWritableStreamMock();
await expect(stopSystemdService({ stdout, env: { HOME: TEST_MANAGED_HOME } })).rejects.toThrow(
/sudo systemctl stop openclaw-gateway\.service/,
);
expect(execFileMock).not.toHaveBeenCalled();
});
});
describe("isNonFatalSystemdInstallProbeError", () => {
it("matches wrapper-only WSL install probe failures", () => {
expect(

View File

@@ -78,6 +78,85 @@ export function resolveSystemdUserUnitPath(env: GatewayServiceEnv): string {
return resolveSystemdUnitPath(env);
}
const SYSTEM_SYSTEMD_UNIT_DIRS = [
"/etc/systemd/system",
"/usr/lib/systemd/system",
"/lib/systemd/system",
] as const;
async function findSystemSystemdUnitPath(env: GatewayServiceEnv): Promise<string | null> {
const serviceFile = `${resolveSystemdServiceName(env)}.service`;
for (const dir of SYSTEM_SYSTEMD_UNIT_DIRS) {
const candidate = path.posix.join(dir, serviceFile);
try {
await fs.access(candidate);
return candidate;
} catch {
continue;
}
}
return null;
}
export type InstalledSystemdGatewayScope = {
scope: SystemdUnitScope;
unitName: string;
unitPath: string;
};
async function findMarkerOwnedSystemSystemdUnit(): Promise<{
unitName: string;
unitPath: string;
} | null> {
const { findSystemGatewayServices } = await import("./inspect.js");
let services: Awaited<ReturnType<typeof findSystemGatewayServices>>;
try {
services = await findSystemGatewayServices();
} catch {
return null;
}
for (const svc of services) {
if (
svc.platform !== "linux" ||
svc.scope !== "system" ||
svc.marker !== "openclaw" ||
!svc.label?.endsWith(".service")
) {
continue;
}
const match = /^unit:\s*(.+)$/.exec(svc.detail.trim());
const unitPath = match?.[1]?.trim();
if (unitPath) {
return { unitName: svc.label, unitPath };
}
}
return null;
}
export async function findInstalledSystemdGatewayScope(
env: GatewayServiceEnv,
): Promise<InstalledSystemdGatewayScope | null> {
const canonicalUnitName = `${resolveSystemdServiceName(env)}.service`;
let userPath: string | null = null;
try {
userPath = resolveSystemdUnitPath(env);
} catch {
userPath = null;
}
if (userPath) {
try {
await fs.access(userPath);
return { scope: "user", unitName: canonicalUnitName, unitPath: userPath };
} catch {}
}
const systemPath = await findSystemSystemdUnitPath(env);
if (systemPath) {
return { scope: "system", unitName: canonicalUnitName, unitPath: systemPath };
}
const owned = await findMarkerOwnedSystemSystemdUnit();
return owned ? { scope: "system", unitName: owned.unitName, unitPath: owned.unitPath } : null;
}
export { enableSystemdUserLinger, readSystemdUserLingerStatus };
// Unit file parsing/rendering: see systemd-unit.ts
@@ -1037,6 +1116,17 @@ export async function uninstallSystemdService({
}
}
function isRunningAsRoot(): boolean {
if (typeof process.geteuid === "function") {
try {
return process.geteuid() === 0;
} catch {
return false;
}
}
return false;
}
async function runSystemdServiceAction(params: {
stdout: NodeJS.WritableStream;
env?: GatewayServiceEnv;
@@ -1044,9 +1134,22 @@ async function runSystemdServiceAction(params: {
label: string;
}) {
const env = params.env ?? process.env;
const installed = await findInstalledSystemdGatewayScope(env);
const unitName = installed?.unitName ?? `${resolveSystemdServiceName(env)}.service`;
if (installed?.scope === "system") {
if (!isRunningAsRoot()) {
throw new Error(
`${unitName} is a system-scope unit (${installed.unitPath}); run \`sudo systemctl ${params.action} ${unitName}\` to ${params.action} it`,
);
}
const res = await execSystemctl([params.action, unitName], env);
if (res.code !== 0) {
throw new Error(`systemctl ${params.action} failed: ${res.stderr || res.stdout}`.trim());
}
params.stdout.write(`${formatLine(params.label, unitName)}\n`);
return;
}
await assertSystemdAvailable(env);
const serviceName = resolveSystemdServiceName(env);
const unitName = `${serviceName}.service`;
const res = await execSystemctlUser(env, [params.action, unitName]);
if (res.code !== 0) {
throw new Error(`systemctl ${params.action} failed: ${res.stderr || res.stdout}`.trim());
@@ -1081,18 +1184,14 @@ export async function restartSystemdService({
export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Promise<boolean> {
const env = args.env ?? process.env;
try {
await fs.access(resolveSystemdUnitPath(env));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
}
throw error;
const installed = await findInstalledSystemdGatewayScope(env);
if (!installed) {
return false;
}
const serviceName = resolveSystemdServiceName(env);
const unitName = `${serviceName}.service`;
const res = await execSystemctlUser(env, ["is-enabled", unitName]);
const res =
installed.scope === "system"
? await execSystemctl(["is-enabled", installed.unitName], env)
: await execSystemctlUser(env, ["is-enabled", installed.unitName]);
if (res.code === 0) {
return true;
}
@@ -1106,23 +1205,29 @@ export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Prom
export async function readSystemdServiceRuntime(
env: GatewayServiceEnv = process.env as GatewayServiceEnv,
): Promise<GatewayServiceRuntime> {
try {
await assertSystemdAvailable(env);
} catch (err) {
return {
status: "unknown",
detail: formatErrorMessage(err),
};
const installed = await findInstalledSystemdGatewayScope(env).catch(() => null);
if (installed?.scope !== "system") {
try {
await assertSystemdAvailable(env);
} catch (err) {
return {
status: "unknown",
detail: formatErrorMessage(err),
};
}
}
const serviceName = resolveSystemdServiceName(env);
const unitName = `${serviceName}.service`;
const res = await execSystemctlUser(env, [
const unitName = installed?.unitName ?? `${resolveSystemdServiceName(env)}.service`;
const showArgs = [
"show",
unitName,
"--no-page",
"--property",
"Id,ActiveState,SubState,MainPID,ExecMainStatus,ExecMainCode,KillMode,TasksCurrent,MemoryCurrent",
]);
];
const res =
installed?.scope === "system"
? await execSystemctl(showArgs, env)
: await execSystemctlUser(env, showArgs);
if (res.code !== 0) {
const detail = (res.stderr || res.stdout).trim();
const missing = normalizeLowercaseStringOrEmpty(detail).includes("not found");