fix: improve WSL2 systemd daemon hints

This commit is contained in:
Peter Steinberger
2026-01-17 18:19:47 +00:00
parent 408f4f2dac
commit 8a67d29748
10 changed files with 110 additions and 36 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot
### Fixes
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.
## 2026.1.16-2

View File

@@ -1,5 +1,8 @@
import { resolveIsNixMode } from "../../config/paths.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
import { isWSL } from "../../infra/wsl.js";
import { defaultRuntime } from "../../runtime.js";
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
import { renderGatewayServiceStartHints } from "./shared.js";
@@ -89,7 +92,13 @@ export async function runDaemonStart(opts: DaemonLifecycleOptions = {}) {
return;
}
if (!loaded) {
const hints = renderGatewayServiceStartHints();
let hints = renderGatewayServiceStartHints();
if (process.platform === "linux") {
const systemdAvailable = await isSystemdUserServiceAvailable().catch(() => false);
if (!systemdAvailable) {
hints = [...hints, ...renderSystemdUnavailableHints({ wsl: await isWSL() })];
}
}
emit({
ok: true,
result: "not-loaded",
@@ -229,7 +238,13 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
return false;
}
if (!loaded) {
const hints = renderGatewayServiceStartHints();
let hints = renderGatewayServiceStartHints();
if (process.platform === "linux") {
const systemdAvailable = await isSystemdUserServiceAvailable().catch(() => false);
if (!systemdAvailable) {
hints = [...hints, ...renderSystemdUnavailableHints({ wsl: await isWSL() })];
}
}
emit({
ok: true,
result: "not-loaded",

View File

@@ -114,7 +114,9 @@ export async function gatherDaemonStatus(
const [loaded, command, runtime] = await Promise.all([
service.isLoaded({ env: process.env }).catch(() => false),
service.readCommand(process.env).catch(() => null),
service.readRuntime(process.env).catch(() => undefined),
service
.readRuntime(process.env)
.catch((err) => ({ status: "unknown", detail: String(err) })),
]);
const configAudit = await auditGatewayServiceConfig({
env: process.env,

View File

@@ -5,6 +5,11 @@ import {
} from "../../daemon/constants.js";
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
import {
isSystemdUnavailableDetail,
renderSystemdUnavailableHints,
} from "../../daemon/systemd-hints.js";
import { isWSLEnv } from "../../infra/wsl.js";
import { getResolvedLoggerSettings } from "../../logging.js";
import { defaultRuntime } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
@@ -164,6 +169,16 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
spacer();
}
const systemdUnavailable =
process.platform === "linux" && isSystemdUnavailableDetail(service.runtime?.detail);
if (systemdUnavailable) {
defaultRuntime.error(errorText("systemd user services unavailable."));
for (const hint of renderSystemdUnavailableHints({ wsl: isWSLEnv() })) {
defaultRuntime.error(errorText(hint));
}
spacer();
}
if (service.runtime?.missingUnit) {
defaultRuntime.error(errorText("Service unit not found."));
for (const hint of renderRuntimeHints(service.runtime)) {

View File

@@ -4,6 +4,11 @@ import {
resolveGatewayWindowsTaskName,
} from "../daemon/constants.js";
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
import {
isSystemdUnavailableDetail,
renderSystemdUnavailableHints,
} from "../daemon/systemd-hints.js";
import { isWSLEnv } from "../infra/wsl.js";
import type { GatewayServiceRuntime } from "../daemon/service-runtime.js";
import { getResolvedLoggerSettings } from "../logging.js";
@@ -54,6 +59,11 @@ export function buildGatewayRuntimeHints(
return null;
}
})();
if (platform === "linux" && isSystemdUnavailableDetail(runtime.detail)) {
hints.push(...renderSystemdUnavailableHints({ wsl: isWSLEnv() }));
if (fileLog) hints.push(`File logs: ${fileLog}`);
return hints;
}
if (runtime.cachedLabel && platform === "darwin") {
const label = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
hints.push(

View File

@@ -12,7 +12,10 @@ import {
} from "../daemon/runtime-paths.js";
import { resolveGatewayService } from "../daemon/service.js";
import { buildServiceEnvironment } from "../daemon/service-env.js";
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
import { isWSL } from "../infra/wsl.js";
import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js";
import { sleep } from "../utils.js";
@@ -55,6 +58,14 @@ export async function maybeRepairGatewayDaemon(params: {
}
if (!loaded) {
if (process.platform === "linux") {
const systemdAvailable = await isSystemdUserServiceAvailable().catch(() => false);
if (!systemdAvailable) {
const wsl = await isWSL();
note(renderSystemdUnavailableHints({ wsl }).join("\n"), "Gateway");
return;
}
}
note("Gateway daemon not installed.", "Gateway");
if (params.cfg.gateway?.mode !== "remote") {
const install = await params.prompter.confirmSkipInNonInteractive({

View File

@@ -1,14 +1,4 @@
import { readFileSync } from "node:fs";
function isWSL(): boolean {
if (process.platform !== "linux") return false;
try {
const release = readFileSync("/proc/version", "utf8").toLowerCase();
return release.includes("microsoft") || release.includes("wsl");
} catch {
return false;
}
}
import { isWSLEnv } from "../infra/wsl.js";
export function isRemoteEnvironment(): boolean {
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
@@ -23,7 +13,7 @@ export function isRemoteEnvironment(): boolean {
process.platform === "linux" &&
!process.env.DISPLAY &&
!process.env.WAYLAND_DISPLAY &&
!isWSL()
!isWSLEnv()
) {
return true;
}

View File

@@ -13,6 +13,7 @@ import { callGateway } from "../gateway/call.js";
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
import { isSafeExecutableValue } from "../infra/exec-safety.js";
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { isWSL } from "../infra/wsl.js";
import { runCommandWithTimeout } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { stylePromptTitle } from "../terminal/prompt-style.js";
@@ -91,27 +92,6 @@ type BrowserOpenSupport = {
command?: string;
};
let wslCached: boolean | null = null;
async function isWSL(): Promise<boolean> {
if (wslCached !== null) return wslCached;
if (process.platform !== "linux") {
wslCached = false;
return wslCached;
}
if (process.env.WSL_INTEROP || process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
wslCached = true;
return wslCached;
}
try {
const release = (await fs.readFile("/proc/version", "utf8")).toLowerCase();
wslCached = release.includes("microsoft") || release.includes("wsl");
} catch {
wslCached = false;
}
return wslCached;
}
type BrowserOpenCommand = {
argv: string[] | null;
reason?: string;

View File

@@ -0,0 +1,25 @@
export function isSystemdUnavailableDetail(detail?: string): boolean {
if (!detail) return false;
const normalized = detail.toLowerCase();
return (
normalized.includes("systemctl --user unavailable") ||
normalized.includes("systemctl not available") ||
normalized.includes("not been booted with systemd") ||
normalized.includes("failed to connect to bus") ||
normalized.includes("systemd user services are required")
);
}
export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}): string[] {
if (options.wsl) {
return [
"WSL2 needs systemd enabled: edit /etc/wsl.conf with [boot]\\nsystemd=true",
"Then run: wsl --shutdown (from PowerShell) and reopen your distro.",
"Verify: systemctl --user status",
];
}
return [
"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 `clawdbot daemon`.",
];
}

25
src/infra/wsl.ts Normal file
View File

@@ -0,0 +1,25 @@
import fs from "node:fs/promises";
let wslCached: boolean | null = null;
export function isWSLEnv(): boolean {
if (process.env.WSL_INTEROP || process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
return true;
}
return false;
}
export async function isWSL(): Promise<boolean> {
if (wslCached !== null) return wslCached;
if (isWSLEnv()) {
wslCached = true;
return wslCached;
}
try {
const release = await fs.readFile("/proc/sys/kernel/osrelease", "utf8");
wslCached = release.toLowerCase().includes("microsoft") || release.toLowerCase().includes("wsl");
} catch {
wslCached = false;
}
return wslCached;
}