mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
fix(openclaw): improve gateway startup diagnostics and fix UI polling loop (#13495)
### What this PR does Before this PR: - Gateway startup failures showed limited diagnostics (only stderr, no stdout) - OpenClawPage had infinite re-render loops caused by `setInterval` + `dispatch` in `useEffect` deps - Process exit with code 0 was not properly diagnosed when the daemonized child failed After this PR: - Capture both stdout and stderr from the gateway process for better failure diagnostics - Replace `setInterval` polling with SWR for stable, dedup'd status/health polling in the UI - Improve error messages for gateway startup timeout (combine health, stderr, stdout diagnostics) - Handle edge case where gateway exits with code 0 but never becomes healthy ### Why we need it and why it was done in this way The infinite re-render loop in the renderer was the root cause of false timeout errors during gateway health checks. Switching to SWR provides stable polling without the pitfall of `setInterval` + `dispatch` in `useEffect` deps. The diagnostics improvements (stdout capture, combined error output) are independent quality-of-life improvements that help debug gateway startup failures. ### Breaking changes None. ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and Keep it simple - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note NONE ``` --------- Signed-off-by: suyao <sy20010504@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ChannelInfo[]> {
|
||||
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 || []
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user