import { spawn, ChildProcess } from 'child_process'; import { app } from 'electron'; import path from 'path'; import log from 'electron-log'; import { appConfig } from '../config'; 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 appConfig.opencode.port; } get healthCheckInterval(): number { return appConfig.opencode.healthCheckInterval; } get maxRestartAttempts(): number { return appConfig.opencode.maxRestartAttempts; } isRunning(): boolean { return this._isRunning; } getStatus(): { running: boolean; port: number; restartAttempts: number } { return { running: this._isRunning, port: this.port, restartAttempts: this.restartAttempts, }; } private getOpenCodePath(): string { if (appConfig.opencode.mode === 'exe') { const basePath = app.isPackaged ? path.dirname(app.getPath('exe')) : path.join(process.cwd(), 'bin'); return path.join(basePath, appConfig.opencode.exePath); } return 'opencode'; } private getOpenCodeArgs(): string[] { if (appConfig.opencode.mode === 'exe') { return ['serve']; } return ['serve']; } private getAuthHeaders(): Record { const password = 'xc_opencode_password'; const encoded = Buffer.from(`:${password}`).toString('base64'); return { 'Authorization': `Basic ${encoded}`, }; } private async checkHealth(): Promise { 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 { if (this.restartAttempts >= this.maxRestartAttempts) { log.error('[OpenCodeService] Max restart attempts reached, giving up'); this._isRunning = false; return; } this.restartAttempts++; log.info(`[OpenCodeService] Attempting restart (${this.restartAttempts}/${this.maxRestartAttempts})...`); 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(); } }, this.healthCheckInterval); } async start(): Promise<{ success: boolean; error?: string }> { if (this._isRunning && this.process) { log.info('[OpenCodeService] Already running'); return { success: true }; } try { const opencodePath = this.getOpenCodePath(); const opencodeArgs = this.getOpenCodeArgs(); const mode = appConfig.opencode.mode; log.info(`[OpenCodeService] Starting OpenCode server (mode: ${mode})...`); log.info(`[OpenCodeService] Path: ${opencodePath}`); this.process = spawn(opencodePath, opencodeArgs, { 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((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((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((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();