Install any skill in seconds. Free to start, no credit card required.
Get Started Free →在无 GUI 调试器的 CLI 环境中,对运行中的 XAS 服务端(Solon,`./gradlew run` 启动)做交互式断点调试。通过 JDWP + jdb + tmux 组合:run 任务暴露 5005 端口,jdb 跑在持久 tmux session 里,由 send-keys/capture-pane 跨调用读写 REPL 状态。适用于:(1) 设断点观察请求现场;(2) 在断点处求值/调用方法;(3) 排查只能在运行时复现的问题(如同步流水线、小米云接口交互)。
.claude/skills/coooolfan-jvm-live-debug/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 53% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 58% | 0% |
当需要在运行中的 server 进程上设断点、看变量、调方法,而没有 IDEA 等 GUI 可用时使用本流程。不适合长时间多线程 step 调试——那种场景效率远不如让用户开 IDEA。
服务端使用 Gradle application 插件,主类 com.coooolfan.xiaomialbumsyncer.App。
bashmkdir -p /tmp/xas-debug cd server && ./gradlew run --debug-jvm > /tmp/xas-debug/server.log 2>&1
工具调用上放到后台执行。然后等待日志出现 Listening for transport dt_socket at address: 5005,再做下一步。
--debug-jvm 默认 suspend=y,JVM 会等调试器附加后才真正启动 Solon。不想 suspend 时改用:
bash./gradlew run -PjvmArgs='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005'
但 suspend=y 更好用——延迟断点能在类加载前就挂上。
bashtmux kill-session -t jdb 2>/dev/null tmux new-session -d -s jdb -x 200 -y 50 'jdb -attach localhost:5005' sleep 3 tmux capture-pane -t jdb -p -S -50
attach 成功后看到 VM 已启动 和 main[1] 提示符,说明 JVM 已经从 suspend 状态接管。
bashtmux send-keys -t jdb 'stop at com.coooolfan.xiaomialbumsyncer.service.AssetService:120' Enter sleep 1 tmux send-keys -t jdb 'cont' Enter
类还没加载时 jdb 会显示 正在延迟断点 ... 将在加载类后设置,正常现象。
等服务端日志出现 Solon 的启动完成行(Started: / started ... ms)或端口监听信息即可。
请求把断点打中后,tmux pane 会出现 Breakpoint hit: 和当前栈,提示符变成 <thread-name>[1]。
bashtmux send-keys -t jdb 'locals' Enter # 局部变量 tmux send-keys -t jdb 'print someVar' Enter # 求值表达式 tmux send-keys -t jdb 'where' Enter # 调用栈 sleep 2 tmux capture-pane -t jdb -p | grep -v '^$' | tail -30
bashtmux send-keys -t jdb 'cont' Enter # 放当前断点 tmux kill-session -t jdb # 退出 jdb(不杀 JVM) # 再停掉后台的 gradlew run / yarn dev 任务
| 命令 | 作用 | |---|---| | stop at FQCN:LINE | 在某文件某行设断点 | | stop in FQCN.method | 在方法入口设断点 | | clear | 列出所有断点 | | clear FQCN:LINE | 删除断点 | | cont | 继续执行 | | step / next | 单步进入 / 跳过 | | step up | 执行到当前方法返回 | | locals | 当前栈帧的局部变量和方法参数 | | print EXPR / eval EXPR | 求值表达式(可调用方法,见下) | | where | 当前线程调用栈 | | up / down | 切换栈帧 | | threads / thread ID | 列线程 / 切线程 | | exit | 退出 jdb(不杀被调试 JVM) |
文件 XiaomiCloudApi.kt 中的顶层函数或扩展函数会被编译成 <FileName>Kt 类的静态方法:
package com.coooolfan.xiaomialbumsyncer.xiaomicloud 下 XiaomiCloudApi.kt 的扩展函数com.coooolfan.xiaomialbumsyncer.xiaomicloud.XiaomiCloudApiKt:行号,不是 XiaomiCloudApiprint / eval 可以求值任意表达式,包括调用方法:
print this.sql.entities.findById(Album::class.java, 1L)this.field 必须显式带 this.——jdb 不会自动解析隐式 receiver。返回值会原样打印。
服务端大量使用 kotlinx.coroutines(下载流水线)。挂起函数在 jdb 里的栈是状态机形态,where 看到的是 invokeSuspend;跨挂起点的 step 基本无意义,优先在挂起点之间的同步片段设断点。
JVM 是 lazy class loading:服务刚启动时大量类还没加载。stop at 一个未加载类的位置时 jdb 会 defer,等类加载时再激活。所以任何时候都可以下断点,不必担心时序。
capture-pane -p 默认只看可见区域。pane 越大空白行越多,输出可能被 tail 掉。两个办法:
tmux capture-pane -t jdb -p -S -50 | grep -v '^$' | tail -30(带 scrollback、过滤空行)tmux new-session -d -s jdb -x 200 -y 50 ...run --debug-jvm 默认 suspend=yJVM 启动后会卡在 Listening for transport ...,不附加 jdb 永远不会进入 Solon 启动流程。所以必须:(1) 启动 run → (2) 等 JDWP listener → (3) attach jdb → (4) JVM 才开始真正启动。
bashmkdir -p /tmp/x && rm -f /tmp/x/*.log # 目录刚建好没文件时整条命令 exit 1
zsh 默认 nomatch 开启。要么先创建占位文件,要么把 rm 分开写,要么避免用 glob。
monitor where 副作用很吵monitor where 会让每次断点命中都打印整个栈。一旦开了就用 unmonitor 1 关掉。命中后真要看栈用一次性的 where。
where N 不工作where 5 在中文版 jdb 里会被解释成「线程 ID 5」并报错。直接用 where 看全栈,或者用 up/down 切帧。
JVM 被 jdb 暂停了,发起这次请求的 HTTP 连接也卡在那里,前端可能报超时。处理完务必 cont 放行。若命中点在定时任务或下载流水线中,还可能触发上游超时重试,注意甄别。
bash# === 1. 启动后端 (后台) === mkdir -p /tmp/xas-debug && cd /Users/yang/Documents/code/XiaomiAlbumSyncer/server && \ ./gradlew run --debug-jvm > /tmp/xas-debug/server.log 2>&1 # === 2. 等 JDWP listener === until grep -qE "Listening for transport|BUILD FAILED|FAILURE:" /tmp/xas-debug/server.log; do sleep 1; done # === 3. tmux + jdb === tmux kill-session -t jdb 2>/dev/null tmux new-session -d -s jdb -x 200 -y 50 'jdb -attach localhost:5005' sleep 3 # === 4. 设断点 + cont === tmux send-keys -t jdb 'stop at com.coooolfan.xiaomialbumsyncer.service.AlbumsService:30' Enter sleep 1 tmux send-keys -t jdb 'cont' Enter # === 5. 启动前端 (后台) === cd /Users/yang/Documents/code/XiaomiAlbumSyncer/web && yarn dev > /tmp/xas-debug/web.log 2>&1 # === 6. 等服务端 + Vite 就绪 === until grep -qiE "started|Solon" /tmp/xas-debug/server.log && \ grep -q "Local:.*http" /tmp/xas-debug/web.log; do sleep 2; done # === 7. 等用户触发,命中后取现场 === tmux send-keys -t jdb 'locals' Enter sleep 1 tmux capture-pane -t jdb -p | grep -v '^$' | tail -30 # === 8. 放行 + 清理 === tmux send-keys -t jdb 'cont' Enter tmux kill-session -t jdb # 然后停掉后台的 gradlew run / yarn dev
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | 20,878 | 12,894 | -38% | 1 | 1 | 0% | 3,483 | 4,751 | +36% | 0 | 0 | — |
case-01 | fail→pass | 25,056 | 17,023 | -32% | 1 | 1 | 0% | 4,663 | 4,960 | +6% | 0 | 0 | — |
case-03 | fail→fail | 19,974 | 19,084 | -4% | 1 | 1 | 0% | 3,518 | 5,735 | +63% | 0 | 0 | — |
case-04 | fail→pass | 17,267 | 17,513 | +1% | 1 | 1 | 0% | 2,797 | 5,128 | +83% | 0 | 0 | — |
case-05 | pass→pass | 17,092 | 15,908 | -7% | 1 | 1 | 0% | 2,813 | 4,907 | +74% | 0 | 0 | — |
case-06 | pass→pass | 17,131 | 8,810 | -49% | 1 | 1 | 0% | 2,898 | 3,741 | +29% | 0 | 0 | — |
case-16 | pass→pass | 12,427 | 11,261 | -9% | 1 | 1 | 0% | 1,957 | 3,976 | +103% | 0 | 0 | — |
case-07 | pass→pass | 14,220 | 12,010 | -16% | 1 | 1 | 0% | 2,380 | 4,179 | +76% | 0 | 0 | — |
case-08 | fail→pass | 14,843 | 8,826 | -41% | 1 | 1 | 0% | 2,356 | 3,596 | +53% | 0 | 0 | — |
case-09 | pass→pass | 20,934 | 19,445 | -7% | 1 | 1 | 0% | 3,290 | 5,161 | +57% | 0 | 0 | — |
case-10 | pass→pass | 11,577 | 6,873 | -41% | 1 | 1 | 0% | 2,066 | 3,397 | +64% | 0 | 0 | — |
case-11 | fail→fail | 15,222 | 11,313 | -26% | 1 | 1 | 0% | 2,493 | 4,176 | +68% | 0 | 0 | — |
case-12 | fail→pass | 11,079 | 5,860 | -47% | 1 | 1 | 0% | 2,083 | 3,283 | +58% | 0 | 0 | — |
case-13 | fail→pass | 12,283 | 6,930 | -44% | 1 | 1 | 0% | 1,944 | 3,322 | +71% | 0 | 0 | — |
case-14 | pass→pass | 5,660 | 2,558 | -55% | 1 | 1 | 0% | 881 | 2,588 | +194% | 0 | 0 | — |
case-15 | pass→pass | 6,009 | 3,723 | -38% | 1 | 1 | 0% | 1,012 | 2,851 | +182% | 0 | 0 | — |
case-17 | fail→pass | 29,751 | 12,008 | -60% | 1 | 1 | 0% | 1,258 | 4,204 | +234% | 0 | 0 | — |
case-18 | pass→pass | 5,302 | 3,990 | -25% | 1 | 1 | 0% | 868 | 2,806 | +223% | 0 | 0 | — |
case-19 | pass→pass | 4,938 | 3,221 | -35% | 1 | 1 | 0% | 821 | 2,768 | +237% | 0 | 0 | — |
case-20 | pass→pass | 16,223 | 15,480 | -5% | 1 | 1 | 0% | 2,696 | 4,851 | +80% | 0 | 0 | — |
case-21 | pass→pass | 14,293 | 11,918 | -17% | 1 | 1 | 0% | 2,482 | 4,311 | +74% | 0 | 0 | — |
case-22 | pass→pass | 15,317 | 17,684 | +15% | 1 | 1 | 0% | 2,402 | 4,963 | +107% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +32 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.