mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 13:07:39 +00:00
Daemon: handle degraded systemd status checks (#39325)
* Daemon: handle degraded systemd status checks * Changelog: note systemd status handling * Update src/commands/status.service-summary.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -90,6 +90,14 @@ describe("systemd availability", () => {
|
||||
await expect(isSystemdUserServiceAvailable()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when systemd is degraded but still reachable", async () => {
|
||||
execFileMock.mockImplementation((_cmd, _args, _opts, cb) => {
|
||||
cb(createExecFileError("degraded", { stderr: "degraded\nsome-unit.service failed" }), "", "");
|
||||
});
|
||||
|
||||
await expect(isSystemdUserServiceAvailable()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to machine user scope when --user bus is unavailable", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
@@ -631,6 +639,26 @@ describe("systemd service control", () => {
|
||||
expect(String(write.mock.calls[0]?.[0])).toContain("Stopped systemd service");
|
||||
});
|
||||
|
||||
it("allows stop when systemd status is degraded but available", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, _args, _opts, cb) =>
|
||||
cb(
|
||||
createExecFileError("degraded", { stderr: "degraded\nsome-unit.service failed" }),
|
||||
"",
|
||||
"",
|
||||
),
|
||||
)
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "stop", "openclaw-gateway.service"]);
|
||||
cb(null, "", "");
|
||||
});
|
||||
|
||||
await stopSystemdService({
|
||||
stdout: { write: vi.fn() } as unknown as NodeJS.WritableStream,
|
||||
env: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("restarts a profile-specific user unit", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, "", ""))
|
||||
@@ -658,6 +686,26 @@ describe("systemd service control", () => {
|
||||
).rejects.toThrow("systemctl stop failed: permission denied");
|
||||
});
|
||||
|
||||
it("throws the user-bus error before stop when systemd is unavailable", async () => {
|
||||
vi.spyOn(os, "userInfo").mockImplementationOnce(() => {
|
||||
throw new Error("no user info");
|
||||
});
|
||||
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
|
||||
cb(
|
||||
createExecFileError("Failed to connect to bus", { stderr: "Failed to connect to bus" }),
|
||||
"",
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
await expect(
|
||||
stopSystemdService({
|
||||
stdout: { write: vi.fn() } as unknown as NodeJS.WritableStream,
|
||||
env: { USER: "", LOGNAME: "" },
|
||||
}),
|
||||
).rejects.toThrow("systemctl --user unavailable: Failed to connect to bus");
|
||||
});
|
||||
|
||||
it("targets the sudo caller's user scope when SUDO_USER is set", async () => {
|
||||
execFileMock
|
||||
.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
|
||||
@@ -306,6 +306,19 @@ function isSystemctlBusUnavailable(detail: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isSystemdUserScopeUnavailable(detail: string): boolean {
|
||||
if (!detail) {
|
||||
return false;
|
||||
}
|
||||
const normalized = detail.toLowerCase();
|
||||
return (
|
||||
isSystemctlMissing(normalized) ||
|
||||
isSystemctlBusUnavailable(normalized) ||
|
||||
normalized.includes("not been booted") ||
|
||||
normalized.includes("not supported")
|
||||
);
|
||||
}
|
||||
|
||||
function isGenericSystemctlIsEnabledFailure(detail: string): boolean {
|
||||
if (!detail) {
|
||||
return false;
|
||||
@@ -409,26 +422,11 @@ export async function isSystemdUserServiceAvailable(
|
||||
if (res.code === 0) {
|
||||
return true;
|
||||
}
|
||||
const detail = `${res.stderr} ${res.stdout}`.toLowerCase();
|
||||
const detail = `${res.stderr} ${res.stdout}`.trim();
|
||||
if (!detail) {
|
||||
return false;
|
||||
}
|
||||
if (detail.includes("not found")) {
|
||||
return false;
|
||||
}
|
||||
if (detail.includes("failed to connect")) {
|
||||
return false;
|
||||
}
|
||||
if (detail.includes("not been booted")) {
|
||||
return false;
|
||||
}
|
||||
if (detail.includes("no such file or directory")) {
|
||||
return false;
|
||||
}
|
||||
if (detail.includes("not supported")) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return !isSystemdUserScopeUnavailable(detail);
|
||||
}
|
||||
|
||||
async function assertSystemdAvailable(env: GatewayServiceEnv = process.env as GatewayServiceEnv) {
|
||||
@@ -440,6 +438,12 @@ async function assertSystemdAvailable(env: GatewayServiceEnv = process.env as Ga
|
||||
if (isSystemctlMissing(detail)) {
|
||||
throw new Error("systemctl not available; systemd user services are required on Linux.");
|
||||
}
|
||||
if (!detail) {
|
||||
throw new Error("systemctl --user unavailable: unknown error");
|
||||
}
|
||||
if (!isSystemdUserScopeUnavailable(detail)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`systemctl --user unavailable: ${detail || "unknown error"}`.trim());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user