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")); } }, 10000); // 10秒超时 }); } 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")); } }, 10000); // 10秒超时 }); } 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; } // 设置超时,如果 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([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; } // 设置超时,如果 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); }); await Promise.race([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;", ); content = content.replace( "{ AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelToken }", "{ AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type CancelToken }", ); // 写回文件 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 url = "http://127.0.0.1:5000/GetAPIClientCode"; const response = await fetch(url); if (!response.ok) { throw new Error( `Failed to fetch API client code: ${response.status} ${response.statusText}`, ); } const code = await response.text(); // 写入 APIClient.ts const filePath = "src/APIClient.ts"; fs.writeFileSync(filePath, code, "utf8"); console.log("✓ API client code fetched and written successfully"); // 添加后处理步骤 await postProcessApiClient(); } catch (error) { throw new Error(`Failed to generate API client: ${error}`); } } async function generateSignalRClient(): Promise { console.log("Generating SignalR TypeScript client..."); try { const { stdout, stderr } = await execAsync( "dotnet tsrts --project ./server/server.csproj --output ./src", ); if (stdout) console.log(stdout); if (stderr) console.error(stderr); console.log("✓ SignalR TypeScript client generated successfully"); } catch (error) { throw new Error(`Failed to generate SignalR client: ${error}`); } } async function main(): Promise { try { // Generate SignalR client await generateSignalRClient(); console.log("✓ SignalR TypeScript client generated successfully"); // 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); });