fix: improve WSL2 systemd daemon hints
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
25
src/daemon/systemd-hints.ts
Normal file
25
src/daemon/systemd-hints.ts
Normal 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
25
src/infra/wsl.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user