190 lines
5.2 KiB
TypeScript
190 lines
5.2 KiB
TypeScript
import { spawn, exec, ChildProcess } from 'child_process';
|
|
import { app } from 'electron';
|
|
import path from 'path';
|
|
import log from 'electron-log';
|
|
|
|
const TERMINAL_PORT = 9997;
|
|
const HEALTH_CHECK_INTERVAL = 10000;
|
|
const HEALTH_CHECK_TIMEOUT = 2000;
|
|
|
|
class TerminalService {
|
|
private process: ChildProcess | null = null;
|
|
private processPid: number | null = null;
|
|
private healthCheckTimer: NodeJS.Timeout | null = null;
|
|
private _isRunning = false;
|
|
private _isStarting = false;
|
|
|
|
get port(): number {
|
|
return TERMINAL_PORT;
|
|
}
|
|
|
|
isRunning(): boolean {
|
|
return this._isRunning;
|
|
}
|
|
|
|
getStatus(): { running: boolean; port: number } {
|
|
return {
|
|
running: this._isRunning,
|
|
port: this.port,
|
|
};
|
|
}
|
|
|
|
private getExePath(): string {
|
|
const exeName = 'XCTerminal.exe';
|
|
const isPackaged = app.isPackaged;
|
|
|
|
let basePath: string;
|
|
if (isPackaged) {
|
|
basePath = path.join(process.resourcesPath, 'app.asar.unpacked');
|
|
} else {
|
|
basePath = process.cwd();
|
|
}
|
|
|
|
return path.join(basePath, 'services', 'xcterminal', exeName);
|
|
}
|
|
|
|
private getExeArgs(): string[] {
|
|
return ['--port', this.port.toString()];
|
|
}
|
|
|
|
private async checkHealth(): Promise<boolean> {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
|
|
|
|
const response = await fetch(`http://127.0.0.1:${this.port}`, {
|
|
signal: controller.signal,
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
return response.ok || response.status === 401;
|
|
} catch (error) {
|
|
log.warn('[TerminalService] Health check failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private startHealthCheck(): void {
|
|
if (this.healthCheckTimer) {
|
|
clearInterval(this.healthCheckTimer);
|
|
}
|
|
|
|
this.healthCheckTimer = setInterval(async () => {
|
|
const isHealthy = await this.checkHealth();
|
|
if (!isHealthy && this._isRunning) {
|
|
log.warn('[TerminalService] Service not responding');
|
|
}
|
|
}, HEALTH_CHECK_INTERVAL);
|
|
}
|
|
|
|
async start(): Promise<{ success: boolean; error?: string }> {
|
|
if (this._isRunning && this.process) {
|
|
log.info('[TerminalService] Already running');
|
|
return { success: true };
|
|
}
|
|
|
|
if (this._isStarting) {
|
|
log.info('[TerminalService] Already starting');
|
|
return { success: true };
|
|
}
|
|
|
|
this._isStarting = true;
|
|
|
|
try {
|
|
const exePath = this.getExePath();
|
|
const exeArgs = this.getExeArgs();
|
|
log.info(`[TerminalService] Starting from: ${exePath} with args: ${exeArgs.join(' ')}`);
|
|
|
|
this.process = spawn(exePath, exeArgs, {
|
|
stdio: 'pipe',
|
|
});
|
|
|
|
this.processPid = this.process.pid ?? null;
|
|
|
|
this.process.stdout?.on('data', (data) => {
|
|
log.info(`[Terminal] ${data}`);
|
|
});
|
|
|
|
this.process.stderr?.on('data', (data) => {
|
|
log.error(`[Terminal error] ${data}`);
|
|
});
|
|
|
|
this.process.on('error', (err) => {
|
|
log.error('[TerminalService] Process error:', err);
|
|
this._isRunning = false;
|
|
this._isStarting = false;
|
|
this.process = null;
|
|
this.processPid = null;
|
|
});
|
|
|
|
this.process.on('exit', (code) => {
|
|
log.info(`[TerminalService] Process exited with code ${code}`);
|
|
this._isRunning = false;
|
|
this._isStarting = false;
|
|
this.process = null;
|
|
this.processPid = null;
|
|
});
|
|
|
|
const maxWaitTime = 30000;
|
|
const checkInterval = 500;
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < maxWaitTime) {
|
|
const isHealthy = await this.checkHealth();
|
|
if (isHealthy) {
|
|
break;
|
|
}
|
|
await new Promise<void>((resolve) => setTimeout(resolve, checkInterval));
|
|
}
|
|
|
|
const finalHealth = await this.checkHealth();
|
|
if (!finalHealth) {
|
|
log.warn('[TerminalService] Health check failed after max wait time');
|
|
}
|
|
|
|
this._isRunning = true;
|
|
this._isStarting = false;
|
|
this.startHealthCheck();
|
|
|
|
log.info(`[TerminalService] Started successfully on port ${this.port}`);
|
|
return { success: true };
|
|
} catch (error: any) {
|
|
log.error('[TerminalService] Failed to start:', error);
|
|
this._isRunning = false;
|
|
this._isStarting = 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.processPid) {
|
|
log.info('[TerminalService] Not running');
|
|
return { success: true };
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
log.info(`[TerminalService] Stopping process ${this.processPid}...`);
|
|
|
|
exec(`taskkill /F /T /PID ${this.processPid}`, (error) => {
|
|
this.process = null;
|
|
this.processPid = null;
|
|
this._isRunning = false;
|
|
|
|
if (error) {
|
|
log.error('[TerminalService] Failed to stop:', error);
|
|
resolve({ success: false, error: error.message });
|
|
} else {
|
|
log.info('[TerminalService] Stopped');
|
|
resolve({ success: true });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
export const terminalService = new TerminalService(); |