- 添加 electron/config.ts 配置文件 - 支持 command(命令行) 和 exe 两种模式 - 更新 package.json 打包配置,添加 bin 目录 - 更新 .gitignore 忽略 bin/*.exe
207 lines
5.6 KiB
TypeScript
207 lines
5.6 KiB
TypeScript
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<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 >= 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<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();
|