import { spawn, exec, ChildProcess } from 'child_process'; import { promisify } from 'util'; import fetch from 'node-fetch'; import * as fs from 'fs'; const execAsync = promisify(exec); // Windows 支持函数 function getCommand(command: string): string { // dotnet 在 Windows 上不需要 .cmd 后缀 if (command === 'dotnet') { return 'dotnet'; } return process.platform === 'win32' ? `${command}.cmd` : command; } function getSpawnOptions() { return process.platform === 'win32' ? { stdio: 'pipe', shell: true } : { stdio: 'pipe' }; } async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (response.ok) { console.log('✓ Server is ready'); return true; } } catch (error) { // Server not ready yet } console.log(`Waiting for server... (${i + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, interval)); } return false; } // 改进全局变量类型 let serverProcess: ChildProcess | null = null; let webProcess: ChildProcess | null = null; async function startWeb(): Promise { console.log('Starting Vite frontend...'); return new Promise((resolve, reject) => { const process = spawn(getCommand('npm'), ['run', 'dev'], getSpawnOptions() as any); let webStarted = false; process.stdout?.on('data', (data) => { const output = data.toString(); console.log(`Web: ${output}`); // 检查 Vite 是否已启动 if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) { webStarted = true; resolve(process); } }); process.stderr?.on('data', (data) => { console.error(`Web Error: ${data}`); }); process.on('error', (error) => { reject(error); }); process.on('exit', (code, signal) => { console.log(`Web process exited with code ${code} and signal ${signal}`); if (!webStarted) { reject(new Error(`Web process exited unexpectedly with code ${code}`)); } }); // 存储进程引用 webProcess = process; // 超时处理 setTimeout(() => { if (!webStarted) { reject(new Error('Web server failed to start within timeout')); } }, 30000); // 30秒超时 }); } async function startServer(): Promise { console.log('Starting .NET server...'); return new Promise((resolve, reject) => { const process = spawn(getCommand('dotnet'), ['run', '--property:Configuration=Release'], { cwd: 'server', ...getSpawnOptions() } as any); let serverStarted = false; process.stdout?.on('data', (data) => { const output = data.toString(); console.log(`Server: ${output}`); // 检查服务器是否已启动 if (output.includes('Now listening on:') && !serverStarted) { serverStarted = true; resolve(process); } }); process.stderr?.on('data', (data) => { console.error(`Server Error: ${data}`); }); process.on('error', (error) => { reject(error); }); process.on('exit', (code, signal) => { console.log(`Server process exited with code ${code} and signal ${signal}`); if (!serverStarted) { reject(new Error(`Server process exited unexpectedly with code ${code}`)); } }); // 存储进程引用 serverProcess = process; // 超时处理 setTimeout(() => { if (!serverStarted) { reject(new Error('Server failed to start within timeout')); } }, 30000); // 30秒超时 }); } async function stopServer(): Promise { console.log('Stopping server...'); if (!serverProcess) { console.log('No server process to stop'); return; } try { // 检查进程是否还存在 if (serverProcess.killed || serverProcess.exitCode !== null) { console.log('✓ Server process already terminated'); serverProcess = null; return; } // 发送 SIGTERM 信号 const killed = serverProcess.kill('SIGTERM'); if (!killed) { console.warn('Failed to send SIGTERM to server process'); return; } // 等待进程优雅退出 const exitPromise = new Promise((resolve) => { if (serverProcess) { serverProcess.on('exit', () => { console.log('✓ Server stopped gracefully'); resolve(); }); } else { resolve(); } }); // 设置超时,如果 3 秒内没有退出则强制终止 const timeoutPromise = new Promise((resolve) => { setTimeout(() => { if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) { console.log('Force killing server process...'); serverProcess.kill('SIGKILL'); } resolve(); }, 3000); // 减少超时时间到3秒 }); await Promise.race([exitPromise, timeoutPromise]); } catch (error) { console.warn('Warning: Could not stop server process:', error); } finally { serverProcess = null; // 只有在进程可能没有正常退出时才执行清理 // 移除自动清理逻辑,因为正常退出时不需要 } } async function stopWeb(): Promise { console.log('Stopping web server...'); if (!webProcess) { console.log('No web process to stop'); return; } try { // 检查进程是否还存在 if (webProcess.killed || webProcess.exitCode !== null) { console.log('✓ Web process already terminated'); webProcess = null; return; } // 发送 SIGTERM 信号 const killed = webProcess.kill('SIGTERM'); if (!killed) { console.warn('Failed to send SIGTERM to web process'); return; } // 等待进程优雅退出 const exitPromise = new Promise((resolve) => { if (webProcess) { webProcess.on('exit', () => { console.log('✓ Web server stopped gracefully'); resolve(); }); } else { resolve(); } }); // 设置超时,如果 3 秒内没有退出则强制终止 const timeoutPromise = new Promise((resolve) => { setTimeout(() => { if (webProcess && !webProcess.killed && webProcess.exitCode === null) { console.log('Force killing web process...'); webProcess.kill('SIGKILL'); } resolve(); }, 3000); // 减少超时时间到3秒 }); await Promise.race([exitPromise, timeoutPromise]); } catch (error) { console.warn('Warning: Could not stop web process:', error); } finally { webProcess = null; // 只有在进程可能没有正常退出时才执行清理 // 移除自动清理逻辑,因为正常退出时不需要 } } async function postProcessApiClient(): Promise { console.log('Post-processing API client...'); try { const filePath = 'src/APIClient.ts'; // 检查文件是否存在 if (!fs.existsSync(filePath)) { throw new Error(`API client file not found: ${filePath}`); } // 读取文件内容 let content = fs.readFileSync(filePath, 'utf8'); // 替换 ArgumentException 中的 message 属性声明 content = content.replace( /(\s+)message!:\s*string;/g, '$1declare message: string;' ); // 写回文件 fs.writeFileSync(filePath, content, 'utf8'); console.log('✓ API client post-processing completed'); } catch (error) { throw new Error(`Failed to post-process API client: ${error}`); } } async function generateApiClient(): Promise { console.log('Generating API client...'); try { const npxCommand = getCommand('npx'); await execAsync(`${npxCommand} nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts`); console.log('✓ API client generated successfully'); // 添加后处理步骤 await postProcessApiClient(); } catch (error) { throw new Error(`Failed to generate API client: ${error}`); } } async function main(): Promise { try { // Start web frontend first await startWeb(); console.log('✓ Frontend started'); // Wait a bit for frontend to fully initialize await new Promise(resolve => setTimeout(resolve, 3000)); // Start server await startServer(); console.log('✓ Backend started'); // Wait for server to be ready (给服务器额外时间完全启动) await new Promise(resolve => setTimeout(resolve, 2000)); // Check if swagger endpoint is available const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json'); if (!serverReady) { throw new Error('Server failed to start within the expected time'); } // Generate API client await generateApiClient(); console.log('✓ API generation completed successfully'); } catch (error) { console.error('❌ Error:', error); process.exit(1); } finally { // Always try to stop processes in order: server first, then web await stopServer(); await stopWeb(); } } // 改进的进程终止处理 - 添加防重复执行 let isCleaningUp = false; const cleanup = async (signal: string) => { if (isCleaningUp) { console.log('Cleanup already in progress, ignoring signal'); return; } isCleaningUp = true; console.log(`\nReceived ${signal}, cleaning up...`); try { await Promise.all([ stopServer(), stopWeb() ]); } catch (error) { console.error('Error during cleanup:', error); } // 立即退出,不等待 process.exit(0); }; process.on('SIGINT', () => cleanup('SIGINT')); process.on('SIGTERM', () => cleanup('SIGTERM')); // 处理未捕获的异常 process.on('uncaughtException', async (error) => { if (isCleaningUp) return; console.error('❌ Uncaught exception:', error); isCleaningUp = true; try { await Promise.all([ stopServer(), stopWeb() ]); } catch (cleanupError) { console.error('Error during cleanup:', cleanupError); } process.exit(1); }); process.on('unhandledRejection', async (reason, promise) => { if (isCleaningUp) return; console.error('❌ Unhandled rejection at:', promise, 'reason:', reason); isCleaningUp = true; try { await Promise.all([ stopServer(), stopWeb() ]); } catch (cleanupError) { console.error('Error during cleanup:', cleanupError); } process.exit(1); }); main().catch(async (error) => { if (isCleaningUp) return; console.error('❌ Unhandled error:', error); isCleaningUp = true; try { await Promise.all([ stopServer(), stopWeb() ]); } catch (cleanupError) { console.error('Error during cleanup:', cleanupError); } process.exit(1); });