Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
a424103dea fix: scope systemd headless hints to bus failures (#54062) (thanks @chocobo9) 2026-03-24 21:32:06 -07:00
chocobo9
c3ce8c7321 fix(daemon): add headless server hints to systemd unavailable error
Add loginctl enable-linger and XDG_RUNTIME_DIR recovery hints to the
generic (non-WSL) systemd unavailable error path, helping users on
SSH/headless servers diagnose and fix the issue without a desktop
session.

Fixes #11805

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:20:57 -07:00
5 changed files with 99 additions and 4 deletions

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Gateway/ports: parse Docker Compose-style `OPENCLAW_GATEWAY_PORT` host publish values correctly without reviving the legacy `CLAWDBOT_GATEWAY_PORT` override. (#44083) Thanks @bebule.
- 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.
- 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.
- 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.
## 2026.3.23

View File

@@ -219,8 +219,16 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
const systemdUnavailable =
process.platform === "linux" && isSystemdUnavailableDetail(service.runtime?.detail);
if (systemdUnavailable) {
const env = service.command?.environment ?? process.env;
const container = Boolean(
env.OPENCLAW_CONTAINER_HINT?.trim() || env.OPENCLAW_CONTAINER?.trim(),
);
defaultRuntime.error(errorText("systemd user services unavailable."));
for (const hint of renderSystemdUnavailableHints({ wsl: isWSLEnv() })) {
for (const hint of renderSystemdUnavailableHints({
wsl: isWSLEnv(),
detail: service.runtime?.detail,
container,
})) {
defaultRuntime.error(errorText(hint));
}
spacer();

View File

@@ -35,6 +35,7 @@ export function buildGatewayRuntimeHints(
}
const platform = options.platform ?? process.platform;
const env = options.env ?? process.env;
const container = Boolean(env.OPENCLAW_CONTAINER_HINT?.trim() || env.OPENCLAW_CONTAINER?.trim());
const fileLog = (() => {
try {
return getResolvedLoggerSettings().file;
@@ -43,7 +44,13 @@ export function buildGatewayRuntimeHints(
}
})();
if (platform === "linux" && isSystemdUnavailableDetail(runtime.detail)) {
hints.push(...renderSystemdUnavailableHints({ wsl: isWSLEnv() }));
hints.push(
...renderSystemdUnavailableHints({
wsl: isWSLEnv(),
detail: runtime.detail,
container,
}),
);
if (fileLog) {
hints.push(`File logs: ${fileLog}`);
}

View File

@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import { formatCliCommand } from "../cli/command-format.js";
import { isSystemdUnavailableDetail, renderSystemdUnavailableHints } from "./systemd-hints.js";
import {
isSystemdUnavailableDetail,
isSystemdUserBusUnavailableDetail,
renderSystemdUnavailableHints,
} from "./systemd-hints.js";
describe("isSystemdUnavailableDetail", () => {
it("matches systemd unavailable error details", () => {
@@ -17,6 +21,24 @@ describe("isSystemdUnavailableDetail", () => {
});
describe("renderSystemdUnavailableHints", () => {
it("matches systemd user bus/session failures that need headless recovery hints", () => {
expect(
isSystemdUserBusUnavailableDetail(
"systemctl --user unavailable: Failed to connect to bus: No medium found",
),
).toBe(true);
expect(
isSystemdUserBusUnavailableDetail(
"Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined",
),
).toBe(true);
expect(
isSystemdUserBusUnavailableDetail(
"systemctl not available; systemd user services are required on Linux.",
),
).toBe(false);
});
it("renders WSL2-specific recovery hints", () => {
expect(renderSystemdUnavailableHints({ wsl: true })).toEqual([
"WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
@@ -31,4 +53,29 @@ describe("renderSystemdUnavailableHints", () => {
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
]);
});
it("adds headless recovery hints only for user bus/session failures", () => {
expect(
renderSystemdUnavailableHints({
detail: "systemctl --user unavailable: Failed to connect to bus: No medium found",
}),
).toEqual([
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
"On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.",
"Also ensure XDG_RUNTIME_DIR is set: `export XDG_RUNTIME_DIR=/run/user/$(id -u)`, then retry.",
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
]);
});
it("skips headless recovery hints when container context is known", () => {
expect(
renderSystemdUnavailableHints({
detail: "systemctl --user unavailable: Failed to connect to bus: No medium found",
container: true,
}),
).toEqual([
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
]);
});
});

View File

@@ -1,5 +1,11 @@
import { formatCliCommand } from "../cli/command-format.js";
type SystemdUnavailableHintOptions = {
wsl?: boolean;
detail?: string;
container?: boolean;
};
export function isSystemdUnavailableDetail(detail?: string): boolean {
if (!detail) {
return false;
@@ -14,7 +20,30 @@ export function isSystemdUnavailableDetail(detail?: string): boolean {
);
}
export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}): string[] {
export function isSystemdUserBusUnavailableDetail(detail?: string): boolean {
if (!detail) {
return false;
}
const normalized = detail.toLowerCase();
return (
normalized.includes("failed to connect to bus") ||
normalized.includes("failed to connect to user scope bus") ||
normalized.includes("dbus_session_bus_address") ||
normalized.includes("xdg_runtime_dir") ||
normalized.includes("no medium found")
);
}
function renderSystemdHeadlessServerHints(): string[] {
return [
"On a headless server (SSH/no desktop session): run `sudo loginctl enable-linger $(whoami)` to persist your systemd user session across logins.",
"Also ensure XDG_RUNTIME_DIR is set: `export XDG_RUNTIME_DIR=/run/user/$(id -u)`, then retry.",
];
}
export function renderSystemdUnavailableHints(
options: SystemdUnavailableHintOptions = {},
): string[] {
if (options.wsl) {
return [
"WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
@@ -24,6 +53,9 @@ export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}):
}
return [
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
...(options.container || !isSystemdUserBusUnavailableDetail(options.detail)
? []
: renderSystemdHeadlessServerHints()),
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
];
}