feat: 将 OpenCode 服务管理抽取为独立模块
- 创建 electron/services/opencodeService.ts 独立服务模块 - 支持健康检测(每10秒)、自动重启(最多3次) - 随软件生命周期自动启动/停止
This commit is contained in:
@@ -5,8 +5,8 @@ import fs from 'fs';
|
||||
import log from 'electron-log';
|
||||
import { generatePdf } from './services/pdfGenerator';
|
||||
import { selectHtmlFile } from './services/htmlImport';
|
||||
import { opencodeService } from './services/opencodeService';
|
||||
import { electronState } from './state';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
|
||||
log.initialize();
|
||||
|
||||
@@ -15,9 +15,6 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
electronState.setDevelopment(!app.isPackaged);
|
||||
|
||||
let opencodeProcess: ChildProcess | null = null;
|
||||
const OPENCODE_PORT = 4096;
|
||||
|
||||
let lastClipboardText = '';
|
||||
|
||||
function startClipboardWatcher() {
|
||||
@@ -352,60 +349,16 @@ ipcMain.handle('remote-download-file', async (_event, id: string, serverHost: st
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('opencode-get-status', () => {
|
||||
return opencodeService.getStatus();
|
||||
});
|
||||
|
||||
ipcMain.handle('opencode-start-server', async () => {
|
||||
if (opencodeProcess) {
|
||||
log.info('Opencode server already running');
|
||||
return { success: true, port: OPENCODE_PORT };
|
||||
}
|
||||
|
||||
try {
|
||||
log.info('Starting opencode server...');
|
||||
opencodeProcess = spawn('opencode', ['serve'], {
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
detached: false,
|
||||
});
|
||||
|
||||
opencodeProcess.stdout?.on('data', (data) => {
|
||||
log.info(`[opencode] ${data}`);
|
||||
});
|
||||
|
||||
opencodeProcess.stderr?.on('data', (data) => {
|
||||
log.error(`[opencode error] ${data}`);
|
||||
});
|
||||
|
||||
opencodeProcess.on('error', (err) => {
|
||||
log.error('Opencode process error:', err);
|
||||
opencodeProcess = null;
|
||||
});
|
||||
|
||||
opencodeProcess.on('exit', (code) => {
|
||||
log.info(`Opencode process exited with code ${code}`);
|
||||
opencodeProcess = null;
|
||||
});
|
||||
|
||||
return { success: true, port: OPENCODE_PORT };
|
||||
} catch (error: any) {
|
||||
log.error('Failed to start opencode server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
return await opencodeService.start();
|
||||
});
|
||||
|
||||
ipcMain.handle('opencode-stop-server', async () => {
|
||||
if (!opencodeProcess) {
|
||||
log.info('Opencode server not running');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
log.info('Stopping opencode server...');
|
||||
opencodeProcess.kill('SIGTERM');
|
||||
opencodeProcess = null;
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
log.error('Failed to stop opencode server:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
return await opencodeService.stop();
|
||||
});
|
||||
|
||||
async function startServer() {
|
||||
@@ -444,6 +397,9 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
|
||||
await startServer();
|
||||
|
||||
await opencodeService.start();
|
||||
|
||||
await createWindow();
|
||||
|
||||
startClipboardWatcher();
|
||||
@@ -473,6 +429,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
opencodeService.stop();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
176
electron/services/opencodeService.ts
Normal file
176
electron/services/opencodeService.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import log from 'electron-log';
|
||||
|
||||
const OPENCODE_PORT = 4096;
|
||||
const HEALTH_CHECK_INTERVAL = 10000;
|
||||
const MAX_RESTART_ATTEMPTS = 3;
|
||||
const HEALTH_CHECK_TIMEOUT = 2000;
|
||||
|
||||
class OpenCodeService {
|
||||
private process: ChildProcess | null = null;
|
||||
private healthCheckTimer: NodeJS.Timeout | null = null;
|
||||
private restartAttempts = 0;
|
||||
private _isRunning = false;
|
||||
|
||||
get port(): number {
|
||||
return OPENCODE_PORT;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this._isRunning;
|
||||
}
|
||||
|
||||
getStatus(): { running: boolean; port: number; restartAttempts: number } {
|
||||
return {
|
||||
running: this._isRunning,
|
||||
port: this.port,
|
||||
restartAttempts: this.restartAttempts,
|
||||
};
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const password = 'xc_opencode_password';
|
||||
const encoded = Buffer.from(`:${password}`).toString('base64');
|
||||
return {
|
||||
'Authorization': `Basic ${encoded}`,
|
||||
};
|
||||
}
|
||||
|
||||
private async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${this.port}/session`, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders(),
|
||||
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT),
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
log.warn('[OpenCodeService] Health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async restart(): Promise<void> {
|
||||
if (this.restartAttempts >= MAX_RESTART_ATTEMPTS) {
|
||||
log.error('[OpenCodeService] Max restart attempts reached, giving up');
|
||||
this._isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.restartAttempts++;
|
||||
log.info(`[OpenCodeService] Attempting restart (${this.restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
|
||||
|
||||
await this.stop();
|
||||
await this.start();
|
||||
}
|
||||
|
||||
private startHealthCheck(): void {
|
||||
if (this.healthCheckTimer) {
|
||||
clearInterval(this.healthCheckTimer);
|
||||
}
|
||||
|
||||
this.healthCheckTimer = setInterval(async () => {
|
||||
const isHealthy = await this.checkHealth();
|
||||
if (!isHealthy && this._isRunning) {
|
||||
log.warn('[OpenCodeService] Health check failed, attempting restart...');
|
||||
await this.restart();
|
||||
}
|
||||
}, HEALTH_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
async start(): Promise<{ success: boolean; error?: string }> {
|
||||
if (this._isRunning && this.process) {
|
||||
log.info('[OpenCodeService] Already running');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
log.info('[OpenCodeService] Starting OpenCode server...');
|
||||
|
||||
this.process = spawn('opencode', ['serve'], {
|
||||
stdio: 'pipe',
|
||||
shell: true,
|
||||
detached: false,
|
||||
});
|
||||
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
log.info(`[OpenCode] ${data}`);
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (data) => {
|
||||
log.error(`[OpenCode error] ${data}`);
|
||||
});
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
log.error('[OpenCodeService] Process error:', err);
|
||||
this._isRunning = false;
|
||||
this.process = null;
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
log.info(`[OpenCodeService] Process exited with code ${code}`);
|
||||
this._isRunning = false;
|
||||
this.process = null;
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
|
||||
const isHealthy = await this.checkHealth();
|
||||
if (!isHealthy) {
|
||||
log.warn('[OpenCodeService] Server started but health check failed, waiting longer...');
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
|
||||
this._isRunning = true;
|
||||
this.restartAttempts = 0;
|
||||
this.startHealthCheck();
|
||||
|
||||
log.info(`[OpenCodeService] Started successfully on port ${this.port}`);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
log.error('[OpenCodeService] Failed to start:', error);
|
||||
this._isRunning = false;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<{ success: boolean; error?: string }> {
|
||||
if (this.healthCheckTimer) {
|
||||
clearInterval(this.healthCheckTimer);
|
||||
this.healthCheckTimer = null;
|
||||
}
|
||||
|
||||
if (!this.process) {
|
||||
log.info('[OpenCodeService] Not running');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
log.info('[OpenCodeService] Stopping...');
|
||||
|
||||
this.process.kill('SIGTERM');
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
if (this.process && !this.process.killed) {
|
||||
this.process.kill('SIGKILL');
|
||||
}
|
||||
|
||||
this.process = null;
|
||||
this._isRunning = false;
|
||||
this.restartAttempts = 0;
|
||||
|
||||
log.info('[OpenCodeService] Stopped');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
log.error('[OpenCodeService] Failed to stop:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const opencodeService = new OpenCodeService();
|
||||
Reference in New Issue
Block a user