feat(opencode): 改进 XCOpenCodeWeb 服务管理和健康检查
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { spawn, exec, ChildProcess } from 'child_process';
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
import log from 'electron-log';
|
||||
@@ -9,8 +9,10 @@ const HEALTH_CHECK_TIMEOUT = 2000;
|
||||
|
||||
class XCOpenCodeWebService {
|
||||
private process: ChildProcess | null = null;
|
||||
private processPid: number | null = null;
|
||||
private healthCheckTimer: NodeJS.Timeout | null = null;
|
||||
private _isRunning = false;
|
||||
private _isStarting = false;
|
||||
|
||||
get port(): number {
|
||||
return XCOPENCODEWEB_PORT;
|
||||
@@ -81,6 +83,13 @@ class XCOpenCodeWebService {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (this._isStarting) {
|
||||
log.info('[XCOpenCodeWebService] Already starting');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
this._isStarting = true;
|
||||
|
||||
try {
|
||||
const exePath = this.getExePath();
|
||||
const exeArgs = this.getExeArgs();
|
||||
@@ -88,10 +97,10 @@ class XCOpenCodeWebService {
|
||||
|
||||
this.process = spawn(exePath, exeArgs, {
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
detached: false,
|
||||
});
|
||||
|
||||
this.processPid = this.process.pid ?? null;
|
||||
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
log.info(`[XCOpenCodeWeb] ${data}`);
|
||||
});
|
||||
@@ -103,18 +112,23 @@ class XCOpenCodeWebService {
|
||||
this.process.on('error', (err) => {
|
||||
log.error('[XCOpenCodeWebService] Process error:', err);
|
||||
this._isRunning = false;
|
||||
this._isStarting = false;
|
||||
this.process = null;
|
||||
this.processPid = null;
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
log.info(`[XCOpenCodeWebService] Process exited with code ${code}`);
|
||||
this._isRunning = false;
|
||||
this._isStarting = false;
|
||||
this.process = null;
|
||||
this.processPid = null;
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
this._isRunning = true;
|
||||
this._isStarting = false;
|
||||
this.startHealthCheck();
|
||||
|
||||
log.info(`[XCOpenCodeWebService] Started successfully on port ${this.port}`);
|
||||
@@ -122,6 +136,7 @@ class XCOpenCodeWebService {
|
||||
} catch (error: any) {
|
||||
log.error('[XCOpenCodeWebService] Failed to start:', error);
|
||||
this._isRunning = false;
|
||||
this._isStarting = false;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
@@ -132,31 +147,28 @@ class XCOpenCodeWebService {
|
||||
this.healthCheckTimer = null;
|
||||
}
|
||||
|
||||
if (!this.process) {
|
||||
if (!this.processPid) {
|
||||
log.info('[XCOpenCodeWebService] Not running');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
log.info('[XCOpenCodeWebService] Stopping...');
|
||||
return new Promise((resolve) => {
|
||||
log.info(`[XCOpenCodeWebService] Stopping process ${this.processPid}...`);
|
||||
|
||||
this.process.kill('SIGTERM');
|
||||
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
exec(`taskkill /F /PID ${this.processPid}`, (error) => {
|
||||
this.process = null;
|
||||
this.processPid = null;
|
||||
this._isRunning = false;
|
||||
|
||||
if (this.process && !this.process.killed) {
|
||||
this.process.kill('SIGKILL');
|
||||
}
|
||||
|
||||
this.process = null;
|
||||
this._isRunning = false;
|
||||
|
||||
log.info('[XCOpenCodeWebService] Stopped');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
log.error('[XCOpenCodeWebService] Failed to stop:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error) {
|
||||
log.error('[XCOpenCodeWebService] Failed to stop:', error);
|
||||
resolve({ success: false, error: error.message });
|
||||
} else {
|
||||
log.info('[XCOpenCodeWebService] Stopped');
|
||||
resolve({ success: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,14 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
|
||||
export const OpenCodePage: React.FC = () => {
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [port, setPort] = useState<number>(3002)
|
||||
const [isHealthy, setIsHealthy] = useState(false)
|
||||
const [port, setPort] = useState<number>(9999)
|
||||
const startedRef = useRef(false)
|
||||
const restartingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
const init = async () => {
|
||||
const start = async () => {
|
||||
try {
|
||||
const portResult = await window.electronAPI.xcOpenCodeWebGetPort()
|
||||
if (mounted) {
|
||||
@@ -16,47 +17,64 @@ export const OpenCodePage: React.FC = () => {
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.xcOpenCodeWebStart()
|
||||
if (!result.success && mounted) {
|
||||
console.error('Failed to start XCOpenCodeWeb:', result.error)
|
||||
}
|
||||
restartingRef.current = false
|
||||
} catch (err) {
|
||||
console.error('Failed to start XCOpenCodeWeb:', err)
|
||||
restartingRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const status = await window.electronAPI.xcOpenCodeWebGetStatus()
|
||||
if (mounted) {
|
||||
if (result.success) {
|
||||
setIsRunning(true)
|
||||
setError(null)
|
||||
} else {
|
||||
setError(result.error || '启动失败')
|
||||
setIsHealthy(status.running)
|
||||
if (!status.running && !restartingRef.current) {
|
||||
restartingRef.current = true
|
||||
start()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) {
|
||||
setError(err instanceof Error ? err.message : '启动失败')
|
||||
setIsHealthy(false)
|
||||
if (!restartingRef.current) {
|
||||
restartingRef.current = true
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
if (!startedRef.current) {
|
||||
startedRef.current = true
|
||||
start()
|
||||
}
|
||||
|
||||
const interval = setInterval(checkStatus, 2000)
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
clearInterval(interval)
|
||||
window.electronAPI.xcOpenCodeWebStop()
|
||||
setIsRunning(false)
|
||||
startedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="max-w-4xl mx-auto w-full px-14 py-4 flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
OpenCode
|
||||
</h1>
|
||||
<span className={`px-2 py-1 text-xs rounded ${isRunning ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}>
|
||||
{isRunning ? '运行中' : '启动中...'}
|
||||
</span>
|
||||
{error && (
|
||||
<span className="text-red-500 text-sm">{error}</span>
|
||||
)}
|
||||
</div>
|
||||
{isRunning && (
|
||||
<div className="h-full w-full relative">
|
||||
<span className={`absolute bottom-2 right-2 z-10 w-1.5 h-1.5 rounded-full ${isHealthy ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
{!isHealthy && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{isHealthy && (
|
||||
<iframe
|
||||
src={`http://localhost:${port}`}
|
||||
className="flex-1 w-full border-0"
|
||||
className="h-full w-full border-0"
|
||||
title="XCOpenCodeWeb"
|
||||
/>
|
||||
)}
|
||||
|
||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -44,6 +44,7 @@ export interface ElectronAPI {
|
||||
xcOpenCodeWebStop: () => Promise<{ success: boolean; error?: string }>
|
||||
xcOpenCodeWebGetStatus: () => Promise<{ running: boolean; port: number }>
|
||||
xcOpenCodeWebGetPort: () => Promise<{ port: number }>
|
||||
xcOpenCodeWebCheckReady: () => Promise<{ ready: boolean }>
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Reference in New Issue
Block a user