509 lines
15 KiB
TypeScript
509 lines
15 KiB
TypeScript
import * as http from "http";
|
||
import * as fs from "fs";
|
||
import * as path from "path";
|
||
import * as vscode from "vscode";
|
||
|
||
/**
|
||
* VCD 文件 HTTP 服务器
|
||
* 用于为 波形查看器提供 VCD 文件访问
|
||
*/
|
||
export class VCDFileServer {
|
||
private server: http.Server | null = null;
|
||
private port: number = 0;
|
||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||
private extensionUri: vscode.Uri;
|
||
|
||
constructor(extensionUri: vscode.Uri) {
|
||
this.extensionUri = extensionUri;
|
||
}
|
||
|
||
/**
|
||
* 启动服务器
|
||
*/
|
||
public async start(): Promise<number> {
|
||
if (this.server) {
|
||
return this.port;
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
this.server = http.createServer((req, res) => {
|
||
this.handleRequest(req, res);
|
||
});
|
||
|
||
// 监听随机端口
|
||
this.server.listen(0, "127.0.0.1", () => {
|
||
const address = this.server!.address();
|
||
if (address && typeof address === "object") {
|
||
this.port = address.port;
|
||
console.log(`[VCDFileServer] 服务器已启动,端口: ${this.port}`);
|
||
resolve(this.port);
|
||
} else {
|
||
reject(new Error("无法获取服务器端口"));
|
||
}
|
||
});
|
||
|
||
this.server.on("error", (error) => {
|
||
console.error("[VCDFileServer] 服务器错误:", error);
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 停止服务器
|
||
*/
|
||
public stop(): void {
|
||
if (this.server) {
|
||
this.server.close();
|
||
this.server = null;
|
||
this.port = 0;
|
||
this.vcdFiles.clear();
|
||
console.log("[VCDFileServer] 服务器已停止");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注册 VCD 文件
|
||
*/
|
||
public registerFile(filePath: string): string {
|
||
const fileId = this.generateFileId(filePath);
|
||
this.vcdFiles.set(fileId, filePath);
|
||
console.log(`[VCDFileServer] 注册文件: ${fileId} -> ${filePath}`);
|
||
return fileId;
|
||
}
|
||
|
||
/**
|
||
* 获取文件 URL
|
||
*/
|
||
public getFileUrl(fileId: string): string {
|
||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||
}
|
||
|
||
/**
|
||
* 获取波形查看器 URL
|
||
*/
|
||
public getViewerUrl(fileId: string): string {
|
||
return `http://127.0.0.1:${this.port}/viewer/${fileId}`;
|
||
}
|
||
|
||
/**
|
||
* 生成文件 ID
|
||
*/
|
||
private generateFileId(filePath: string): string {
|
||
const timestamp = Date.now();
|
||
const fileName = path.basename(filePath);
|
||
return `${timestamp}-${fileName}`;
|
||
}
|
||
|
||
/**
|
||
* 处理 HTTP 请求
|
||
*/
|
||
private handleRequest(
|
||
req: http.IncomingMessage,
|
||
res: http.ServerResponse,
|
||
): void {
|
||
const url = req.url || "";
|
||
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
||
|
||
// 设置 CORS 头
|
||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||
|
||
// 处理 OPTIONS 请求
|
||
if (req.method === "OPTIONS") {
|
||
res.writeHead(200);
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
// 路由处理
|
||
if (url.startsWith("/viewer/")) {
|
||
this.handleViewerRequest(url, res);
|
||
} else if (url.startsWith("/vcd/")) {
|
||
this.handleVcdFileRequest(url, res);
|
||
} else if (url.startsWith("/static/")) {
|
||
this.handleStaticFileRequest(url, res);
|
||
} else {
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("Not Found");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理查看器页面请求
|
||
*/
|
||
private handleViewerRequest(url: string, res: http.ServerResponse): void {
|
||
const match = url.match(/^\/viewer\/(.+)$/);
|
||
if (!match) {
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("Not Found");
|
||
return;
|
||
}
|
||
|
||
const fileId = match[1];
|
||
const filePath = this.vcdFiles.get(fileId);
|
||
|
||
if (!filePath) {
|
||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("File Not Found");
|
||
return;
|
||
}
|
||
|
||
// 生成 HTML 页面
|
||
const html = this.generateViewerHtml(fileId, filePath);
|
||
res.writeHead(200, {
|
||
"Content-Type": "text/html; charset=utf-8",
|
||
"Content-Length": Buffer.byteLength(html),
|
||
});
|
||
res.end(html);
|
||
}
|
||
|
||
/**
|
||
* 处理 VCD 文件请求
|
||
*/
|
||
private handleVcdFileRequest(url: string, res: http.ServerResponse): void {
|
||
const match = url.match(/^\/vcd\/(.+)$/);
|
||
if (!match) {
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("Not Found");
|
||
return;
|
||
}
|
||
|
||
const fileId = match[1];
|
||
const filePath = this.vcdFiles.get(fileId);
|
||
|
||
if (!filePath) {
|
||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("File Not Found");
|
||
return;
|
||
}
|
||
|
||
// 检查文件是否存在
|
||
if (!fs.existsSync(filePath)) {
|
||
console.error(`[VCDFileServer] 文件不存在: ${filePath}`);
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("File Not Found");
|
||
return;
|
||
}
|
||
|
||
// 读取并发送文件
|
||
try {
|
||
const fileContent = fs.readFileSync(filePath);
|
||
res.writeHead(200, {
|
||
"Content-Type": "text/plain",
|
||
"Content-Length": fileContent.length,
|
||
});
|
||
res.end(fileContent);
|
||
console.log(`[VCDFileServer] 成功发送文件: ${filePath}`);
|
||
} catch (error) {
|
||
console.error(`[VCDFileServer] 读取文件失败:`, error);
|
||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||
res.end("Internal Server Error");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理静态文件请求(Surfer 资源)
|
||
*/
|
||
private handleStaticFileRequest(url: string, res: http.ServerResponse): void {
|
||
const match = url.match(/^\/static\/(.+)$/);
|
||
if (!match) {
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("Not Found");
|
||
return;
|
||
}
|
||
|
||
const fileName = match[1];
|
||
const filePath = path.join(
|
||
this.extensionUri.fsPath,
|
||
"media",
|
||
"surfer",
|
||
fileName,
|
||
);
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
console.error(`[VCDFileServer] 静态文件不存在: ${filePath}`);
|
||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||
res.end("File Not Found");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const fileContent = fs.readFileSync(filePath);
|
||
const contentType = this.getContentType(fileName);
|
||
res.writeHead(200, {
|
||
"Content-Type": contentType,
|
||
"Content-Length": fileContent.length,
|
||
});
|
||
res.end(fileContent);
|
||
} catch (error) {
|
||
console.error(`[VCDFileServer] 读取静态文件失败:`, error);
|
||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||
res.end("Internal Server Error");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取文件的 Content-Type
|
||
*/
|
||
private getContentType(fileName: string): string {
|
||
const ext = path.extname(fileName).toLowerCase();
|
||
const contentTypes: { [key: string]: string } = {
|
||
".js": "application/javascript",
|
||
".wasm": "application/wasm",
|
||
".html": "text/html",
|
||
".css": "text/css",
|
||
};
|
||
return contentTypes[ext] || "application/octet-stream";
|
||
}
|
||
|
||
/**
|
||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||
*/
|
||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||
try {
|
||
const buffer = fs.readFileSync(vcdFilePath, { encoding: "utf8" });
|
||
const lines = buffer.split("\n");
|
||
|
||
const scopeNames: string[] = [];
|
||
let scopeDepth = 0;
|
||
const scopeStack: string[] = [];
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
|
||
if (trimmed.startsWith("$enddefinitions")) {
|
||
break;
|
||
}
|
||
|
||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||
if (scopeMatch) {
|
||
const scopeType = scopeMatch[1];
|
||
const scopeName = scopeMatch[2];
|
||
|
||
if (scopeDepth === 0 && scopeType === "module") {
|
||
scopeStack.push(scopeName);
|
||
} else if (scopeDepth === 1 && scopeType === "module") {
|
||
const fullPath = [...scopeStack, scopeName];
|
||
scopeNames.push(fullPath.join("."));
|
||
}
|
||
|
||
scopeDepth++;
|
||
}
|
||
|
||
if (trimmed.startsWith("$upscope")) {
|
||
scopeDepth--;
|
||
if (scopeDepth === 0) {
|
||
scopeStack.pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
return scopeNames;
|
||
} catch (error) {
|
||
console.error("[VCDFileServer] 解析 VCD 文件失败:", error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成波形查看器 HTML 页面
|
||
*/
|
||
private generateViewerHtml(fileId: string, vcdFilePath: string): string {
|
||
const vcdUrl = this.getFileUrl(fileId);
|
||
const fileName = path.basename(vcdFilePath);
|
||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||
const scopeNamesJson = JSON.stringify(scopeNames);
|
||
|
||
const htmlPart1 = this.getHtmlPart1(fileName);
|
||
const htmlPart2 = this.getHtmlPart2(vcdUrl, scopeNamesJson);
|
||
const htmlPart3 = this.getHtmlPart3();
|
||
|
||
return htmlPart1 + htmlPart2 + htmlPart3;
|
||
}
|
||
|
||
private getHtmlPart1(fileName: string): string {
|
||
return `<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>波形查看器 - ${fileName}</title>
|
||
<script>
|
||
window.surferReady = false;
|
||
window.pendingVcdData = null;
|
||
|
||
function on_surfer_error(msg) {
|
||
console.log("Surfer error:", msg);
|
||
document.getElementById("error_message").innerHTML = msg;
|
||
document.getElementById("error_container").style.display = "block";
|
||
}
|
||
window.on_surfer_error = on_surfer_error;
|
||
</script>
|
||
<script type="module">
|
||
console.log('[Browser] 开始初始化 Surfer...');
|
||
import init from '/static/surfer.js';
|
||
await init({module_or_path: '/static/surfer_bg.wasm'});
|
||
console.log('[Browser] Surfer WASM 已加载');
|
||
|
||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/static/surfer.js';
|
||
window.inject_message = inject_message;
|
||
window.id_of_name = id_of_name;
|
||
window.draw_text_arrow = draw_text_arrow;
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
window.surferReady = true;
|
||
console.log('[Browser] Surfer 已完全初始化并准备就绪');
|
||
|
||
try {
|
||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||
console.log('[Browser] 已发送关闭日志面板命令');
|
||
} catch (e) {
|
||
console.log('[Browser] 关闭日志面板失败:', e);
|
||
}
|
||
|
||
if (window.pendingVcdData) {
|
||
console.log('[Browser] 发现待处理的 VCD 数据,立即加载');
|
||
loadVcdUrl(window.pendingVcdData);
|
||
window.pendingVcdData = null;
|
||
}
|
||
</script>`;
|
||
}
|
||
|
||
private getHtmlPart2(vcdUrl: string, scopeNamesJson: string): string {
|
||
return `
|
||
<script>
|
||
function loadVcdUrl(data) {
|
||
try {
|
||
console.log('[Browser] ========== 开始加载 VCD URL ==========');
|
||
console.log('[Browser] URL:', data.url);
|
||
console.log('[Browser] Scope names from VCD:', data.scopeNames);
|
||
|
||
setTimeout(() => {
|
||
console.log('[Browser] 通过 postMessage 发送 LoadUrl 命令');
|
||
window.postMessage({
|
||
command: 'LoadUrl',
|
||
url: data.url
|
||
}, '*');
|
||
console.log('[Browser] ✅ 已发送 LoadUrl 命令');
|
||
|
||
setTimeout(async () => {
|
||
try {
|
||
console.log('[Browser] 尝试自动添加所有信号');
|
||
let scopeNamesToTry = [];
|
||
|
||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||
console.log('[Browser] 使用解析的作用域名称:', scopeNamesToTry);
|
||
} else {
|
||
scopeNamesToTry = [['top'], ['testbench'], ['tb'], ['test'], ['dut']];
|
||
console.log('[Browser] 使用回退作用域名称');
|
||
}
|
||
|
||
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||
const scopeName = scopeNamesToTry[i];
|
||
try {
|
||
const addScopeMsg = {
|
||
"AddScope": [
|
||
{"strs": scopeName, "id": {"Wellen": i + 1}},
|
||
true
|
||
]
|
||
};
|
||
window.inject_message(JSON.stringify(addScopeMsg));
|
||
console.log('[Browser] 已发送 AddScope: ' + scopeName.join('.'));
|
||
} catch (e) {
|
||
console.log('[Browser] AddScope 失败: ' + scopeName.join('.'), e);
|
||
}
|
||
}
|
||
|
||
setTimeout(() => {
|
||
try {
|
||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||
console.log('[Browser] 已发送 ZoomToFit 命令');
|
||
} catch (e) {
|
||
console.log('[Browser] ZoomToFit 失败:', e);
|
||
}
|
||
}, 500);
|
||
} catch (e) {
|
||
console.error('[Browser] 添加信号失败:', e);
|
||
}
|
||
}, 1500);
|
||
}, 100);
|
||
} catch (error) {
|
||
console.error('[Browser] ❌ 加载 VCD 失败:', error);
|
||
on_surfer_error(error.message + '\\n' + error.stack);
|
||
}
|
||
}
|
||
window.loadVcdUrl = loadVcdUrl;
|
||
|
||
// 页面加载完成后自动加载 VCD
|
||
window.addEventListener('load', () => {
|
||
const vcdData = {
|
||
url: '${vcdUrl}',
|
||
scopeNames: ${scopeNamesJson}
|
||
};
|
||
if (window.surferReady) {
|
||
loadVcdUrl(vcdData);
|
||
} else {
|
||
window.pendingVcdData = vcdData;
|
||
}
|
||
});
|
||
</script>`;
|
||
}
|
||
|
||
private getHtmlPart3(): string {
|
||
return `
|
||
<style>
|
||
html, body {
|
||
overflow: hidden;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
height: 100%;
|
||
width: 100%;
|
||
background: #1e1e1e;
|
||
}
|
||
canvas {
|
||
margin-right: auto;
|
||
margin-left: auto;
|
||
display: block;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
#error_container {
|
||
padding: 1em;
|
||
border-radius: 0.5em;
|
||
margin: 0px auto;
|
||
max-width: 980px;
|
||
color: #f48771;
|
||
background-color: #5a1d1d;
|
||
position: relative;
|
||
height: 90%;
|
||
overflow: scroll;
|
||
}
|
||
#error_message {
|
||
overflow: scroll;
|
||
white-space: break-spaces;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="the_canvas_id"></canvas>
|
||
<div id="error_container" style="display: none;">
|
||
<h3>❌ Surfer 加载失败</h3>
|
||
<code id="error_message"></code>
|
||
</div>
|
||
<script src="/static/integration.js"></script>
|
||
<script>
|
||
register_message_listener();
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
}
|