完善 GDB-DAP 同步调试链路与测试模板

This commit is contained in:
rain 2026-05-19 09:50:17 +08:00
parent c9f54b5d52
commit 2b8b1181eb
11 changed files with 915 additions and 97 deletions

3
.gitignore vendored
View File

@ -10,6 +10,9 @@ dist/
*.log
npm-debug.log*
# Local temporary analysis/test artifacts
.tmp/
# Local script state
scripts/.local-state/

View File

@ -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.

View File

@ -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 协议迁移
- [ ] 原项目管理界面接入
- [ ] 用户态安装流程升级

View File

@ -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)

View File

@ -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
}
]
}

View File

@ -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)

View File

@ -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,

View File

@ -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 模式必须指定要调试的程序文件

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
}
}