Files
XCDesktop/electron/services/opencodeService.ts

190 lines
5.4 KiB
TypeScript

import { spawn, exec, 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...');
const pid = this.process?.pid;
const processRef = this.process;
this.process = null;
this._isRunning = false;
if (pid && process.platform === 'win32') {
return new Promise((resolve) => {
exec(`taskkill /F /T /PID ${pid}`, (error) => {
if (error) {
log.warn('[OpenCodeService] taskkill failed, process may already be dead:', error.message);
}
this.restartAttempts = 0;
log.info('[OpenCodeService] Stopped');
resolve({ success: true });
});
});
} else if (pid && processRef) {
processRef.kill('SIGTERM');
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
if (!processRef.killed) {
processRef.kill('SIGKILL');
}
this.restartAttempts = 0;
log.info('[OpenCodeService] Stopped');
return { success: true };
}
this.restartAttempts = 0;
return { success: true };
} catch (error: any) {
log.error('[OpenCodeService] Failed to stop:', error);
return { success: false, error: error.message };
}
}
}
export const opencodeService = new OpenCodeService();