ArkTS Debug Adapter —— DAP↔CDP 桥接,让任意 DAP 客户端(VSCode、nvim-dap 等) 断点调试 ArkTS 应用。
ArkTS 应用跑在 Ark JS 运行时上,运行时内置一个 CDP(Chrome DevTools Protocol 风格) 调试器,
以 WebSocket 暴露(previewer/应用以 -d -p <port> 启动时即在 127.0.0.1:<port> 开 raw CDP 服务)。
但编辑器/IDE 说的是 DAP(Debug Adapter Protocol)。本工具在两者之间做翻译,
即业界成熟的 DAP↔CDP 桥接模式(同 vscode-js-debug)。
它也补齐了 previewer-frontend 的缺口:rich Inspector 的实时组件树
(inspector 命令)需要有调试器 attach 到运行时才能产出。
VSCode 用户:装
vscode-extension/(ArkTS Debug),在编辑器里按 F5 即可, 无需命令行 —— 它把 launch.json 配置映射成本工具的参数并经 stdio 启动。
DAP 客户端 (VSCode...) ──DAP(stdio/TCP)──► arkts-dap ──CDP(WebSocket)──► Ark 运行时调试器
(翻译器) (previewer -d -p <port>)
cargo build --release
# 1) previewer / 本地 CDP 端口(DAP over stdio)
arkts-dap --cdp-port 29900
# 2) 真机 attach(hdc 全自动:aa attach + ConnectServer 发现 + fport → CDP)
arkts-dap --device com.huawei.hmos.sample # 一键 attach 真机已运行 app(主线程+worker 自动发现)
arkts-dap --device <bundle> --serial <设备序列号> # 多设备时指定
arkts-dap --device <bundle> --no-attach # app 已在调试模式则跳过 aa attach
# 3) 真机 launch(适配器用 aa start -D 拉起 app 进调试,免手动先起)
arkts-dap --device <bundle> --launch --ability EntryAbility --module phone
# 可选 --hap:先装包再拉起(同 DevEco:force-stop + bm install + aa start -D)
arkts-dap --device <bundle> --launch --ability EntryAbility --module phone \
--hap .../outputs/default/phone-default-signed.hap
# 4) DAP over TCP(自测)
arkts-dap --tcp 4711 --cdp-port 29900 --logattach 目标:previewer 端口(-d -p)或真机(--device)。--cdp-port 为默认;
也可由 DAP attach 请求的 cdpPort 覆盖。
--device <bundle> 自动完成:
hdc list targets选设备hdc shell aa attach -b <bundle>让 app 进调试模式hdc shell pidof <bundle>取 pidhdc fport tcp:<本地空闲端口> ark:<pid>@Debugger转发设备调试 socket- 连
ws://127.0.0.1:<port>/raw CDP(与 previewer 同协议) - 退出(disconnect / 断开 / SIGINT/SIGTERM)时
hdc fport rm <local> <target>(两参,单参会 "ruler not exist" 清不掉)清理
健壮性:CDP 连接断开(设备拔出 / app 崩溃)→ 主线程断发 DAP
terminated并结束会话、worker 断发threadexited; in-flight CDP 调用在断开时立即失败(不等 15s 超时);信号退出经显式cleanup()(process::exit不跑 Drop)。
设备准备延迟到 launch/attach 请求:
initialize秒回(实测 0.17s),force-stop/装包/aa start -D/fport 放在 launch 请求阶段(spawn_blocking),VSCode 期间显示"正在启动",不会因准备耗时误判适配器无响应。
Ark 调试 socket 约定(来自 hdc fport ls,真机确认):
ark:<pid>@<bundle>(ConnectServer,多实例发现)、ark:<pid>@Debugger(主线程 raw CDP,默认)、
ark:<pid>@<tid>@Debugger(worker 实例)。hdc 在 ~/Library/OpenHarmony/Sdk/*/toolchains/hdc
或 DevEco SDK 下,可用 --hdc <path> / HDC_PATH 指定。
实测(com.huawei.hmos.sample 真机):attach 后 Debugger.enable 回放 578 个真实脚本;
pause 暂停活动 app → 真实调用栈(TrackManager.ts:112)+ 真实局部变量(_event/node)+ continue。
前提:app 须为 debuggable(debug 签名)构建。未执行到的脚本断点为 pending(
verified:false), 待该脚本运行时经Debugger.breakpointResolved自动 verify(标准行为)。
--launch --ability <Ability> [--module <m>] [--hap <signed.hap>]:适配器自己把 app
拉起进调试模式(-D=启动即阻塞等调试器),无需手动先起 app。流程(同 DevEco 的 install→launch):
aa force-stop <bundle>(干净重启,确保-D可靠阻塞在入口)- (
--hap)按需装包:bm dump -n判已装 + 比对 hap 指纹(size+mtime,不读内容)与本地缓存~/.cache/arkts-dap/install-<bundle>-<serial>.txt—— 已装且 hap 未变则跳过bm install(省去每次传 300MB+); 否则mkdir tmp→file send→bm install -p→rm tmp并刷新缓存。设备上未装(卸载/刷机)必装。 强制重装:touch <hap>或删该缓存文件。 aa start -b <bundle> -a <Ability> [-m <m>] -D拉起 → app 阻塞在入口- 连 ConnectServer,等主线程实例连上(关键:否则
configurationDone先于实例连接、漏发runIfWaitingForDebugger,app 永久阻塞) configurationDone广播runIfWaitingForDebugger放行(launch 下attach_mode=false)stopOnEntry则停在第一条指令(实测停在CommonEnums.ets:1),否则自动跨过启动期的 break-on-start
最早期生命周期断点(onCreate 等)已稳定命中 ✅(实测 6/6)。关键机制:
Debugger.enable带options:["enableLaunchAccelerate"]→ 开启 LaunchAccelerate:启用saveAllPossibleBreakpoints(pending 断点池,脚本加载时自动解析命中)+ 关闭逐模块 break-on-start 风暴。setBreakpoints双轨下发:已加载脚本用getPossibleAndSetBreakpointByUrl(立即 verified), 全量saveAllPossibleBreakpoints(覆盖未加载脚本的早期断点)。- launch 前
force-stop并轮询 pidof 直到进程真正退出,确保-D可靠阻塞在入口(否则 onCreate 已执行必漏)。
Ark 断点限制:只能给已加载脚本即时下断点(
setBreakpointByUrl对未知 url 报 "Unknown file name"、getPossibleAndSetBreakpointByUrl对未加载返回id:"invalid");saveAllPossibleBreakpoints是其 pending 池(仅 LaunchAccelerate 下可用,全量覆盖语义,对已加载脚本不即时解析)。
VSCode launch.json(attach)示例:
{ "type": "arkts", "request": "attach", "name": "Attach ArkTS", "cdpPort": 29900 }attach(previewer / 真机)· 断点(普通/条件,智能吸附 getPossibleAndSetBreakpointByUrl)·
logpoint(logMessage,{expr} 插值,命中输出不中断)· hitCondition 断点(>n/>=n/==n/<n/%n/裸数字,未满足自动放行)·
pending 断点 + breakpointResolved→DAP breakpoint 事件 · 异常断点(all/uncaught)+ exceptionInfo ·
worker 多实例(ConnectServer 自动发现主线程+worker,多线程,断点广播,按 threadId 路由栈/变量/单步)·
暂停/继续 · 单步(over/into/out,stepIn 用 smartStepInto 跳过库/getter 帧)· restartFrame(dropFrame 回退顶帧重执行)·
调用栈(分页 startFrame/levels)· 作用域/变量(对象展开)· setVariable ·
evaluate/watch(clipboard 经 callFunctionOn 取完整值)· CPU Profiler(.cpuprofile)/ Heap Snapshot(.heapsnapshot)·
console 输出转发(Runtime.consoleAPICalled/exceptionThrown→DAP output)·
sourcemap 行/列映射(编译 .ts 坐标 ↔ 源码 .ets,双向,--source-root 自动探索)·
源文件定位(记录名 → 磁盘 .ets 路径)· 源码获取 · stopOnEntry · attach-mode(不发 runIfWaitingForDebugger)。
ArkTS 主线程与每个 Worker/TaskPool 子线程各跑独立 VM、各有独立 CDP 调试器。真机 --device 下,
适配器连 ConnectServer(ark:<pid>@<bundle>,发 {"type":"connected"})枚举实例:
- 主线程
addInstance{instanceId:0, tid:pid}→ark:<pid>@Debugger→ DAP thread 1 - worker
addInstance{instanceId, tid, name:"workerThread_<tid>"}→ark:<pid>@<tid>@Debugger→ DAP thread 2/3/… destroyInstance→ 关连接、DAPthreadexited;运行中新建的 worker 动态threadstarted 并补设已有断点。
每个实例一条 CDP 连接;断点 setBreakpoints 广播到所有实例(各自 resolve、各存 cdp_id);
stackTrace/scopes/variables/evaluate/单步/continue/pause 按 threadId 路由到对应实例。
(实测 com.huawei.hmos.sample:主线程 + workerThread_* 并列为多 DAP 线程,暂停 worker 得其独立调用栈
func_main_0 @ Logger.ets:16,断点广播到主线程与 worker 两条连接。)
真机/打包应用的运行时调试信息用 编译后 .ts 坐标,脚本 url 是归一化记录名
(如 phone|@ohos/common|1.0.0|src/main/ets/trackmanager/TrackManager.ts),且设备不支持 getScriptSource。
要把它对回编辑器里的 .ets,需要分别解决「哪个文件」和「哪一行」——这是两件不同的事:
-
源文件定位(
source.rs):记录名 → 磁盘.ets路径。扫描工程建「模块名/包名→目录」索引。 -
行/列映射(
sourcemap.rs):编译.ts行/列 ↔ 源码.ets行/列。必须用 sourcemap—— 运行时报告的是编译行,与.ets相差一个可变偏移(实测TrackManager:运行时 185 行 ↔ 源码 212 行,差 27 行; 各处偏移不同)。读工程的**/intermediates/source_map/default/sourceMaps.map(标准 Source Map v3), VLQ 解码后双向用:- 调用栈/断点回显:
gen→.ets(显示正确源码行) - 下断点:
.ets→gen(把用户打的.ets行换成编译行再发给运行时)
多 product(phone/pc/tv)共享同一
.ets源,下断点时按真机当前 product(scriptParsed见过的 url 前缀)择优。 传--source-root <工程根>自动探索并加载sourceMaps.map;也可--source-map <文件>显式指定。 - 调用栈/断点回显:
无对应 sourcemap 条目的模块(previewer 本地编译:url 本身即 .ets 绝对路径、行号已是 .ets 基准)→
自动回退恒等(路径解析 + 行号原样),previewer 流程不受影响。
Ark 支持 Profiler.*(CPU 采样)与 HeapProfiler.*(堆快照),适配器经自定义 DAP 请求暴露
(VSCode 扩展用命令触发):
| customRequest | 动作 | 产物 |
|---|---|---|
startCpuProfile {threadId?,interval?} |
Profiler.enable+setSamplingInterval+start |
—(开始采样) |
stopCpuProfile {threadId?,path?} |
Profiler.stop → 写文件 |
.cpuprofile(VSCode 内置火焰图查看器) |
takeHeapSnapshot {threadId?,path?} |
HeapProfiler.takeHeapSnapshot(收 addHeapSnapshotChunk 流)→ 写文件 |
.heapsnapshot(Chrome DevTools → Memory 加载) |
path 缺省写到临时目录。实测真机 com.huawei.hmos.sample:CPU profile nodes=216 samples=11680(143 KB),
堆快照 30 MB(合法 Chrome 格式 {"snapshot":{"meta":…)。
src/
├── main.rs 入口:clap;DAP over stdio 或 --tcp;--device 真机模式;请求循环
├── dap/transport.rs Content-Length 帧编解码(同 LSP)
├── cdp/client.rs CDP WS 客户端:call(method,params) 关联应答;事件 channel
├── device.rs 真机编排:hdc list/attach/pidof/fport + ConnectServer 转发 + Forwarder(动态 worker fport)+ Drop 清理
├── source.rs 源文件定位:记录名 → 磁盘 .ets 路径(模块名/包名索引)
├── sourcemap.rs 行/列映射:sourceMaps.map(VLQ) 双向 编译.ts坐标 ↔ 源码.ets
└── session.rs DebugSession:DAP↔CDP 翻译;实例表(instances)/帧表/变量引用(均编码 threadId)/断点(cdp_ids 多实例)
多实例模型:
Instance{thread_id, cdp, frame_ids…};全局frames(frameId→Frame,编码 threadId)与var_refs(vref→(threadId,objectId))使scopes/variables/evaluate(只带 frameId/vref)也能路由到正确实例。 主线程固定 thread 1,worker 自增(≥2,不复用)。
📖 完整协议参考(含形式化描述:状态机 / 消息文法 / 不变量 / 时序图)见
docs/ark-cdp-protocol.md。下面是要点摘录。
- 连接
ws://127.0.0.1:<port>/(任意 path,无/json发现端点),raw CDP,每实例一条连接。 - ConnectServer(
ark:<pid>@<bundle>):连后发{"type":"connected"},收addInstance{instanceId,tid,name}/destroyInstance{instanceId}。worker 的instanceId==tid,socket =ark:<pid>@<tid>@Debugger(主线程ark:<pid>@Debugger)。 - debug 模式运行时启动即阻塞等调试器,收到
Runtime.runIfWaitingForDebugger才继续。 ⇒ 适配器在configurationDone(断点已设好)时才发该命令,确保命中早期断点。 - 握手:
Runtime.enable→Debugger.enable(返回{debuggerId,protocols},并回放已加载脚本scriptParsed)。 - 行列号 0-based(CDP)↔ DAP 1-based,适配器做 ±1。
scriptParsed.url:本地编译/previewer = 源码绝对路径(.ets),行号即.ets行; 真机/打包 = 归一化记录名(...|...ts),行号是编译后.ts行,须经sourceMaps.map映射回.ets(见上)。- id 类(scriptId/callFrameId/objectId)一律按
serde_json::Value透传,不假设字符串/整数。
cargo test --test e2etests/e2e.rs 起一个 mock CDP 服务器(按 Ark CDP 线格式回放 scriptParsed/paused/getProperties...),
驱动真实 arkts-dap 二进制走完整 DAP 流程,断言:断点 verified、stopped 事件、调用栈行号 ±1 正确、
作用域/变量(count=42 + 可展开对象)、evaluate、continue。这是协议翻译器正确性的权威验证。
对真实 rich previewer(Stage 应用)attach,完整断点调试已跑通:
- 断点
verified:true、命中EntryAbility.ts:16(reason:"breakpoint",行号 ±1 正确) - 真实调用栈(顶帧
onWindowStageCreate)、真实 local 变量(onWindowStageCreate/windowStage)、evaluate(this)→EntryAbility@74516c2e。
已调通的 debug 启动配置(关键、非显然,见 scripts/run-debug-target.sh):
| 参数 | 值 | 说明 |
|---|---|---|
-j |
<intermediates>/loader_out/default/ets |
modules.abc 所在目录 |
-arp |
<intermediates>/res/default |
resources.index + module.json |
-ljPath |
<intermediates>/loader/default/loader.json |
旁加载 pkgContextInfo.json → ohmurl 解析 |
-abp |
@normalized:N&&&<module>/src/main/ets/entryability/<Ability>& |
归一化 ohmurl 入口(注意含 src/main) |
| 其它 | -pm Stage -d -p <port> -abn <Ability> -device phone |
踩坑要点:-abp 必须是 归一化 ohmurl(@normalized:N&&&...&),而非 @bundle: 或裸路径——
abc 记录名为 <module>/src/main/ets/...(preview 构建保留 src/main);@bundle: 不会被剥离 bundleName 而失败。
-ljPath 必需,否则 pkgContextInfo.json 不加载、ohmurl 无法解析(Cannot find module ... Entry Point)。
启动后运行时阻塞等调试器;adapter 在 configurationDone 发 runIfWaitingForDebugger。
首停为 break-on-start(reason:"entry"),脚本随后 scriptParsed、断点 verified,continue 即命中源码断点。
# 1) 起调试目标(运行时阻塞等调试器)
scripts/run-debug-target.sh ~/Library/OpenHarmony/Sdk/23/previewer \
~/DevEcoStudioProjects/MyApplication2/entry/build/default/intermediates \
com.example.myapplication entry EntryAbility 29900
# 2) attach
arkts-dap --cdp-port 29900 # 或 --tcp 4711 自测MVP 完成(attach 核心调试),previewer 断点已实测命中(rich/Stage); 真机调试已打通(hdc 全自动 attach + fport + CDP;578 真实脚本 + 暂停真实栈/变量,实测)。
sourcemap(.ets↔.ts 行/列精确映射,VLQ 双向,多 product 择优)已实测打通 ✅。
worker 多实例(ConnectServer 发现 + 每实例独立连接 + 断点广播 + threadId 路由)已实测打通 ✅。
launch 模式(aa start -D 自动拉起 + 等主实例 + stopOnEntry + break-on-start 处理)已实测打通 ✅。
设备扩展方法 smartStepInto(stepIn)/dropFrame(restartFrame)/callFunctionOn(clipboard 完整值)已接入并实测 ✅。
VSCode 扩展(vscode-extension/,F5 即调;含 CPU/Heap Profiler 命令)已封装 ✅。
CPU/Heap Profiler(Profiler.*/HeapProfiler.* → .cpuprofile/.heapsnapshot)已接入并实测 ✅。
健壮性:CDP 断开检测(主线程断→DAP terminated+结束会话,worker 断→thread exited)、
SIGINT/SIGTERM 优雅退出清 fport、in-flight 调用断开即失败(不卡 15s)、hdc fport rm 正确清理(两参形式)已加固 ✅。
最早期生命周期断点(onCreate 等)经 LaunchAccelerate + saveAllPossibleBreakpoints + 等进程退出再 launch 已稳定(6/6) ✅。
Backlog(真机):热重载、CDP 自动重连、CPU/Heap profiler 火焰图内联展示。
已与 previewer-frontend 集成:previewer-frontend/scripts/preview-and-debug.sh
一键启动同一个 Previewer,浏览器实时预览 + 本工具断点调试共存(命令/图像通道 vs CDP 通道互不干扰);
debug 模式下 previewer-frontend 的 Inspector 可显示实时组件树(attach 后应用运行态可用)。
Backlog:break-on-start 自动跨过/stopOnEntry 选项、breakpointResolved→DAP breakpoint 事件(pending 断点 verified 更新)、
launch 模式(自 spawn previewer -d -p 再 attach)、CPU/Heap Profiler、热重载、多实例(worker/sessionId)、
设备 attach(hdc 端口转发)。
拟 Apache-2.0,与 OpenHarmony 主仓一致。