fix: improve WSL2 systemd daemon hints
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
|
- 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
|
## 2026.1.16-2
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { resolveIsNixMode } from "../../config/paths.js";
|
import { resolveIsNixMode } from "../../config/paths.js";
|
||||||
import { resolveGatewayService } from "../../daemon/service.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 { defaultRuntime } from "../../runtime.js";
|
||||||
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
|
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
|
||||||
import { renderGatewayServiceStartHints } from "./shared.js";
|
import { renderGatewayServiceStartHints } from "./shared.js";
|
||||||
@@ -89,7 +92,13 @@ export async function runDaemonStart(opts: DaemonLifecycleOptions = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!loaded) {
|
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({
|
emit({
|
||||||
ok: true,
|
ok: true,
|
||||||
result: "not-loaded",
|
result: "not-loaded",
|
||||||
@@ -229,7 +238,13 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!loaded) {
|
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({
|
emit({
|
||||||
ok: true,
|
ok: true,
|
||||||
result: "not-loaded",
|
result: "not-loaded",
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ export async function gatherDaemonStatus(
|
|||||||
const [loaded, command, runtime] = await Promise.all([
|
const [loaded, command, runtime] = await Promise.all([
|
||||||
service.isLoaded({ env: process.env }).catch(() => false),
|
service.isLoaded({ env: process.env }).catch(() => false),
|
||||||
service.readCommand(process.env).catch(() => null),
|
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({
|
const configAudit = await auditGatewayServiceConfig({
|
||||||
env: process.env,
|
env: process.env,
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import {
|
|||||||
} from "../../daemon/constants.js";
|
} from "../../daemon/constants.js";
|
||||||
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
|
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
|
||||||
import { resolveGatewayLogPaths } from "../../daemon/launchd.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 { getResolvedLoggerSettings } from "../../logging.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
||||||
@@ -164,6 +169,16 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
|||||||
spacer();
|
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) {
|
if (service.runtime?.missingUnit) {
|
||||||
defaultRuntime.error(errorText("Service unit not found."));
|
defaultRuntime.error(errorText("Service unit not found."));
|
||||||
for (const hint of renderRuntimeHints(service.runtime)) {
|
for (const hint of renderRuntimeHints(service.runtime)) {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import {
|
|||||||
resolveGatewayWindowsTaskName,
|
resolveGatewayWindowsTaskName,
|
||||||
} from "../daemon/constants.js";
|
} from "../daemon/constants.js";
|
||||||
import { resolveGatewayLogPaths } from "../daemon/launchd.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 type { GatewayServiceRuntime } from "../daemon/service-runtime.js";
|
||||||
import { getResolvedLoggerSettings } from "../logging.js";
|
import { getResolvedLoggerSettings } from "../logging.js";
|
||||||
|
|
||||||
@@ -54,6 +59,11 @@ export function buildGatewayRuntimeHints(
|
|||||||
return null;
|
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") {
|
if (runtime.cachedLabel && platform === "darwin") {
|
||||||
const label = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
const label = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||||
hints.push(
|
hints.push(
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
} from "../daemon/runtime-paths.js";
|
} from "../daemon/runtime-paths.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { buildServiceEnvironment } from "../daemon/service-env.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 { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||||
|
import { isWSL } from "../infra/wsl.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
import { sleep } from "../utils.js";
|
import { sleep } from "../utils.js";
|
||||||
@@ -55,6 +58,14 @@ export async function maybeRepairGatewayDaemon(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
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");
|
note("Gateway daemon not installed.", "Gateway");
|
||||||
if (params.cfg.gateway?.mode !== "remote") {
|
if (params.cfg.gateway?.mode !== "remote") {
|
||||||
const install = await params.prompter.confirmSkipInNonInteractive({
|
const install = await params.prompter.confirmSkipInNonInteractive({
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { isWSLEnv } from "../infra/wsl.js";
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRemoteEnvironment(): boolean {
|
export function isRemoteEnvironment(): boolean {
|
||||||
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
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.platform === "linux" &&
|
||||||
!process.env.DISPLAY &&
|
!process.env.DISPLAY &&
|
||||||
!process.env.WAYLAND_DISPLAY &&
|
!process.env.WAYLAND_DISPLAY &&
|
||||||
!isWSL()
|
!isWSLEnv()
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { callGateway } from "../gateway/call.js";
|
|||||||
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
||||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||||
|
import { isWSL } from "../infra/wsl.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
@@ -91,27 +92,6 @@ type BrowserOpenSupport = {
|
|||||||
command?: string;
|
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 = {
|
type BrowserOpenCommand = {
|
||||||
argv: string[] | null;
|
argv: string[] | null;
|
||||||
reason?: string;
|
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