完善 GDB-DAP 同步调试链路与测试模板
This commit is contained in:
parent
c9f54b5d52
commit
2b8b1181eb
|
|
@ -10,6 +10,9 @@ dist/
|
|||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Local temporary analysis/test artifacts
|
||||
.tmp/
|
||||
|
||||
# Local script state
|
||||
scripts/.local-state/
|
||||
|
||||
|
|
|
|||
10
AGENTS.md
10
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.
|
||||
|
|
|
|||
|
|
@ -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 协议迁移
|
||||
- [ ] 原项目管理界面接入
|
||||
- [ ] 用户态安装流程升级
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
- [VS Code 调试扩展指南](https://code.visualstudio.com/api/extension-guides/debugging-extension)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
- [GDB DAP 文档](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debug-Adapter-Protocol.html)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 模式必须指定要调试的程序文件
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
// 解析 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -265,6 +265,25 @@ export interface GdbDapLaunchConfig {
|
|||
*/
|
||||
gdbDataDirectory?: string;
|
||||
|
||||
/**
|
||||
* GDB 路径转换模式。
|
||||
* - auto: 自动判断。Windows 下默认按 Cygwin GDB 转换,Linux/macOS 保持原路径
|
||||
* - cygwin: 强制将 Windows 路径转换为 /cygdrive/<drive>/...
|
||||
* - 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 <program>)
|
||||
* @default 0
|
||||
*/
|
||||
fileCommandDelayMs?: number;
|
||||
|
|
|
|||
|
|
@ -136,12 +136,42 @@ export class GdbDapWrapper {
|
|||
*/
|
||||
private launchConfig: GdbDapLaunchConfig = {};
|
||||
|
||||
/**
|
||||
* VS Code 已下发的源码断点缓存。
|
||||
* GDB 执行 file/load 时可能会移除之前的 pending 断点,因此加载后需要重放。
|
||||
*/
|
||||
private readonly cachedSetBreakpoints = new Map<string, Record<string, unknown>>();
|
||||
|
||||
/**
|
||||
* VS Code 原始 setBreakpoints 请求参数。
|
||||
* 用于把 GDB 返回的 pending/重放断点重新映射回用户断点模型。
|
||||
*/
|
||||
private readonly pendingClientSetBreakpoints = new Map<number, Record<string, unknown>>();
|
||||
|
||||
/** 按源码位置记录 VS Code 侧断点 ID。 */
|
||||
private readonly clientBreakpointIdsByLocation = new Map<string, number>();
|
||||
|
||||
/** GDB 重放后的真实断点 ID 到 VS Code 断点 ID 的映射。 */
|
||||
private readonly serverToClientBreakpointIds = new Map<number, number>();
|
||||
|
||||
/**
|
||||
* 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 <program>
|
||||
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<void> {
|
||||
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 <program>
|
||||
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 <program>
|
||||
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<void> {
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
const response = this.buildDeferredSetBreakpointsResponse(originalRequest, args);
|
||||
this.sendMessage(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 manualLoad 延迟断点的 pending 响应。
|
||||
*/
|
||||
private buildDeferredSetBreakpointsResponse(
|
||||
request: DapRequest,
|
||||
args: Record<string, unknown>,
|
||||
): DapResponse {
|
||||
const source = args.source as Record<string, unknown> | 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<string, unknown> : {};
|
||||
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<string, unknown>;
|
||||
const normalizedArgs: Record<string, unknown> = { ...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<string, unknown>;
|
||||
const source = originalArgs.source as Record<string, unknown> | 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<T extends DapMessage>(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<string, unknown> | 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<string, unknown> : {};
|
||||
const requested = requestedBreakpoints[index] as Record<string, unknown> | 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<string, unknown> | 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<string, unknown> = {};
|
||||
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
||||
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<string, unknown>;
|
||||
const source = args.source as Record<string, unknown> | 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<string, unknown>;
|
||||
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<void> {
|
||||
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<string, unknown>): void {
|
||||
const source = args.source as Record<string, unknown> | 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<string, unknown> : {};
|
||||
const requested = requestedBreakpoints[index] as Record<string, unknown> | 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<string, unknown>);
|
||||
});
|
||||
}
|
||||
|
||||
private breakpointLocationKey(sourcePath: string, line: number): string {
|
||||
return `${sourcePath.replace(/\\/g, '/').toLowerCase()}:${line}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制 DAP 参数,避免后续重放时被外部对象意外修改。
|
||||
*/
|
||||
private cloneDapArguments(args: Record<string, unknown>): Record<string, unknown> {
|
||||
return JSON.parse(JSON.stringify(args)) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 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 <program>。
|
||||
*
|
||||
* @description
|
||||
* 这个方法按顺序执行三个 GDB 命令来完整初始化程序加载:
|
||||
* 1. file <program> - 加载程序符号和调试信息
|
||||
* 2. opencore <args> - 初始化多核环境
|
||||
* 3. [确保停止] - pause 所有核并等待 stopped 事件
|
||||
* 4. load - 将程序二进制加载到目标内存
|
||||
* 1. opencore <args> - 初始化多核环境
|
||||
* 2. file <program> - 加载程序符号和调试信息
|
||||
* 3. load <program> - 将程序二进制加载到目标内存
|
||||
*
|
||||
* 三步模式相比单纯的 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 <program>`);
|
||||
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<void>((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<void>((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<void>((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<void> {
|
||||
private async sendBestEffortPause(threadId?: number): Promise<boolean> {
|
||||
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<void> {
|
||||
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<string, unknown>): 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue