From 2b8b1181eba9af82670bdf0e97f9d5d60d0acff5 Mon Sep 17 00:00:00 2001 From: rain <14272775+rrain123@user.noreply.gitee.com> Date: Tue, 19 May 2026 09:50:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20GDB-DAP=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=B0=83=E8=AF=95=E9=93=BE=E8=B7=AF=E4=B8=8E=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + AGENTS.md | 10 + docs/01-实施任务清单.md | 6 + docs/10-GDB-DAP-Wrapper设计文档.md | 17 +- .../examples/gdb-dap-sync-allstop.launch.json | 24 + .../docs/GDB-DAP-Wrapper测试说明.md | 58 +- packages/dsp-mod-debug/package.json | 8 + .../dsp-mod-debug/src/gdbDapDebugProvider.ts | 4 + .../dsp-mod-debug/src/gdbDapWrapper/index.ts | 36 +- .../dsp-mod-debug/src/gdbDapWrapper/types.ts | 21 +- .../src/gdbDapWrapper/wrapper.ts | 825 ++++++++++++++++-- 11 files changed, 915 insertions(+), 97 deletions(-) create mode 100644 docs/examples/gdb-dap-sync-allstop.launch.json diff --git a/.gitignore b/.gitignore index 092277d..ac30138 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ dist/ *.log npm-debug.log* +# Local temporary analysis/test artifacts +.tmp/ + # Local script state scripts/.local-state/ diff --git a/AGENTS.md b/AGENTS.md index 86b5c82..fcbe556 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,16 @@ These rules have the highest priority because the same issues have already happe 2. Do not proactively delete files. If deletion appears necessary, explain which files would be deleted and why, then wait for explicit approval. 3. Do not proactively perform global replacement or sweeping rewrites, including encoding conversion, bulk formatting, batch rename, or similar broad transformations. If such a change appears necessary, explain scope and risk first and wait for approval. +## Cross-Platform Rules + +1. The product is delivered as the `dsp-suite` extension, but the internal modules still have separate responsibilities. Cross-platform behavior must be designed at the product level, not only inside one module. +2. When changing debug, install, path resolution, toolchain resolution, archive extraction, build execution, or script-launching code, consider Linux compatibility at the same time. Do not treat Linux as a final-stage patch. +3. Keep platform-specific logic centralized in `dsp-core` whenever possible. Modules such as `dsp-mod-build`, `dsp-mod-debug`, `dsp-mod-project`, and `dsp-mod-ai` should consume shared core capabilities instead of duplicating `win32` / `linux` checks. +4. Do not hardcode Windows-only assumptions such as `.exe`, `.bat`, `cmd.exe`, backslash paths, `AppData`, or Cygwin `/cygdrive` paths unless the code path is explicitly Windows-only. +5. Tool package metadata should describe platform-specific binaries and layouts. Prefer resolving tools from package/index metadata over guessing executable names in feature modules. +6. Linux support should be preserved while the Windows main workflow is being stabilized. Full Linux validation can happen after the Windows install/build/debug path is stable, but new platform-sensitive code should keep Linux paths viable from the start. +7. For local developer scripts, PowerShell scripts may remain Windows-focused, but product runtime code should avoid depending on PowerShell or Windows shell behavior. + ## Chinese And Encoding Rules 1. Use UTF-8 by default for all file reads and writes. diff --git a/docs/01-实施任务清单.md b/docs/01-实施任务清单.md index 8f7e039..95f44ff 100644 --- a/docs/01-实施任务清单.md +++ b/docs/01-实施任务清单.md @@ -97,6 +97,8 @@ - 保持调试能力检查与入口命令可用 - 在统一入口中收口用户任务文案 - 明确调试配置入口与调试启动入口的关系 +- GDB-DAP 默认采用同步 all-stop 多核调试语义,多个 DSP core 映射为 DAP threads +- GDB-DAP DSP/JTAG 启动默认采用 `manualLoad` 链路,由 wrapper 接管 `opencore -> file -> load` 后续任务: @@ -104,6 +106,7 @@ - 对齐原调试配置流程 - 补齐线程控制与回归验证 - 完成 DSS 调试在 `dsp-suite` 单插件形态下的主链路验证 +- 异步调试作为后续高级能力扩展,不影响当前同步 all-stop 默认链路 ### `dsp-mod-ai` @@ -188,12 +191,15 @@ - [x] 单文件构建入口 - [x] 构建错误/警告摘要 - [x] 调试线程语义兼容层 +- [x] GDB-DAP 同步 all-stop 多核语义确认 +- [ ] GDB-DAP 异步调试能力扩展 ### P3 - [x] `dsp-language` client/server 全链路迁移 - [x] 完整离线安装器流程 - [x] `dsp-suite` 单插件入口与聚合打包链路 +- [x] GDB-DAP `manualLoad` 启动链路可用性验证 - [ ] AI 与工程管理 Webview 协议迁移 - [ ] 原项目管理界面接入 - [ ] 用户态安装流程升级 diff --git a/docs/10-GDB-DAP-Wrapper设计文档.md b/docs/10-GDB-DAP-Wrapper设计文档.md index 9d1f4f1..850378e 100644 --- a/docs/10-GDB-DAP-Wrapper设计文档.md +++ b/docs/10-GDB-DAP-Wrapper设计文档.md @@ -213,6 +213,7 @@ VS Code 配置提供者,负责: // DAP 后端配置 "dapServerMode": "gdb", + "dspLaunchMode": "manualLoad", "dapServerPath": "/path/to/gdb", "gdbPath": "/path/to/gdb", "gdbDataDirectory": "/path/to/share/gdb", @@ -239,6 +240,7 @@ VS Code 配置提供者,负责: | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `dapServerMode` | string | 'gdb' | DAP 后端模式:'gdb' 或 'external' | +| `dspLaunchMode` | string | 'manualLoad' | DSP/JTAG 默认启动模式,由 wrapper 接管 `opencore -> file -> load` | | `dapServerPath` | string | - | DAP 后端路径 | | `gdbPath` | string | 'gdb' | gdb 路径 | | `injectOpenCore` | boolean | true | 是否注入 opencore | @@ -251,6 +253,19 @@ VS Code 配置提供者,负责: --- +## 调试语义 + +当前 GDB-DAP DSP/JTAG 链路默认采用同步 all-stop 语义: + +- 多个 DSP core 通过 DAP `threads` 暴露给 VS Code +- `continue` 默认让全部 core 继续运行 +- 命中断点后按 all-stop 语义停止全部 core +- 当前选中的 thread/core 决定调用栈、变量和单步上下文 + +异步调试能力作为后续高级扩展保留,不影响当前同步 all-stop 默认链路。 + +--- + ## 消息流程 ### 正常启动流程 @@ -366,4 +381,4 @@ Wrapper 启动时读取以下环境变量: - [DAP 协议规范](https://microsoft.github.io/debug-adapter-protocol/) - [GDB DAP 文档](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debug-Adapter-Protocol.html) -- [VS Code 调试扩展指南](https://code.visualstudio.com/api/extension-guides/debugging-extension) \ No newline at end of file +- [VS Code 调试扩展指南](https://code.visualstudio.com/api/extension-guides/debugging-extension) diff --git a/docs/examples/gdb-dap-sync-allstop.launch.json b/docs/examples/gdb-dap-sync-allstop.launch.json new file mode 100644 index 0000000..d0b303f --- /dev/null +++ b/docs/examples/gdb-dap-sync-allstop.launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "DSP GDB-DAP 同步多核调试", + "type": "gdb-dap", + "request": "launch", + "program": "${workspaceFolder}/O0/sync-smoke-cio-suite-corelocal.out", + "cwd": "${workspaceFolder}", + "gdbPath": "D:/Rain/work/VscodeIDE-WorkSpace/public/gdb/cyg/bin/gdb.exe", + "gdbDataDirectory": "D:/Rain/work/VscodeIDE-WorkSpace/public/gdb/cyg/share/gdb", + "dapServerMode": "gdb", + "dspLaunchMode": "manualLoad", + "injectOpenCore": true, + "opencoreCommand": "opencore", + "opencoreArgs": "0 1 2 3", + "opencoreInjectPhase": "beforeConfigurationDone", + "pauseAllThreadsBeforeConfigurationDone": false, + "allowNotStoppedOnConfigurationDone": true, + "traceDapTraffic": true, + "traceDapPayloadMaxLen": 2000 + } + ] +} diff --git a/packages/dsp-mod-debug/docs/GDB-DAP-Wrapper测试说明.md b/packages/dsp-mod-debug/docs/GDB-DAP-Wrapper测试说明.md index ff9b401..0e7d780 100644 --- a/packages/dsp-mod-debug/docs/GDB-DAP-Wrapper测试说明.md +++ b/packages/dsp-mod-debug/docs/GDB-DAP-Wrapper测试说明.md @@ -41,7 +41,30 @@ npm install npm run build ``` -### 4. 环境变量配置(可选) +### 4. 推荐测试模板 + +当前 DSP/JTAG 深测推荐使用同步 all-stop 模式: + +- `dspLaunchMode` 使用 `manualLoad` +- `opencoreArgs` 使用 `0 1 2 3` 打开 4 个 core +- 多个 DSP core 映射为 DAP `threads` +- `continue` 默认全核继续,断点命中后按 all-stop 停止 + +模板文件: + +```text +docs/examples/gdb-dap-sync-allstop.launch.json +``` + +测试端使用时只需要按实际环境调整: + +- `program` +- `cwd` +- `gdbPath` +- `gdbDataDirectory` +- `opencoreArgs` + +### 5. 环境变量配置(可选) ```bash # GDB 路径 @@ -87,9 +110,12 @@ export TRACE_DAP_TRAFFIC=1 "name": "GDB DAP Debug", "program": "${workspaceFolder}/Debug/program.out", "gdbPath": "/usr/bin/gdb", + "gdbDataDirectory": "/usr/share/gdb", "dapServerMode": "gdb", + "dspLaunchMode": "manualLoad", "injectOpenCore": true, "opencoreArgs": "0 1 2 3", + "allowNotStoppedOnConfigurationDone": true, "traceDapTraffic": true } ] @@ -293,7 +319,26 @@ npm run test --- -### TC-07: 流量日志测试 +### TC-07: 同步 all-stop 多核语义测试 + +**目的**: 验证当前默认同步多核调试语义 + +**步骤**: +1. 使用 `dspLaunchMode: "manualLoad"` 启动调试 +2. 确认线程列表显示多个 DSP core +3. 执行 Continue +4. 命中任意断点 +5. 查看 DAP stopped 事件 + +**预期结果**: +- Continue 响应包含 `allThreadsContinued: true` +- stopped 事件包含 `allThreadsStopped: true` +- 调用栈、变量、寄存器按当前选中的 thread/core 展示 +- 源码断点红点和断点面板保持一致 + +--- + +### TC-08: 流量日志测试 **目的**: 验证 DAP 流量日志功能 @@ -315,7 +360,7 @@ npm run test --- -### TC-08: external 模式测试 +### TC-09: external 模式测试 **目的**: 验证外部 dapserver 模式 @@ -334,7 +379,7 @@ npm run test --- -### TC-09: 配置默认值测试 +### TC-10: 配置默认值测试 **目的**: 验证配置默认值补齐 @@ -356,10 +401,11 @@ npm run test - `injectOpenCore` 默认为 true - `traceDapTraffic` 默认为 true - `pauseAllThreadsBeforeConfigurationDone` 默认为 true +- `dspLaunchMode` 默认为 `manualLoad` --- -### TC-10: 错误配置测试 +### TC-11: 错误配置测试 **目的**: 验证错误配置的提前失败 @@ -542,4 +588,4 @@ export TRACE_DAP_TRAFFIC=1 - [GDB-DAP Wrapper 设计文档](../../docs/10-GDB-DAP-Wrapper设计文档.md) - [DAP 协议规范](https://microsoft.github.io/debug-adapter-protocol/) -- [GDB DAP 文档](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debug-Adapter-Protocol.html) \ No newline at end of file +- [GDB DAP 文档](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debug-Adapter-Protocol.html) diff --git a/packages/dsp-mod-debug/package.json b/packages/dsp-mod-debug/package.json index f18603b..5449445 100644 --- a/packages/dsp-mod-debug/package.json +++ b/packages/dsp-mod-debug/package.json @@ -210,6 +210,12 @@ "default": "gdb", "description": "gdb: gdb binary itself serves DAP; external: launch separate dapserver --gdb" }, + "dspLaunchMode": { + "type": "string", + "enum": ["manualLoad", "gdbNative"], + "default": "manualLoad", + "description": "manualLoad: DSP/JTAG synchronized all-stop launch, wrapper runs opencore -> file -> load; gdbNative: use native GDB DAP launch behavior." + }, "dapServerPath": { "type": "string", "description": "Path to DAP server executable (used in external mode, optional in gdb mode)" @@ -279,6 +285,7 @@ "program": "${workspaceFolder}/Debug/program.out", "cwd": "${workspaceFolder}", "dapServerMode": "gdb", + "dspLaunchMode": "manualLoad", "traceDapTraffic": true, "traceDapPayloadMaxLen": 1200, "pauseAllThreadsBeforeConfigurationDone": true, @@ -298,6 +305,7 @@ "program": "^\"\\${workspaceFolder}/Debug/program.out\"", "cwd": "^\"\\${workspaceFolder}\"", "dapServerMode": "gdb", + "dspLaunchMode": "manualLoad", "traceDapTraffic": true, "traceDapPayloadMaxLen": 1200, "pauseAllThreadsBeforeConfigurationDone": true, diff --git a/packages/dsp-mod-debug/src/gdbDapDebugProvider.ts b/packages/dsp-mod-debug/src/gdbDapDebugProvider.ts index fb6e442..33a160a 100644 --- a/packages/dsp-mod-debug/src/gdbDapDebugProvider.ts +++ b/packages/dsp-mod-debug/src/gdbDapDebugProvider.ts @@ -65,6 +65,7 @@ export class GdbDapDebugProvider implements vscode.DebugConfigurationProvider { * - pauseAllThreadsBeforeConfigurationDone: true (多核场景稳定性) * - allowNotStoppedOnConfigurationDone: true (容错处理) * - injectOpenCore: true + * - dspLaunchMode: 'manualLoad' (DSP/JTAG 默认由 wrapper 接管加载链路) * - opencoreInjectPhase: gdb 模式为 'beforeConfigurationDone',external 模式为 'afterConfigurationDone' */ public async resolveDebugConfiguration( @@ -145,6 +146,9 @@ export class GdbDapDebugProvider implements vscode.DebugConfigurationProvider { normalized.opencoreCommand = normalized.opencoreCommand || 'opencore'; // opencore 参数,默认 "0 1 2 3" 表示对核 0-3 执行操作 normalized.opencoreArgs = normalized.opencoreArgs || '0 1 2 3'; + // DSP/JTAG 调试默认使用同步 all-stop 语义: + // wrapper 接管 opencore -> file -> load,多个 core 以 DAP threads 展示。 + normalized.dspLaunchMode = normalized.dspLaunchMode || 'manualLoad'; // ========== 配置验证 ========== // launch 模式必须指定要调试的程序文件 diff --git a/packages/dsp-mod-debug/src/gdbDapWrapper/index.ts b/packages/dsp-mod-debug/src/gdbDapWrapper/index.ts index 515ce4a..979590f 100644 --- a/packages/dsp-mod-debug/src/gdbDapWrapper/index.ts +++ b/packages/dsp-mod-debug/src/gdbDapWrapper/index.ts @@ -126,6 +126,37 @@ function normalizeConfiguredPath(value: string | undefined): string | undefined return path.resolve(trimmed); } +/** + * 将 Windows 路径转换为 Cygwin 风格路径。 + * + * @description + * 某些基于 Cygwin 的 gdb 在传递 --data-directory 时,需要使用 + * /cygdrive/<盘符>/... 形式,而不是 D:\... 这种 Windows 路径。 + * 如果输入本身已经是类 Unix 路径,则保持原样。 + * + * @param value - 原始路径 + * @returns 可供 Cygwin gdb 使用的路径 + */ +function toCygwinPath(value: string): string { + const trimmed = value.trim(); + if (!trimmed) { + return trimmed; + } + + if (trimmed.startsWith('/')) { + return trimmed; + } + + const match = trimmed.match(/^([a-zA-Z]):[\\/](.*)$/); + if (!match) { + return trimmed.replace(/\\/g, '/'); + } + + const drive = match[1].toLowerCase(); + const rest = match[2].replace(/\\/g, '/'); + return `/cygdrive/${drive}/${rest}`; +} + /** * 自动探测 gdb 的 data-directory 路径。 * @@ -188,7 +219,7 @@ function buildGdbModeArgs( const args = hasExplicitArgs ? [...configuredArgs] : ['-i', 'dap']; // 若 data-directory 存在且未显式指定,则添加到参数开头 if (dataDirectory && !hasDataDirectoryArg(args)) { - args.unshift(`--data-directory=${dataDirectory}`); + args.unshift(`--data-directory=${toCygwinPath(dataDirectory)}`); } return args; } @@ -252,12 +283,13 @@ export async function startWrapperFromEnv(): Promise { // 解析 gdb data-directory:优先使用配置值,其次自动探测 const configuredDataDir = normalizeConfiguredPath(process.env.GDB_DATA_DIRECTORY); const autoDataDir = configuredDataDir || resolveAutoDataDirectory(gdbPath); + const gdbDataDir = autoDataDir ? toCygwinPath(autoDataDir) : undefined; // ========== 构建启动参数 ========== const args = mode === 'external' ? ['--gdb', gdbPath, ...configuredArgs] // external 模式:传递 gdb 路径给 dapserver - : buildGdbModeArgs(configuredArgs, hasExplicitArgs, autoDataDir); // gdb 直连模式 + : buildGdbModeArgs(configuredArgs, hasExplicitArgs, gdbDataDir); // gdb 直连模式 const cwd = resolveWorkingDirectory(process.env.DAP_SERVER_CWD); diff --git a/packages/dsp-mod-debug/src/gdbDapWrapper/types.ts b/packages/dsp-mod-debug/src/gdbDapWrapper/types.ts index 0d882ff..cd80134 100644 --- a/packages/dsp-mod-debug/src/gdbDapWrapper/types.ts +++ b/packages/dsp-mod-debug/src/gdbDapWrapper/types.ts @@ -265,6 +265,25 @@ export interface GdbDapLaunchConfig { */ gdbDataDirectory?: string; + /** + * GDB 路径转换模式。 + * - auto: 自动判断。Windows 下默认按 Cygwin GDB 转换,Linux/macOS 保持原路径 + * - cygwin: 强制将 Windows 路径转换为 /cygdrive//... + * - native: 不做路径风格转换,适用于 Linux 或原生 Windows GDB + * @default 'auto' + */ + gdbPathMode?: 'auto' | 'cygwin' | 'native'; + + /** + * DSP 启动链路模式。 + * - manualLoad: wrapper 接管 launch/configurationDone,按 opencore -> file -> load 初始化目标 + * - gdbNative: 保持 GDB DAP 原生 launch 行为,由 GDB 在 configurationDone 后执行 run/start + * + * DSP/JTAG 场景推荐 manualLoad,避免 GDB DAP 原生 launch 与外部目标加载流程冲突。 + * @default 'manualLoad' when injectOpenCore is enabled in gdb mode + */ + dspLaunchMode?: 'manualLoad' | 'gdbNative'; + /** * 是否输出 DAP 透传流日志。 * 开启后会在调试输出面板显示所有 DAP 消息,便于诊断协议问题。 @@ -336,7 +355,7 @@ export interface GdbDapLaunchConfig { /** * 三步加载模式中,file 命令执行前的延迟(毫秒)。 - * 默认始终使用三步加载模式(file -> opencore -> load) + * 默认按手动验证链路执行三步加载(opencore -> file -> load ) * @default 0 */ fileCommandDelayMs?: number; diff --git a/packages/dsp-mod-debug/src/gdbDapWrapper/wrapper.ts b/packages/dsp-mod-debug/src/gdbDapWrapper/wrapper.ts index c72afe5..0395224 100644 --- a/packages/dsp-mod-debug/src/gdbDapWrapper/wrapper.ts +++ b/packages/dsp-mod-debug/src/gdbDapWrapper/wrapper.ts @@ -136,12 +136,42 @@ export class GdbDapWrapper { */ private launchConfig: GdbDapLaunchConfig = {}; + /** + * VS Code 已下发的源码断点缓存。 + * GDB 执行 file/load 时可能会移除之前的 pending 断点,因此加载后需要重放。 + */ + private readonly cachedSetBreakpoints = new Map>(); + + /** + * VS Code 原始 setBreakpoints 请求参数。 + * 用于把 GDB 返回的 pending/重放断点重新映射回用户断点模型。 + */ + private readonly pendingClientSetBreakpoints = new Map>(); + + /** 按源码位置记录 VS Code 侧断点 ID。 */ + private readonly clientBreakpointIdsByLocation = new Map(); + + /** GDB 重放后的真实断点 ID 到 VS Code 断点 ID 的映射。 */ + private readonly serverToClientBreakpointIds = new Map(); + + /** + * file/load 期间 GDB 会删除 pending 断点。 + * 这些事件是加载流程内部副作用,不应影响 VS Code 用户断点 UI。 + */ + private suppressBreakpointEventsDuringManualLoad = false; + /** * 内部请求 seq 序号分配器。 * 使用高位值(10亿起步)避免与客户端 seq 冲突。 */ private nextInternalSeq = 1_000_000_000; + /** + * wrapper 生成的用户断点 ID。 + * manualLoad 下初始断点先由 wrapper 返回 pending,加载后再映射到 GDB 真实断点。 + */ + private nextClientBreakpointId = 1; + /** * 本会话是否已注入过 opencore。 * 防止重复注入。 @@ -278,10 +308,26 @@ export class GdbDapWrapper { // 缓存配置供后续使用 this.launchConfig = (request.arguments ?? {}) as GdbDapLaunchConfig; this.opencoreInjected = false; + this.cachedSetBreakpoints.clear(); + this.pendingClientSetBreakpoints.clear(); + this.clientBreakpointIdsByLocation.clear(); + this.serverToClientBreakpointIds.clear(); + this.suppressBreakpointEventsDuringManualLoad = false; + this.nextClientBreakpointId = 1; + + if (request.command === 'launch' && this.shouldUseManualDspLaunch()) { + this.log('console', '[wrapper] Manual DSP launch mode enabled; launch will be handled by wrapper'); + this.sendSuccessResponse(request); + return; + } // beforeLaunch 时机注入 - if (request.command === 'launch' && this.resolveInjectPhase() === 'beforeLaunch') { - // 始终使用三步加载模式(file -> opencore -> load) + if ( + request.command === 'launch' && + this.shouldInjectOpenCore() && + this.resolveInjectPhase() === 'beforeLaunch' + ) { + // 按手动验证链路执行:opencore -> file -> load const loaded = await this.executeThreeStepLoad(); if (!loaded.success) { this.sendMessage({ @@ -296,8 +342,11 @@ export class GdbDapWrapper { } } + // 透传给 GDB 之前先做一次路径归一化 + const forwardedRequest = this.normalizeLaunchRequestForServer(request); + // 透传请求到 GDB - this.forwardToServer(request); + this.forwardToServer(forwardedRequest); return; } @@ -307,6 +356,32 @@ export class GdbDapWrapper { return; } + // ========== 源码断点路径处理 ========== + if (request.command === 'setBreakpoints') { + const forwardedRequest = this.normalizeSetBreakpointsRequestForServer(request); + this.cacheSetBreakpointsRequest(forwardedRequest); + if (this.shouldDeferSetBreakpointsUntilManualLoad()) { + this.sendDeferredSetBreakpointsResponse(request, forwardedRequest); + return; + } + this.trackClientSetBreakpointsRequest(forwardedRequest); + this.forwardToServer(forwardedRequest); + return; + } + + if (request.command === 'breakpointLocations') { + this.forwardToServer(this.normalizeSourcePathRequestForServer(request)); + return; + } + + if (request.command === 'evaluate' && this.shouldShortCircuitHoverEvaluate(request)) { + this.sendSuccessResponse(request, { + result: '', + variablesReference: 0, + }); + return; + } + // ========== 其他请求直接透传 ========== this.forwardToServer(request); } @@ -331,17 +406,22 @@ export class GdbDapWrapper { // 非 response 类型直接透传 if (message.type !== 'response') { - this.sendMessage(message); + if (this.shouldSuppressServerEvent(message)) { + this.log('console', '[wrapper] Suppressed breakpoint event during manual load'); + return; + } + this.sendMessage(this.normalizeServerMessageForClient(message)); return; } // ========== 响应匹配处理 ========== const response = message as DapResponse; + const pending = this.pendingInternalRequests.get(response.request_seq); if (!pending) { // 不是内部请求,直接透传给 VS Code - this.sendMessage(response); + this.sendMessage(this.normalizeServerMessageForClient(response)); return; } @@ -351,7 +431,7 @@ export class GdbDapWrapper { // 根据 consumeResponse 决定是否透传 if (!pending.consumeResponse) { - this.sendMessage(response); + this.sendMessage(this.normalizeServerMessageForClient(response)); } // 解析 Promise @@ -372,11 +452,16 @@ export class GdbDapWrapper { */ private async handleConfigurationDone(request: DapRequest): Promise { try { + if (this.shouldUseManualDspLaunch()) { + await this.handleManualDspConfigurationDone(request); + return; + } + const phase = this.resolveInjectPhase(); // ========== beforeConfigurationDone 时机注入 ========== - if (phase === 'beforeConfigurationDone') { - // 始终使用三步加载模式(file -> opencore -> load) + if (phase === 'beforeConfigurationDone' && this.shouldInjectOpenCore()) { + // 按手动验证链路执行:opencore -> file -> load const loaded = await this.executeThreeStepLoad(); if (!loaded.success) { this.sendMessage({ @@ -425,19 +510,21 @@ export class GdbDapWrapper { return; } - // 始终使用三步加载模式(file -> opencore -> load) - const loaded = await this.executeThreeStepLoad(); - if (!loaded.success) { - const errorResponse: DapResponse = { - seq: configDoneResp.seq, - type: 'response', - request_seq: request.seq, - command: request.command, - success: false, - message: loaded.message || 'Failed to execute three-step load', - }; - this.sendMessage(errorResponse); - return; + if (this.shouldInjectOpenCore()) { + // 按手动验证链路执行:opencore -> file -> load + const loaded = await this.executeThreeStepLoad(); + if (!loaded.success) { + const errorResponse: DapResponse = { + seq: configDoneResp.seq, + type: 'response', + request_seq: request.seq, + command: request.command, + success: false, + message: loaded.message || 'Failed to execute three-step load', + }; + this.sendMessage(errorResponse); + return; + } } this.sendMessage(configDoneResp); @@ -455,6 +542,37 @@ export class GdbDapWrapper { } } + /** + * DSP 手动加载模式下的 configurationDone 处理。 + * + * 不再把 configurationDone 转发给 GDB DAP,避免其按本地进程 launch 语义继续执行 run/start。 + */ + private async handleManualDspConfigurationDone(request: DapRequest): Promise { + const loaded = await this.executeThreeStepLoad(); + if (!loaded.success) { + this.sendMessage({ + seq: this.nextInternalSeq++, + type: 'response', + request_seq: request.seq, + command: request.command, + success: false, + message: loaded.message || 'Failed to execute three-step load', + }); + return; + } + + await this.ensureTargetStoppedForConfigurationDone(); + this.sendSuccessResponse(request); + + if (this.targetStopped) { + this.sendEvent('stopped', { + reason: 'entry', + threadId: 1, + allThreadsStopped: true, + }); + } + } + /** * 判断是否需要注入 opencore。 * @@ -498,6 +616,95 @@ export class GdbDapWrapper { return true; // 默认容错 } + /** + * 判断是否由 wrapper 接管 DSP launch/configurationDone。 + */ + private shouldUseManualDspLaunch(): boolean { + if (this.launchConfig.dspLaunchMode === 'manualLoad') { + return true; + } + if (this.launchConfig.dspLaunchMode === 'gdbNative') { + return false; + } + return this.launchConfig.dapServerMode === 'gdb' && this.launchConfig.injectOpenCore !== false; + } + + /** + * 临时屏蔽 hover 求值。 + * + * 当前 GDB/DSP 后端在源码 hover 场景下会收到大量试探表达式,例如空表达式、 + * 类型名和解引用表达式。部分表达式会导致 GDB 进程直接退出,因此先短路 hover, + * 保留变量面板、监视表达式和调试控制台的显式求值能力。 + */ + private shouldShortCircuitHoverEvaluate(request: DapRequest): boolean { + const args = (request.arguments ?? {}) as Record; + return args.context === 'hover'; + } + + /** + * manualLoad 初始化完成前延迟下发源码断点。 + * + * GDB 在 file 命令前没有符号表,提前 setBreakpoints 会产生 + * “No source file named ...” 噪声。此时先让 VS Code 保持 pending UI, + * 等 file/load 完成后再由 replayCachedSetBreakpoints 真正下发。 + */ + private shouldDeferSetBreakpointsUntilManualLoad(): boolean { + return this.shouldUseManualDspLaunch() && !this.opencoreInjected; + } + + /** + * 给 VS Code 返回延迟断点响应。 + */ + private sendDeferredSetBreakpointsResponse(originalRequest: DapRequest, forwardedRequest: DapRequest): void { + const args = (forwardedRequest.arguments ?? {}) as Record; + const response = this.buildDeferredSetBreakpointsResponse(originalRequest, args); + this.sendMessage(response); + } + + /** + * 构造 manualLoad 延迟断点的 pending 响应。 + */ + private buildDeferredSetBreakpointsResponse( + request: DapRequest, + args: Record, + ): DapResponse { + const source = args.source as Record | undefined; + const sourcePath = typeof source?.path === 'string' ? source.path : undefined; + const requestedBreakpoints = Array.isArray(args.breakpoints) ? args.breakpoints : []; + const requestedLines = Array.isArray(args.lines) ? args.lines : []; + + const breakpoints = requestedBreakpoints.map((item, index) => { + const requested = item && typeof item === 'object' ? item as Record : {}; + const line = typeof requested.line === 'number' + ? requested.line + : typeof requestedLines[index] === 'number' + ? requestedLines[index] as number + : undefined; + const id = sourcePath && typeof line === 'number' + ? this.getOrCreateClientBreakpointId(sourcePath, line) + : this.nextClientBreakpointId++; + + return { + id, + verified: false, + reason: 'pending', + ...(sourcePath ? { source: { ...(source ?? {}), path: sourcePath } } : {}), + ...(typeof line === 'number' ? { line } : {}), + }; + }); + + return this.normalizeProtocolPathsForClient({ + seq: this.nextInternalSeq++, + type: 'response', + request_seq: request.seq, + command: request.command, + success: true, + body: { + breakpoints, + }, + }) as DapResponse; + } + /** * 解析最终注入时机。 * @@ -539,6 +746,431 @@ export class GdbDapWrapper { return args ? `${command} ${args}` : command; } + /** + * 将 launch/attach 请求中的路径参数归一化为 GDB 可识别的格式。 + * + * @description + * 当前 GDB DAP 后端运行在 Cygwin 环境下时,Windows 风格路径会在 + * launch 请求阶段就被错误解析。因此需要在透传前先转换关键路径字段。 + * + * @param request - 原始请求 + * @returns 归一化后的请求 + */ + private normalizeLaunchRequestForServer(request: DapRequest): DapRequest { + if (request.type !== 'request') { + return request; + } + + const originalArgs = (request.arguments ?? {}) as Record; + const normalizedArgs: Record = { ...originalArgs }; + const pathKeys = ['program', 'cwd', 'coreFile', 'executable']; + + for (const key of pathKeys) { + const value = normalizedArgs[key]; + if (typeof value === 'string' && value.trim()) { + normalizedArgs[key] = this.toGdbPath(value); + } + } + + return { + ...request, + arguments: normalizedArgs, + }; + } + + /** + * 将 setBreakpoints 请求中的源码路径归一化为 GDB 可识别的格式。 + * + * @description + * VS Code 发送的 source.path 是 Windows 路径,Cygwin GDB 不能直接按 + * d:\... 解析源码文件,会导致源码断点长期 pending。 + * + * @param request - 原始 setBreakpoints 请求 + * @returns 归一化后的请求 + */ + private normalizeSourcePathRequestForServer(request: DapRequest): DapRequest { + const originalArgs = (request.arguments ?? {}) as Record; + const source = originalArgs.source as Record | undefined; + const sourcePath = source?.path; + + if (typeof sourcePath !== 'string' || !sourcePath.trim()) { + return request; + } + + return { + ...request, + arguments: { + ...originalArgs, + source: { + ...source, + path: this.toGdbPath(sourcePath), + }, + }, + }; + } + + private normalizeSetBreakpointsRequestForServer(request: DapRequest): DapRequest { + return this.normalizeSourcePathRequestForServer(request); + } + + /** + * 将 GDB 返回给 VS Code 的路径改回宿主 IDE 可打开的路径。 + */ + private normalizeServerMessageForClient(message: T): T { + if (message.type === 'response' && message.command === 'initialize' && message.body) { + return this.normalizeInitializeResponseForClient(message) as T; + } + if (message.type === 'response' && message.command === 'setBreakpoints') { + return this.normalizeSetBreakpointsResponseForClient(message) as T; + } + if (message.type === 'event') { + return this.normalizeServerEventForClient(message) as T; + } + return this.normalizeProtocolPathsForClient(message) as T; + } + + /** + * 收口初始化能力声明。 + * + * 当前 DSP GDB DAP 后端在 hover 求值时会收到大量试探表达式, + * 部分表达式会触发后端异常。这里主动关闭 hover 求值能力, + * 保留变量面板、监视表达式和调试控制台的显式求值。 + */ + private normalizeInitializeResponseForClient(response: DapResponse): DapResponse { + const body = { + ...(response.body ?? {}), + supportsEvaluateForHovers: false, + }; + return this.normalizeProtocolPathsForClient({ + ...response, + body, + }) as DapResponse; + } + + /** + * 补齐 setBreakpoints 响应中的源码位置,并记录 VS Code 侧断点 ID。 + * + * GDB 在 file/load 前返回 pending 断点时可能没有 source/line。 + * VS Code 需要这些信息维持断点面板和源码 gutter 的用户断点状态。 + */ + private normalizeSetBreakpointsResponseForClient(response: DapResponse): DapResponse { + const args = this.pendingClientSetBreakpoints.get(response.request_seq); + this.pendingClientSetBreakpoints.delete(response.request_seq); + + if (!args || !response.body) { + return this.normalizeProtocolPathsForClient(response) as DapResponse; + } + + const source = args.source as Record | undefined; + const sourcePath = typeof source?.path === 'string' ? source.path : undefined; + const requestedBreakpoints = Array.isArray(args.breakpoints) ? args.breakpoints : []; + const requestedLines = Array.isArray(args.lines) ? args.lines : []; + const breakpoints = Array.isArray(response.body.breakpoints) ? response.body.breakpoints : []; + + const normalizedBreakpoints = breakpoints.map((item, index) => { + const bp = item && typeof item === 'object' ? item as Record : {}; + const requested = requestedBreakpoints[index] as Record | undefined; + const line = typeof bp.line === 'number' + ? bp.line + : typeof requested?.line === 'number' + ? requested.line + : typeof requestedLines[index] === 'number' + ? requestedLines[index] as number + : undefined; + const id = typeof bp.id === 'number' ? bp.id : undefined; + + if (sourcePath && typeof line === 'number' && typeof id === 'number') { + this.getOrCreateClientBreakpointId(sourcePath, line); + } + + return { + ...bp, + ...(sourcePath ? { source: { ...(source ?? {}), path: sourcePath } } : {}), + ...(typeof line === 'number' ? { line } : {}), + }; + }); + + return this.normalizeProtocolPathsForClient({ + ...response, + body: { + ...response.body, + breakpoints: normalizedBreakpoints, + }, + }) as DapResponse; + } + + /** + * 对服务端事件做用户态断点 ID 与路径归一化。 + */ + private normalizeServerEventForClient(event: DapEvent): DapEvent { + if (event.event === 'breakpoint') { + return this.normalizeBreakpointEventForClient(event); + } + if (event.event === 'stopped') { + return this.normalizeStoppedEventForClient(event); + } + return this.normalizeProtocolPathsForClient(event) as DapEvent; + } + + private normalizeBreakpointEventForClient(event: DapEvent): DapEvent { + const body = event.body ?? {}; + const bp = body.breakpoint as Record | undefined; + if (!bp || typeof bp.id !== 'number') { + return this.normalizeProtocolPathsForClient(event) as DapEvent; + } + + const clientId = this.serverToClientBreakpointIds.get(bp.id) ?? bp.id; + return this.normalizeProtocolPathsForClient({ + ...event, + body: { + ...body, + breakpoint: { + ...bp, + id: clientId, + }, + }, + }) as DapEvent; + } + + private normalizeStoppedEventForClient(event: DapEvent): DapEvent { + const body = event.body ?? {}; + const hitBreakpointIds = Array.isArray(body.hitBreakpointIds) + ? body.hitBreakpointIds.map((id) => typeof id === 'number' ? this.serverToClientBreakpointIds.get(id) ?? id : id) + : undefined; + + if (!hitBreakpointIds) { + return this.normalizeProtocolPathsForClient(event) as DapEvent; + } + + return this.normalizeProtocolPathsForClient({ + ...event, + body: { + ...body, + hitBreakpointIds, + }, + }) as DapEvent; + } + + /** + * 判断服务端事件是否应在手动加载阶段被屏蔽。 + */ + private shouldSuppressServerEvent(message: DapMessage): boolean { + if (!this.suppressBreakpointEventsDuringManualLoad || message.type !== 'event') { + return false; + } + return message.event === 'breakpoint'; + } + + private normalizeProtocolPathsForClient(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => this.normalizeProtocolPathsForClient(item)); + } + if (!value || typeof value !== 'object') { + return value; + } + + const normalized: Record = {}; + for (const [key, item] of Object.entries(value as Record)) { + if (typeof item === 'string' && this.isPathLikeProtocolKey(key)) { + normalized[key] = this.fromGdbPath(item); + continue; + } + normalized[key] = this.normalizeProtocolPathsForClient(item); + } + return normalized; + } + + private isPathLikeProtocolKey(key: string): boolean { + return key === 'path' || key === 'moduleId' || key === 'id'; + } + + /** + * 缓存源码断点请求,供 file/load 完成后重放。 + */ + private cacheSetBreakpointsRequest(request: DapRequest): void { + const args = (request.arguments ?? {}) as Record; + const source = args.source as Record | undefined; + const sourcePath = source?.path; + const sourceName = source?.name; + const key = typeof sourcePath === 'string' && sourcePath.trim() + ? sourcePath + : typeof sourceName === 'string' && sourceName.trim() + ? sourceName + : `source-${this.cachedSetBreakpoints.size + 1}`; + + this.cachedSetBreakpoints.set(key, this.cloneDapArguments(args)); + this.log('console', `[wrapper] Cached source breakpoints for replay: ${key}`); + } + + /** + * 记录 VS Code setBreakpoints 请求,等待 GDB 响应时补齐用户断点信息。 + */ + private trackClientSetBreakpointsRequest(request: DapRequest): void { + const args = (request.arguments ?? {}) as Record; + this.pendingClientSetBreakpoints.set(request.seq, this.cloneDapArguments(args)); + } + + /** + * 获取或创建 VS Code 侧用户断点 ID。 + */ + private getOrCreateClientBreakpointId(sourcePath: string, line: number): number { + const key = this.breakpointLocationKey(sourcePath, line); + const existing = this.clientBreakpointIdsByLocation.get(key); + if (existing !== undefined) { + return existing; + } + const id = this.nextClientBreakpointId++; + this.clientBreakpointIdsByLocation.set(key, id); + return id; + } + + /** + * 重放缓存的源码断点。 + * + * @description + * GDB 的 file/load 会让 pending 断点先 verified、随后 removed。 + * 为了保持 VS Code 用户断点有效,需要在加载完成后重新发送 setBreakpoints。 + */ + private async replayCachedSetBreakpoints(): Promise { + if (this.cachedSetBreakpoints.size === 0) { + return; + } + + this.log('console', `[wrapper] Replaying ${this.cachedSetBreakpoints.size} cached source breakpoint request(s)`); + for (const [key, args] of this.cachedSetBreakpoints) { + try { + const resp = await this.sendInternalRequest('setBreakpoints', this.cloneDapArguments(args), 30_000); + if (!resp.success) { + this.log('stderr', `[wrapper] replay setBreakpoints failed for ${key}: ${resp.message || 'unknown error'}`); + continue; + } + this.syncReplayedBreakpointsToClient(resp, args); + this.log('console', `[wrapper] Replayed source breakpoints: ${key}`); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + this.log('stderr', `[wrapper] replay setBreakpoints exception for ${key}: ${msg}`); + } + } + } + + /** + * 将重放后的 GDB 真实断点同步回 VS Code 用户断点模型。 + */ + private syncReplayedBreakpointsToClient(resp: DapResponse, args: Record): void { + const source = args.source as Record | undefined; + const sourcePath = typeof source?.path === 'string' ? source.path : undefined; + const requestedBreakpoints = Array.isArray(args.breakpoints) ? args.breakpoints : []; + const requestedLines = Array.isArray(args.lines) ? args.lines : []; + const breakpoints = Array.isArray(resp.body?.breakpoints) ? resp.body.breakpoints : []; + + breakpoints.forEach((item, index) => { + const bp = item && typeof item === 'object' ? item as Record : {}; + const requested = requestedBreakpoints[index] as Record | undefined; + const line = typeof bp.line === 'number' + ? bp.line + : typeof requested?.line === 'number' + ? requested.line + : typeof requestedLines[index] === 'number' + ? requestedLines[index] as number + : undefined; + const serverId = typeof bp.id === 'number' ? bp.id : undefined; + const clientId = sourcePath && typeof line === 'number' + ? this.clientBreakpointIdsByLocation.get(this.breakpointLocationKey(sourcePath, line)) ?? serverId + : serverId; + + if (typeof serverId === 'number' && typeof clientId === 'number') { + this.serverToClientBreakpointIds.set(serverId, clientId); + } + + this.sendEvent('breakpoint', this.normalizeProtocolPathsForClient({ + reason: 'changed', + breakpoint: { + ...bp, + ...(typeof clientId === 'number' ? { id: clientId } : {}), + ...(sourcePath ? { source: { ...(source ?? {}), path: sourcePath } } : {}), + ...(typeof line === 'number' ? { line } : {}), + }, + }) as Record); + }); + } + + private breakpointLocationKey(sourcePath: string, line: number): string { + return `${sourcePath.replace(/\\/g, '/').toLowerCase()}:${line}`; + } + + /** + * 复制 DAP 参数,避免后续重放时被外部对象意外修改。 + */ + private cloneDapArguments(args: Record): Record { + return JSON.parse(JSON.stringify(args)) as Record; + } + + /** + * 将 VS Code 传入的 Windows 路径转换为 GDB 更稳定识别的路径格式。 + */ + private toGdbPath(filePath: string): string { + const normalized = filePath.trim().replace(/\\/g, '/'); + if (!normalized) { + return normalized; + } + if (normalized.startsWith('/')) { + return normalized; + } + const match = normalized.match(/^([a-zA-Z]):\/(.*)$/); + if (!match) { + return normalized; + } + if (!this.shouldUseCygwinPathMode()) { + return normalized; + } + const drive = match[1].toLowerCase(); + const rest = match[2]; + return `/cygdrive/${drive}/${rest}`; + } + + /** + * 将 GDB/Cygwin 路径转换为 VS Code 所在宿主系统可打开的路径。 + */ + private fromGdbPath(filePath: string): string { + const normalized = filePath.trim(); + const match = normalized.match(/^\/cygdrive\/([a-zA-Z])\/(.*)$/); + if (!match) { + return filePath; + } + const drive = match[1].toUpperCase(); + const rest = match[2].replace(/\//g, '\\'); + return `${drive}:\\${rest}`; + } + + /** + * 判断当前 GDB 是否需要 Cygwin 路径格式。 + * + * @description + * Windows 下的 Cygwin GDB 需要 /cygdrive/d/...;Linux 或原生 GDB + * 应保持原路径,避免把跨平台适配写死成 Windows 规则。 + */ + private shouldUseCygwinPathMode(): boolean { + const mode = this.launchConfig.gdbPathMode; + if (mode === 'cygwin') { + return true; + } + if (mode === 'native') { + return false; + } + + const gdbPath = [ + this.launchConfig.dapServerPath, + this.launchConfig.gdbPath, + this.launchConfig.gdb, + ].find((value): value is string => typeof value === 'string' && value.trim().length > 0); + + if (gdbPath && /(?:^|[\\/])cyg(?:win)?(?:[\\/]|$)/i.test(gdbPath)) { + return true; + } + + return process.platform === 'win32'; + } + /** * 注入 opencore evaluate 命令。 * @@ -578,14 +1210,13 @@ export class GdbDapWrapper { } /** - * 执行三步加载流程:file -> opencore -> load。 + * 执行三步加载流程:opencore -> file -> load 。 * * @description * 这个方法按顺序执行三个 GDB 命令来完整初始化程序加载: - * 1. file - 加载程序符号和调试信息 - * 2. opencore - 初始化多核环境 - * 3. [确保停止] - pause 所有核并等待 stopped 事件 - * 4. load - 将程序二进制加载到目标内存 + * 1. opencore - 初始化多核环境 + * 2. file - 加载程序符号和调试信息 + * 3. load - 将程序二进制加载到目标内存 * * 三步模式相比单纯的 opencore 注入提供: * - 更完整的初始化流程,确保程序完全加载 @@ -606,77 +1237,53 @@ export class GdbDapWrapper { }; } - this.log('console', `[wrapper] Executing three-step load: file -> opencore -> load`); - this.log('console', `[wrapper] Program: ${programPath}`); + const gdbProgramPath = this.toGdbPath(programPath); + this.log('console', `[wrapper] Executing three-step load: opencore -> file -> load `); + this.log('console', `[wrapper] Program: ${gdbProgramPath}`); - // ========== 第一步:file 命令 ========== - const fileDelayMs = Number(this.launchConfig.fileCommandDelayMs || 0); - if (fileDelayMs > 0) { - this.log('console', `[wrapper] Waiting ${fileDelayMs}ms before file command`); - await new Promise((resolve) => setTimeout(resolve, fileDelayMs)); - } - - this.log('console', `[wrapper] Step 1/3: Executing file command`); - const fileResp = await this.sendInternalRequest('evaluate', { - expression: `file "${programPath}"`, - context: 'repl', - }, 120_000); - - if (!fileResp.success) { - return { - success: false, - message: `file command failed: ${fileResp.message || 'unknown error'}`, - }; - } - this.log('console', `[wrapper] Step 1/3 completed: file command succeeded`); - - // ========== 第二步:opencore 命令 ========== + // ========== 复合命令:opencore + file + load ========== + // GDB DAP 在 opencore 之后会把会话视为 running,后续单独 evaluate + // 会被拒绝为 notStopped,因此这里必须在同一次 evaluate 中完成加载链路。 const opencoreDelayMs = Number(this.launchConfig.opencoreCommandDelayMs || 0); if (opencoreDelayMs > 0) { this.log('console', `[wrapper] Waiting ${opencoreDelayMs}ms before opencore command`); await new Promise((resolve) => setTimeout(resolve, opencoreDelayMs)); } - this.log('console', `[wrapper] Step 2/3: Executing opencore command`); - const opencoreExpr = this.buildOpenCoreExpression(); - const opencoreResp = await this.sendInternalRequest('evaluate', { - expression: opencoreExpr, - context: 'repl', - }, 120_000); + const expression = [ + 'set confirm off', + this.buildOpenCoreExpression(), + `file ${gdbProgramPath}`, + `load ${gdbProgramPath}`, + ].join('\n'); - if (!opencoreResp.success) { - return { - success: false, - message: `opencore command failed: ${opencoreResp.message || 'unknown error'}`, - }; - } - this.log('console', `[wrapper] Step 2/3 completed: opencore command succeeded`); - - // ========== 第二步半:确保目标处于 stopped 状态 ========== - // load 命令必须在目标 stopped 时执行,否则返回 notStopped 错误 - this.log('console', `[wrapper] Ensuring target stopped before load command`); - await this.ensureTargetStoppedForConfigurationDone(); - - // ========== 第三步:load 命令 ========== - const loadDelayMs = Number(this.launchConfig.loadCommandDelayMs || 0); - if (loadDelayMs > 0) { - this.log('console', `[wrapper] Waiting ${loadDelayMs}ms before load command`); - await new Promise((resolve) => setTimeout(resolve, loadDelayMs)); - } - - this.log('console', `[wrapper] Step 3/3: Executing load command`); + this.log('console', `[wrapper] Step 1/1: Executing combined opencore/file/load command`); + this.suppressBreakpointEventsDuringManualLoad = true; const loadResp = await this.sendInternalRequest('evaluate', { - expression: 'load', + expression, context: 'repl', - }, 120_000); + }, 180_000); if (!loadResp.success) { + this.suppressBreakpointEventsDuringManualLoad = false; return { success: false, - message: `load command failed: ${loadResp.message || 'unknown error'}`, + message: `combined load command failed: ${loadResp.message || 'unknown error'}`, }; } - this.log('console', `[wrapper] Step 3/3 completed: load command succeeded`); + + const result = typeof loadResp.body?.result === 'string' ? loadResp.body.result : ''; + if (result.includes('DSPIDE:PROGRESS:ERROR:ConnectFailed')) { + this.suppressBreakpointEventsDuringManualLoad = false; + return { + success: false, + message: 'opencore command failed: ConnectFailed', + }; + } + this.log('console', `[wrapper] Combined opencore/file/load command completed`); + await this.replayCachedSetBreakpoints(); + this.suppressBreakpointEventsDuringManualLoad = false; + await this.markDspTargetStoppedInServer(); // 标记已注入(用于防止重复注入) this.opencoreInjected = true; @@ -684,6 +1291,7 @@ export class GdbDapWrapper { this.log('console', `[wrapper] Three-step load completed successfully`); return { success: true }; } catch (error) { + this.suppressBreakpointEventsDuringManualLoad = false; const msg = error instanceof Error ? error.message : String(error); return { success: false, @@ -714,7 +1322,7 @@ export class GdbDapWrapper { this.log('console', '[wrapper] Target not stopped after opencore, sending pause requests'); // 发送全局 pause - await this.sendBestEffortPause(); + let pauseAccepted = await this.sendBestEffortPause(); // 可选:逐线程 pause if (this.shouldPauseAllThreadsBeforeConfigurationDone()) { @@ -723,13 +1331,18 @@ export class GdbDapWrapper { this.log('console', `[wrapper] Sending per-thread pause for ${threadIds.length} threads`); } for (const threadId of threadIds) { - await this.sendBestEffortPause(threadId); + pauseAccepted = (await this.sendBestEffortPause(threadId)) || pauseAccepted; } } - // 等待 stopped 事件 - const stopped = await this.waitForStoppedEvent(15_000); + // 等待 stopped 事件。当前后端可能只返回 pause success,不额外发 stopped 事件。 + const stopped = await this.waitForStoppedEvent(1_500); if (!stopped) { + if (pauseAccepted) { + this.targetStopped = true; + this.log('console', '[wrapper] pause accepted without stopped event; continuing as stopped'); + return; + } this.log('stderr', '[wrapper] wait for stopped timed out, configurationDone may fail with notStopped'); } else { this.log('console', '[wrapper] Received stopped state before configurationDone'); @@ -773,7 +1386,7 @@ export class GdbDapWrapper { * * @param threadId - 可选的线程 ID,不指定则为全局 pause */ - private async sendBestEffortPause(threadId?: number): Promise { + private async sendBestEffortPause(threadId?: number): Promise { const args = threadId !== undefined ? { threadId } : {}; try { const pauseResp = await this.sendInternalRequest('pause', args, 10_000); @@ -782,13 +1395,37 @@ export class GdbDapWrapper { 'stderr', `[wrapper] pause request failed${threadId !== undefined ? ` (threadId=${threadId})` : ''}: ${pauseResp.message || 'unknown error'}`, ); + return false; } + return true; } catch (error) { const msg = error instanceof Error ? error.message : String(error); this.log( 'stderr', `[wrapper] pause request exception${threadId !== undefined ? ` (threadId=${threadId})` : ''}: ${msg}`, ); + return false; + } + } + + /** + * 同步 GDB DAP 内部 running/stopped 状态。 + * + * DSP 的 load 命令完成后目标实际处于等待调试器状态,但 GDB DAP 只看到 new_thread, + * 没有收到标准 stop 事件,因此需要通过内部请求把 DAP 状态标记为 stopped。 + */ + private async markDspTargetStoppedInServer(): Promise { + try { + const resp = await this.sendInternalRequest('dspMarkStopped', { reason: 'entry' }, 10_000); + if (!resp.success) { + this.log('stderr', `[wrapper] dspMarkStopped request failed: ${resp.message || 'unknown error'}`); + return; + } + this.targetStopped = true; + this.log('console', '[wrapper] Marked DSP target as stopped in GDB DAP server'); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + this.log('stderr', `[wrapper] dspMarkStopped request exception: ${msg}`); } } @@ -968,6 +1605,20 @@ export class GdbDapWrapper { process.stdout.write(DapFraming.encode(message)); } + /** + * 向 VS Code 返回成功响应。 + */ + private sendSuccessResponse(request: DapRequest, body?: Record): void { + this.sendMessage({ + seq: this.nextInternalSeq++, + type: 'response', + request_seq: request.seq, + command: request.command, + success: true, + ...(body ? { body } : {}), + }); + } + /** * 发送 DAP 事件。 * @@ -1115,4 +1766,4 @@ export class GdbDapWrapper { this.dapServer = null; } -} \ No newline at end of file +}