diff --git a/electron/main.ts b/electron/main.ts index 19319a4..ccedfe7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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(); } diff --git a/electron/services/opencodeService.ts b/electron/services/opencodeService.ts new file mode 100644 index 0000000..a46e06e --- /dev/null +++ b/electron/services/opencodeService.ts @@ -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 { + 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 >= 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((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();