FPGA_WebLab/scripts/GenerateWebAPI.ts

430 lines
11 KiB
TypeScript

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<boolean> {
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<ChildProcess> {
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<ChildProcess> {
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<void> {
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<void>((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<void> {
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<void>((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<void> {
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<void> {
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<void> {
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<void> {
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);
});