Skip to content

Commit a424103

Browse files
committed
fix: scope systemd headless hints to bus failures (#54062) (thanks @chocobo9)
1 parent c3ce8c7 commit a424103

File tree

5 files changed

+98
-7
lines changed

5 files changed

+98
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
4444
- Gateway/ports: parse Docker Compose-style `OPENCLAW_GATEWAY_PORT` host publish values correctly without reviving the legacy `CLAWDBOT_GATEWAY_PORT` override. (#44083) Thanks @bebule.
4545
- Feishu/MSTeams message tool: keep provider-native `card` payloads optional in merged tool schemas so media-only sends stop failing validation before channel runtime dispatch. (#53715) Thanks @lndyzwdxhs.
4646
- Feishu/startup: keep `requireMention` enforcement strict when bot identity startup probes fail, raise the startup bot-info timeout to 30s, and add cancellable background identity recovery so mention-gated groups recover without noisy fallback. (#43788) Thanks @lefarcen.
47+
- Gateway/systemd hints: keep generic Linux systemd-unavailable guidance broad, and only show the headless-server `loginctl enable-linger` / `XDG_RUNTIME_DIR` recovery steps when the runtime detail proves a user-bus/session failure. (#54062) Thanks @chocobo9.
4748

4849
## 2026.3.23
4950

src/cli/daemon-cli/status.print.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,16 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
219219
const systemdUnavailable =
220220
process.platform === "linux" && isSystemdUnavailableDetail(service.runtime?.detail);
221221
if (systemdUnavailable) {
222+
const env = service.command?.environment ?? process.env;
223+
const container = Boolean(
224+
env.OPENCLAW_CONTAINER_HINT?.trim() || env.OPENCLAW_CONTAINER?.trim(),
225+
);
222226
defaultRuntime.error(errorText("systemd user services unavailable."));
223-
for (const hint of renderSystemdUnavailableHints({ wsl: isWSLEnv() })) {
227+
for (const hint of renderSystemdUnavailableHints({
228+
wsl: isWSLEnv(),
229+
detail: service.runtime?.detail,
230+
container,
231+
})) {
224232
defaultRuntime.error(errorText(hint));
225233
}
226234
spacer();

src/commands/doctor-format.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function buildGatewayRuntimeHints(
3535
}
3636
const platform = options.platform ?? process.platform;
3737
const env = options.env ?? process.env;
38+
const container = Boolean(env.OPENCLAW_CONTAINER_HINT?.trim() || env.OPENCLAW_CONTAINER?.trim());
3839
const fileLog = (() => {
3940
try {
4041
return getResolvedLoggerSettings().file;
@@ -43,7 +44,13 @@ export function buildGatewayRuntimeHints(
4344
}
4445
})();
4546
if (platform === "linux" && isSystemdUnavailableDetail(runtime.detail)) {
46-
hints.push(...renderSystemdUnavailableHints({ wsl: isWSLEnv() }));
47+
hints.push(
48+
...renderSystemdUnavailableHints({
49+
wsl: isWSLEnv(),
50+
detail: runtime.detail,
51+
container,
52+
}),
53+
);
4754
if (fileLog) {
4855
hints.push(`File logs: ${fileLog}`);
4956
}

src/daemon/systemd-hints.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, expect, it } from "vitest";
22
import { formatCliCommand } from "../cli/command-format.js";
3-
import { isSystemdUnavailableDetail, renderSystemdUnavailableHints } from "./systemd-hints.js";
3+
import {
4+
isSystemdUnavailableDetail,
5+
isSystemdUserBusUnavailableDetail,
6+
renderSystemdUnavailableHints,
7+
} from "./systemd-hints.js";
48

59
describe("isSystemdUnavailableDetail", () => {
610
it("matches systemd unavailable error details", () => {
@@ -17,6 +21,24 @@ describe("isSystemdUnavailableDetail", () => {
1721
});
1822

1923
describe("renderSystemdUnavailableHints", () => {
24+
it("matches systemd user bus/session failures that need headless recovery hints", () => {
25+
expect(
26+
isSystemdUserBusUnavailableDetail(
27+
"systemctl --user unavailable: Failed to connect to bus: No medium found",
28+
),
29+
).toBe(true);
30+
expect(
31+
isSystemdUserBusUnavailableDetail(
32+
"Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined",
33+
),
34+
).toBe(true);
35+
expect(
36+
isSystemdUserBusUnavailableDetail(
37+
"systemctl not available; systemd user services are required on Linux.",
38+
),
39+
).toBe(false);
40+
});
41+
2042
it("renders WSL2-specific recovery hints", () => {
2143
expect(renderSystemdUnavailableHints({ wsl: true })).toEqual([
2244
"WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
@@ -28,9 +50,32 @@ describe("renderSystemdUnavailableHints", () => {
2850
it("renders generic Linux recovery hints outside WSL", () => {
2951
expect(renderSystemdUnavailableHints()).toEqual([
3052
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
31-
"On a headless server (SSH/no desktop session): run `loginctl enable-linger` to persist your systemd user session across logins.",
53+
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
54+
]);
55+
});
56+
57+
it("adds headless recovery hints only for user bus/session failures", () => {
58+
expect(
59+
renderSystemdUnavailableHints({
60+
detail: "systemctl --user unavailable: Failed to connect to bus: No medium found",
61+
}),
62+
).toEqual([
63+
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
64+
"On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.",
3265
"Also ensure XDG_RUNTIME_DIR is set: `export XDG_RUNTIME_DIR=/run/user/$(id -u)`, then retry.",
3366
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
3467
]);
3568
});
69+
70+
it("skips headless recovery hints when container context is known", () => {
71+
expect(
72+
renderSystemdUnavailableHints({
73+
detail: "systemctl --user unavailable: Failed to connect to bus: No medium found",
74+
container: true,
75+
}),
76+
).toEqual([
77+
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
78+
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
79+
]);
80+
});
3681
});

src/daemon/systemd-hints.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { formatCliCommand } from "../cli/command-format.js";
22

3+
type SystemdUnavailableHintOptions = {
4+
wsl?: boolean;
5+
detail?: string;
6+
container?: boolean;
7+
};
8+
39
export function isSystemdUnavailableDetail(detail?: string): boolean {
410
if (!detail) {
511
return false;
@@ -14,7 +20,30 @@ export function isSystemdUnavailableDetail(detail?: string): boolean {
1420
);
1521
}
1622

17-
export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}): string[] {
23+
export function isSystemdUserBusUnavailableDetail(detail?: string): boolean {
24+
if (!detail) {
25+
return false;
26+
}
27+
const normalized = detail.toLowerCase();
28+
return (
29+
normalized.includes("failed to connect to bus") ||
30+
normalized.includes("failed to connect to user scope bus") ||
31+
normalized.includes("dbus_session_bus_address") ||
32+
normalized.includes("xdg_runtime_dir") ||
33+
normalized.includes("no medium found")
34+
);
35+
}
36+
37+
function renderSystemdHeadlessServerHints(): string[] {
38+
return [
39+
"On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.",
40+
"Also ensure XDG_RUNTIME_DIR is set: `export XDG_RUNTIME_DIR=/run/user/$(id -u)`, then retry.",
41+
];
42+
}
43+
44+
export function renderSystemdUnavailableHints(
45+
options: SystemdUnavailableHintOptions = {},
46+
): string[] {
1847
if (options.wsl) {
1948
return [
2049
"WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
@@ -24,8 +53,9 @@ export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}):
2453
}
2554
return [
2655
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
27-
"On a headless server (SSH/no desktop session): run `loginctl enable-linger` to persist your systemd user session across logins.",
28-
"Also ensure XDG_RUNTIME_DIR is set: `export XDG_RUNTIME_DIR=/run/user/$(id -u)`, then retry.",
56+
...(options.container || !isSystemdUserBusUnavailableDetail(options.detail)
57+
? []
58+
: renderSystemdHeadlessServerHints()),
2959
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
3060
];
3161
}

0 commit comments

Comments
 (0)