diff --git a/src/main/services/OpenClawService.ts b/src/main/services/OpenClawService.ts index 81b4184806..855997f2f4 100644 --- a/src/main/services/OpenClawService.ts +++ b/src/main/services/OpenClawService.ts @@ -399,14 +399,18 @@ class OpenClawService { const proc = spawn(openclawPath, args, { env: shellEnv, detached: !isWin, // Only detach on non-Windows to avoid console flash - stdio: ['ignore', 'ignore', 'pipe'], + stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) proc.unref() // Collect early exit errors (e.g. binary crash on startup) let earlyExitError = '' + let stdoutOutput = '' let stderrOutput = '' + proc.stdout?.on('data', (data) => { + stdoutOutput += data.toString() + }) proc.stderr?.on('data', (data) => { stderrOutput += data.toString() }) @@ -414,10 +418,16 @@ class OpenClawService { earlyExitError = err.message }) proc.on('exit', (code) => { + // Capture output from both streams for diagnostics + const combinedOutput = [stderrOutput.trim(), stdoutOutput.trim()].filter(Boolean).join('\n') + const detail = combinedOutput.split('\n').filter(Boolean).slice(0, 5).join('\n') if (code !== 0) { - // Extract the most useful line from stderr for the error message - const detail = stderrOutput.trim().split('\n').filter(Boolean).slice(0, 3).join('\n') earlyExitError = detail || `gateway exited with code ${code}` + } else { + // Process exited with code 0 but gateway may not be healthy (e.g. daemonized child failed) + earlyExitError = detail + ? `gateway exited with code 0 but output: ${detail}` + : 'gateway process exited with code 0 before becoming healthy' } }) @@ -446,7 +456,15 @@ class OpenClawService { if (healthError) lastError = healthError } - const detail = lastError ? `\n${lastError}` : '' + // Combine all available diagnostics: health check errors, stderr, and stdout + const diagnostics = [ + lastError ? `health: ${lastError}` : '', + stderrOutput.trim() ? `stderr: ${stderrOutput.trim().split('\n').slice(0, 5).join('\n')}` : '', + stdoutOutput.trim() ? `stdout: ${stdoutOutput.trim().split('\n').slice(0, 5).join('\n')}` : '' + ] + .filter(Boolean) + .join('\n') + const detail = diagnostics ? `\n${diagnostics}` : '' throw new Error(`Gateway failed to start within ${maxWaitMs}ms (${pollCount} polls)${detail}`) } @@ -904,10 +922,9 @@ class OpenClawService { */ public async getChannelStatus(): Promise { try { - const response = await fetch(`http://localhost:${this.gatewayPort}/api/channels`, { + const response = await fetch(`http://127.0.0.1:${this.gatewayPort}/api/channels`, { signal: AbortSignal.timeout(5000) }) - if (response.ok) { const data = await response.json() return data.channels || [] diff --git a/src/renderer/src/pages/openclaw/OpenClawPage.tsx b/src/renderer/src/pages/openclaw/OpenClawPage.tsx index 5d7ffcd5b1..27fa633088 100644 --- a/src/renderer/src/pages/openclaw/OpenClawPage.tsx +++ b/src/renderer/src/pages/openclaw/OpenClawPage.tsx @@ -19,6 +19,7 @@ import { Download, ExternalLink, Play, Square } from 'lucide-react' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import useSWR from 'swr' import UpdateButton from './components/UpdateButton' @@ -188,23 +189,30 @@ const OpenClawPage: FC = () => { } }, [uninstallSuccess]) - const fetchStatus = useCallback(async () => { - try { - const status = await window.api.openclaw.getStatus() - dispatch(setGatewayStatus(status.status as GatewayStatus)) - } catch (err) { - logger.debug('Failed to fetch status', err as Error) - } - }, [dispatch]) + // Poll gateway status every 5s (only when installed). + // useSWR handles deduplication and stable polling without the infinite-loop + // pitfall of setInterval + dispatch in useEffect deps. + const isInstallPage = pageState === 'installed' - const fetchHealth = useCallback(async () => { - try { + useSWR( + isInstallPage ? 'openclaw/status' : null, + async () => { + const [status] = await Promise.all([window.api.openclaw.getStatus(), checkInstallation()]) + dispatch(setGatewayStatus(status.status as GatewayStatus)) + return status + }, + { refreshInterval: 5000, revalidateOnFocus: false } + ) + + useSWR( + isInstallPage && gatewayStatus === 'running' ? 'openclaw/health' : null, + async () => { const health = await window.api.openclaw.checkHealth() dispatch(setLastHealthCheck(health as HealthInfo)) - } catch (err) { - logger.debug('Failed to check health', err as Error) - } - }, [dispatch]) + return health + }, + { refreshInterval: 5000, revalidateOnFocus: false } + ) useEffect(() => { checkInstallation() @@ -221,23 +229,6 @@ const OpenClawPage: FC = () => { return cleanup }, []) - useEffect(() => { - if (pageState !== 'installed') return - - fetchStatus() - if (gatewayStatus === 'running') { - fetchHealth() - } - const interval = setInterval(() => { - checkInstallation() - fetchStatus() - if (gatewayStatus === 'running') { - fetchHealth() - } - }, 5000) - return () => clearInterval(interval) - }, [fetchStatus, fetchHealth, checkInstallation, gatewayStatus, pageState]) - const handleModelSelect = (modelUniqId: string) => { dispatch(setSelectedModelUniqId(modelUniqId)) }