Skip to content

Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719

Closed

Description

@fbs2016

Bug type: Debugger

Describe the bug

  • OS and Version: Ubuntu 20.04
  • VS Code Version: 1.56.2
  • C/C++ Extension Version: v1.4.1
  • Other extensions you installed (and if the issue persists after disabling them):
  • A clear and concise description of what the bug is.
    Unable to set breakpoint or pause on a running application with extended-remote connection debug

To Reproduce
Please include a code sample and launch.json configuration.
Source code of hello:
`#include <stdio.h>
#include <unistd.h>
int a = 10000;
int main(){
int i = 0;
while(i < a){
i++;
printf("test \n");
sleep(2);
}

printf("test \n");

}`

launch.json:
` "configurations": [
{
"name": "gcc - Build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "/folk/bfu/log/hello",
"args": [],
"stopAtEntry": false,
"cwd": ".",
"environment": [],
"debugServerPath": "",
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"text": "target extended-remote 192.168.1.20:12345"
},
{
"text": "set remote exec-file /folk/bfu/log/hello"
},
{
"text": "set logging file /folk/bfu/log/gdbClient.log"
},
{
"text": "set logging on"
},
{
"text": "-enable-pretty-printing"
}
],

        "miDebuggerPath": "/usr/bin/gdb",
        "logging": {
            "trace": true,
            "engineLogging": true,
            "traceResponse": true
        }
    }`

Steps to reproduce the behavior:

  1. Build source code to file "hello", and put it on host 192.168.1.20 which run gdb server;
  2. on host 192.168.1.20, run gdb server "gdbserver --multi --remote-debug --debug-file=gdbServer.log :12345"
  3. run hello in VSC using launch configuration in above;
  4. Try to set breakpoint on the line "printf(**)", it failed and the breakpoint icon is gray
    <-- C (setBreakpoints-8): {"command":"setBreakpoints","arguments":{"source":{"name":"hello.c","path":"/buildarea/bfu/devcode/testFolder/cPrj/hello.c"},"lines":[8],"breakpoints":[{"line":8}],"sourceModified":false},"type":"request","seq":8}
    --> E (output): {"type":"event","event":"output","body":{"category":"console","output":"1: (7935) Send Event AD7BreakpointErrorEvent\n"},"seq":374}
    1: (7935) Send Event AD7BreakpointErrorEvent
    --> R (setBreakpoints-8): {"type":"response","request_seq":8,"success":true,"command":"setBreakpoints","body":{"breakpoints":[{"id":1,"verified":true,"line":8}]},"seq":376}
    --> E (breakpoint): {"type":"event","event":"breakpoint","body":{"reason":"changed","breakpoint":{"id":1,"verified":false,"message":"Attempting to bind the breakpoint....","line":8}},"seq":378}
  5. Click pause in debug menu:
    <-- C (pause-9): {"command":"pause","arguments":{"threadId":1},"type":"request","seq":9}
    --> R (pause-9): {"type":"response","request_seq":9,"success":true,"command":"pause","body":{},"seq":381}
  • From the log in gdbserver, set BP or pause the application, gdbserver can't received any information from gdb client.

I also try to parse the message using wireshark, gdbserver doesn't receive any message about the action set BP or pause.

  • #Cannot pause debugger on Linux #807 , check this similar bug, it's different as vsc has get response from gdbserver, in my case no response.

  • If set breakpoint first then run the application, it works well and it can hit the breakpoint.

  • It works well if run gdbserver on localhost like "target extended-remote 127.0.0.1:12345", VSC received signal "SIGTRAP" first when set BP.
    <-- C (setBreakpoints-8): {"command":"setBreakpoints","arguments":{"source":{"name":"hello.c","path":"/buildarea/bfu/devcode/testFolder/cPrj/hello.c"},"lines":[8],"breakpoints":[{"line":8}],"sourceModified":false},"type":"request","seq":8} --> E (output): {"type":"event","event":"output","body":{"category":"console","output":"1: (16377) ->~\"\\nProgram\"\n"},"seq":374} 1: (16377) ->~"\nProgram" --> E (output): {"type":"event","event":"output","body":{"category":"stdout","output":"\nProgram"},"seq":376} Program --> E (output): {"type":"event","event":"output","body":{"category":"console","output":"1: (16377) ->~\" received signal SIGTRAP, Trace/breakpoint trap.\\n\"\n"},"seq":377} 1: (16377) ->~" received signal SIGTRAP, Trace/breakpoint trap.\n" --> E (output): {"type":"event","event":"output","body":{"category":"stdout","output":" received signal SIGTRAP, Trace/breakpoint trap.\n"},"seq":380} received signal SIGTRAP, Trace/breakpoint trap.

Activity

  1. fbs2016 commented on Jun 18, 2021

    @fbs2016
    Author
  2. WardenGnaw commented on Jun 21, 2021

    @WardenGnaw
    Member

    Thank you for reporting this issue. We’ll let you know if we need more information to investigate it.

  3. fbs2016 commented on Aug 27, 2021

    @fbs2016
    Author

    Andrew Wang (@WardenGnaw)
    Have any progress? Or is there any guide to setup dev environment to debug it by myself?

  4. BillPlunkett commented on Sep 1, 2021

    @BillPlunkett

    I had similar problems until I set MIDebuggerServerAddress to the ip address:port of the gdb server

  5. haneefdm commented on Sep 1, 2021

    @haneefdm

    fbs2016, I saw your commend on #844. I am not sure what your question is. I will try to help see if there is a better way. I know a lot more about gdb-servers than I did two years ago so, maybe there is a better way in general to handle remote gdb servers.

    But, as things stand now, what BillPlunkett is saying is probably right. If your gdb-server is truly running on a remote machine, then you probably do need to use MIDebuggerServerAddress

    If that does not work, let me know and I can look into it.

  6. fbs2016 commented on Sep 3, 2021

    @fbs2016
    Author

    BillPlunkett Haneef Mohammed (@haneefdm) Thanks.
    Firstly, we launch gdbserver with multi mode "gdbserver --multi ...", the address set in MIDebuggerServerAddress can't support multi mode connection. So I configure setupCommands ""text": "target extended-remote 192.168.1.20:12345"".

    Secondly, I put the out file to target using setupCommands "remote put *out" and other commands which depends on remote connection.
    In cppdbg launch, if you configure MIDebuggerServerAddress and setupCommands, it run setupCommands first then connect to MIDebuggerServerAddress . Then "remote put" will failed as the connection doesn't established.

    Do you have any advice to configure it to support this scenario? Thanks in advance.

  7. BillPlunkett commented on Sep 3, 2021

    @BillPlunkett
  8. fbs2016 commented on Sep 6, 2021

    @fbs2016
    Author

    BillPlunkett
    Thanks a lot. It works.
    It confused me that "MIDebuggerServerAddress" can work with gdbserver multi mode after replacing "setupCommands" with"customLaunchSetupCommands".
    Form the debug console, I just see the connection log of command "target extended-remote xxx" but no log about "MIDebuggerServerAddress", I try to remove the the option "MIDebuggerServerAddress" but it failed.

    And another different is that the option "targetArchitecture" must be set, otherwise the launch report err. But it can work before apply your advice as vsc set it as x86 auto.
    Thanks

  9. github-actions commented on Mar 6, 2022

    @github-actions

    This issue has been closed automatically because it has not had recent activity.

  10. lingr7 commented on Sep 1, 2026

    @lingr7

    Source-Protected Remote Debugging with cppdbg: Principles & Key Findings

    本文记录在 VSCode C/C++ 扩展(cppdbg / MIEngine)下实现「源码保护型远程调试」时踩出的关键原理与发现。目标是回馈社区(对应 vscode-cpptools#7719 / #833 / #12890、MIEngine#844 等长年开放 issue),让后来者——无论用 Python、Node.js 还是 Rust 实现——都有一套可遵循的、与具体语言解耦的机理说明。

    受启发且经源码核实的两个上游仓库:

    文中所有 MIEngine 行号引用基于本地检出一份主干源码核实(src/MICore/Debugger.cs、src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs、src/MICore/LaunchOptions.cs、src/MICore/Transports/ServerTransport.cs 等),不同版本可能略有偏移,以符号名为准。


    0. 一句话总览

    把 debugServerPath 当作「让 MIEngine 把会话判成远程 + 在 MI 通道之外旁路传递动态端口」的杠杆,加上一个由该 debugServer 进程在 B 侧拉起 gdbserver --multi localhost:<port> 并经 ssh -N -L 反向建隧道的常驻管理器,即可在「A 持源码与符号、B 仅持 stripped 二进制」的前提下,获得 F6 暂停、运行中增删断点、源码定位全可用的远程调试体验——且 launch.json 保持静态、无需写死端口。

    整条链路没有任何黑魔法,全是 MIEngine 已有能力与 GDB/MI + RSP 协议既有语义的组合。下面逐条拆解,并标注每一处「为什么」对应上游 issue 的哪个根因。


    1. 问题溯源:为什么直接用 setupCommands 连 target extended-remote 会失效

    这是 #7719 / #833 / MIEngine#844 的共同根因,也是本方案要解决的核心痛点。

    1.1 MIEngine 用 launch.json 字段——而非 gdb 命令——判定 local/remote

    MIEngine 的 IsLocalGdbTarget() / IsRemoteGdbTarget() 判定依据是 launch options 的类型与字段,不是 info target 的输出,也不是你在 setupCommands 里写了 target extended-remote:

    // src/MICore/Debugger.cs
    internal bool IsLocalGdbTarget()        // ~L593
      => MICommandFactory.Mode == MIMode.Gdb
         && _launchOptions is LocalLaunchOptions && !IsLocalLaunchUsingServer();
    
    internal bool IsRemoteGdbTarget()       // ~L599
      => MICommandFactory.Mode == MIMode.Gdb
         && (_launchOptions is PipeLaunchOptions
             || _launchOptions is UnixShellPortLaunchOptions
             || IsLocalLaunchUsingServer());
    
    protected bool IsLocalLaunchUsingServer()  // ~L586
      => _launchOptions is LocalLaunchOptions llo
         && (!IsNullOrWhiteSpace(llo.MIDebuggerServerAddress)
             || !IsNullOrWhiteSpace(llo.DebugServer));

    关键:当 launch.json 既没填 miDebuggerServerAddress、也没填 debugServerPath,但你在 setupCommands 里手写了 target extended-remote host:port 时——MIEngine 看不到这条命令的语义,它只认字段。于是 IsLocalGdbTarget()==true,会话被当成「本地」。

    1.2 误判为 local 的灾难性后果:暂停 = 给 inferior pid 发 kill -5

    CmdBreakInternal()(Debugger.cs ~L713)的分支:

    if (IsLocalGdbTarget()) {               // L720
        // 取 _debuggeePids 的第一个 pid
        if (PlatformUtilities.IsLinux() || PlatformUtilities.IsOSX())
            if (useSignal) return CmdBreakUnix(debuggeePid, ...);   // L741
    }
    else if (IsRemoteGdbTarget() && _transport is PipeTransport) { ... }
    else if (IsRemoteGdbTarget() && _transport is UnixShellPortTransport) { ... }
    
    // 兜底:IsUsingExecInterrupt = true; -exec-interrupt  (L762-763)

    CmdBreakUnix → UnixUtilities.Interrupt(pid) → Kill(pid, 5)(UnixUtilities.cs L254-263),即对外inferior 的 pid 跑 kill -5(SIGTRAP)。

    当 inferior 真在本地,SIGTRAP 经终端进程组投递,恰好等价于 Ctrl-C,意外地能用。但一旦 inferior 在远端 B:

    • 你按下 F6 / 运行中加断点,MIEngine 走 CmdBreakUnix 分支,对「它以为在本地的 inferior pid」发 kill -5;
    • 那个 pid 要么是 gdbserver 编造的 fake pid/线程组(remote 协议允许 gdbserver 报告并不真实映射到 OS 进程的 pid,见 MIEngine#844 作者 haneefdm 的分析),要么干脆是 A 机上一个无辜的本地进程——后者即 haneefdm 警告的「MIEngine might kill a random process」。
    • gdbserver 端则「收不到任何 set BP / pause 的信息」(Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719 报告者的 wireshark 抓包结论)。

    这就是 #7719 / #833 / #12890 的机制级根因:不是 gdb/gdbserver 的 bug,是 MIEngine 的分类器在「字段缺失 + setupCommands 手连」这一组合下的必然误判。haneefdm 在 #844 提议用 gdb 启动后 info target 的 mode == "connected" 来纠正,gregg-miskelly 回复「更干净的做法是让 IsLocalGdb 在这些场景返回 false」——而社区侧无需改 MIEngine 即可达成该效果的办法,就是填上让它判 remote 的字段。

    1.3 两条让 MIEngine 判 remote 的字段

    只要 launch.json 满足任一,IsLocalLaunchUsingServer()==true → IsLocalGdbTarget()==false,后续暂停/断点走 -exec-interrupt(对所有 remote target 普遍可靠):

    1. miDebuggerServerAddress: "host:port" —— 经典做法(Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719 中 BillPlunkett 给出的可工作配置即此)。但它是静态地址端口,且 MIEngine 会在 setupCommands 之后自己发 -target-select remote/extended-remote(见 DebuggedProcess.cs ~L836-840),与 --multi + 需要 remote put/set remote exec-file 先于连接的场景时序冲突(Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719 报告者的困惑正在于此)。
    2. debugServerPath + serverStarted —— MIEngine 先拉起你指定的 debugServer 进程,等其 stdout 匹配 serverStarted 正则后才放行 setupCommands(含你手写的 target extended-remote)。时序由你掌控,且能传递动态端口。本方案选这一条。

    2. 整体架构(语言无关)

    机器 A(安全,持源码+符号)              机器 B(不安全,仅 stripped 二进制)
    ┌────────────────────────────┐         ┌──────────────────────────┐
    │ VSCode cppdbg              │         │ gdbserver --multi        │
    │  └ MIEngine                │         │   localhost:<Bport>      │
    │     └ 本地 gdb(符号源)    │         │  (只绑 loopback)        │
    │ debugServerPath 拉起 ──────┼────────>│                          │
    │  隧道管理器(常驻进程)      │ ssh -L  │  stripped exe + .so      │
    │   动态选 Bport             │<========│  (由 rsync 镜像部署)     │
    │   起远端 gdbserver         │  RSP    │                          │
    │   建 ssh -N -L 隧道        │         └──────────────────────────┘
    │   生成 .gdb_remote_connect │
    │     .gdb(含 target ext-   │
    │      remote 127.0.0.1:Ap) │
    │   stdout 打 READY marker   │
    │ setupCommands: source 该   │
    │   .gdb → gdb 连隧道本地端口│
    └────────────────────────────┘
    

    四个角色的职责边界是语言无关的设计要点:

    角色 职责 不做什么
    MIEngine / cppdbg 启动本地 gdb、发 MI 命令、把会话判 remote(靠 debugServerPath 字段) 不需要知道 B 的 user@host / 端口
    debugServer 进程(隧道管理器) 选端口、远端拉 gdbserver、建 ssh 隧道、生成连接 .gdb、打 READY marker、会话结束回收 不参与 MI 协议、不替 gdb 转发 RSP
    ssh -N -L 隧道 把 A 的 loopback:Ap 端口转发到 B 的 loopback:Bport 不暴露公网端口(两侧都绑 loopback)
    gdbserver --multi localhost:Bport 跑被调试程序、响应 RSP、允许多次 vRun 不绑 INADDR_ANY(localhost: 不是 :Bport)

    3. 关键原理逐条(每条都标注「为什么」与「语言无关的实现要点」)

    3.1 原理一:debugServerPath + serverStarted 是「判 remote + 传动态端口」的干净杠杆

    机制(MIEngine 源码核实):

    • LaunchOptions.cs InitializeServerOptions(~L480-494):当 debugServerPath 非空,记下 DebugServer/DebugServerArgs/ServerStarted/FilterStdout/FilterStderr,且若 filterStderr 与 filterStdout 都没显式设,默认 FilterStdout=true(L489-492)。
    • LaunchOptions.cs ShouldStartServer()(L530-533):DebugServer 非空即启 server 模式。
    • DebuggedProcess.cs ~L193-201:ShouldStartServer() 为真时构造 ClientServerTransport(LocalTransport, ServerTransport(killOnClose:true, filterStdout, filterStderr))。注意:MI 通道是 client transport(你的本地 gdb),ServerTransport 仅用于拉起/监控 debugServer 进程,二者解耦。
    • ServerTransport.cs L29-42 InitStreams:用 DebugServer 当 FileName、DebugServerArgs 当 Arguments 起进程,_startPattern = ServerStarted。
    • ServerTransport.cs L44-54 FilterLine:逐行匹配 _startPattern 正则,首次匹配置 _started=true 并 StartedEvent.Set();之后始终返回 null(不喂给 MI 解析)。
    • ClientServerTransport.cs L28-46 Init → WaitForStart():StartedEvent.WaitOne(launchTimeout),超时抛 Error_DebugServerInitializationFailed。这是 MIEngine 等 serverStarted 再放行 setupCommands 的同步点。

    为什么这是干净路径:

    1. debugServerPath 非空 → IsLocalLaunchUsingServer()==true → IsLocalGdbTarget()==false(§1.1)→ 暂停/断点走 -exec-interrupt,绕开 kill -5 误杀(§1.2)。直接修掉 Unable to set breakpoint or pause on a running application with extended-remote connection debug #7719/installation issue #844/Unable to pause, step button while using gdb connect to remote gdbserver #12890 的根因,无需改 MIEngine。
    2. 你在 debugServer 进程(隧道管理器)里动态选端口,把端口写进生成的 .gdb_remote_connect.gdb(target extended-remote 127.0.0.1:<动态端口>),setupCommands 末尾 source 该文件。launch.json 因此保持静态、无端口——避开了 「preLaunchTask 改 launch.json 不生效」「${command:}/${env:} 注入 port 皆不可行」的 VSCode 限制。
    3. 时序天然正确:MIEngine 等 serverStarted 匹配后才跑 setupCommands,此时连接 .gdb 已就绪,source 不会落空。

    语言无关实现要点:

    • 你的 debugServer 进程的唯一职责:选端口 → 起 B 侧 gdbserver → 建 ssh -L → 写连接文件 → 往 stdout 打一行 READY marker(flush 确保立即输出)→ 常驻到会话结束。
    • marker 必须是「正则可匹配、且 stdout 只此一行有意义输出」——FilterLine 在匹配后仍把每一行经 LogText 转发到 Debug Console(见 §3.7),多打噪声会污染调试控制台,但不影响 serverStarted 判定(那是个 latch)。
    • 进程要能被 MIEngine killOnClose 整树回收(会话结束 MIEngine 杀 debugServer 进程树)——你的实现里 ssh 隧道、远端 gdbserver 都得挂在该进程树下,或由该进程的信号处理统一清理。

    3.2 原理二:隧道就绪判定只看 LISTEN,绝不在 gdb 连接前 connect()

    这是最容易踩的坑,且与语言完全无关。

    gdbserver --multi localhost:<port> 这一族版本有一个行为:在尚未用 vRun 启动 inferior 时,若收到一个「连上但不发任何 RSP 包即关闭」的连接,会直接退出。于是留给真正 gdb target extended-remote 的就是 Connection refused。

    表现(来自实测 + 上游 issue 共有现象):

    • gdb 报 Remote communication error. Target disconnected.: Connection reset by peer;
    • ssh 隧道日志 channel 2: open failed: connect failed: Connection refused;
    • gdbserver.log 只记录了那次探测连接 Remote debugging from host ...,之后再无连接。

    根因: 你在「隧道就绪」判定里用了 socket.create_connection(localhost, port) 主动 connect 探测。该 connect 经 ssh -L 打穿到 B 侧 gdbserver,触发了上述「空连接即退出」。

    正确做法(语言无关):

    • 就绪循环只看「A 侧 ssh -L 本地转发端口是否进入 LISTEN 状态」。Linux 上 ss -tln 列内核监听表,只读、不发起连接。ss 不可用 → 假阴性 → 重试(由超时 + ssh 死亡检测兜底)。
    • gdb 必须是第一个、也是唯一连接 gdbserver 的客户端。 任何对隧道/端口连通性的「主动 connect 探测」都要避免在 gdb 连接前发生。
    • 探活远端 gdbserver 用另起一条 ssh 跑 kill -0 $pid(包 try/except),不要连 RSP 端口。

    通用教训:「端口可达」不等于「服务可扛探测」。Stateful 协议(RSP、GDB/MI、各类 REPL)的服务端常对「空连接」敏感,就绪探测必须停留在传输层(LISTEN)而非应用层(connect/handshake)。

    3.3 原理三:远端常驻守护进程的后台化写法(exec + 子 shell 重定向)

    经 ssh 远程启动一个需常驻的 gdbserver --multi,写法不对会 ssh TimeoutExpired 永不返回。错误与正确对照:

    # ✗ 错:& 后台化的是整个 && 列表,其子 shell 仍持有 sshd 会话管道
    #       sshd 等不到 EOF → ssh 永不返回
    cd <deploy> && nohup gdbserver ... >log 2>&1 &
    
    # ✓ 对:重定向挂 ( ) 子 shell + 内部 exec 替换;& 后台化替换后的进程
    ( cd <deploy> && exec nohup env LD_LIBRARY_PATH=... \
        gdbserver --multi localhost:PORT ) </dev/null >gdbserver.log 2>&1 & echo $!

    要点(语言无关):

    1. 重定向必须挂在 ( ) 子 shell 上,内部用 exec 替换——这样后台化的进程不再持有 sshd 会话管道,sshd 拿到 EOF,ssh 正常返回。
    2. exec 后 $! 即 gdbserver 真实 pid(不是中间 nohup/sh 的 pid),供后续 kill -0 探活、会话结束时 kill 清理。
    3. stdin 重定向到 /dev/null——否则 gdbserver 可能继承 ssh 的 stdin,干扰隧道。
    4. gdbserver --multi localhost:PORT 用 localhost:(只绑 loopback),不要用 :PORT(绑 INADDR_ANY,B 机其他用户可连执行任意代码)。

    3.4 原理四:set remote exec-file 是远端 inferior stdio 的唯一有效杠杆

    cppdbg 下,inferior 跑在 B、gdb 在 A。你无法用常规手段重定向 inferior 的 stdio:

    • run > file、set args >、set inferior-tty:对 remote inferior 实测全部无效(remote inferior 的 stdio 由 gdbserver 侧 fork+exec 决定,gdb 端的 tty 设置不跨 RSP 生效)。
    • 唯一有效杠杆:set remote exec-file 指向一个 wrapper 脚本(而非 inferior 本体),由 wrapper 在 B 侧重定向 inferior 的 stdio(接到 pty / 写日志文件 / 接独立 TCP 端口皆可)。-exec-run 经 vRun 发到 gdbserver,gdbserver exec 的就是这个 wrapper。

    这条原理解决两个独立问题:

    1. 源码保护下 B 侧运行时环境:wrapper 里 cd <deploy> + export LD_LIBRARY_PATH=<deploy>:<deploy>/tool/lib,gdbserver 继承给 inferior,替代旧 stdio-pipe 模式里内嵌在 ssh 命令里的环境注入。
    2. inferior 输出的独立终端显示(§3.6)。

    语言无关:wrapper 是 B 侧一个普通脚本(sh/python/任何 B 上有的运行时),内容是「设好环境 → 把 inferior 的 stdio 接到某个通道 → exec inferior」。set remote exec-file 的值是它在 B 上的绝对路径。exe 名由 wrapper 内部决定,与 launch.json 的 program basename 同源即可(切换调试对象 = 改 program 末段 exe 名)。

    3.5 原理五:符号/源码定位——solib-search-path 在 F5 下必被 MIEngine 覆盖

    这条对「源码保护但要在 A 上看源码栈」至关重要,且是个反直觉的覆盖行为。

    MIEngine 源码核实的执行顺序(DebuggedProcess.cs GetInitializeCommands ~L617-663):

    L621  commands.AddRange(_launchOptions.SetupCommands);   // 含你的 source .gdb_remote_connect.gdb
                                                           //   (脚本里 set solib-search-path out:out/tool/lib)
    ...
    L648  escapedSearchPath = join(_launchOptions.GetSOLibSearchPath() ...)
    L651  if (!IsNullOrWhiteSpace(escapedSearchPath))
    L656      commands.Add("-gdb-set solib-search-path " + escapedSearchPath + sep)  // 后发 → 覆盖
    

    GetSOLibSearchPath() → GetSOLibSearchPathCandidates()(LaunchOptions.cs L1832-1876)仅两个来源:

    1. exe 所在目录 dirname(program)(L1845-1857);
    2. additionalSOLibSearchPath 字段,分号分隔(L1860-1875)。

    结论: 你在 source 脚本里、或 setupCommands 里写的 set solib-search-path,在 F5(经 MIEngine)路径下必然被 L656 那条覆盖。这是 #11949 等报告「symbols not loaded」的一类根因——多产物项目(exe 在 out/,关键 .so 在 out/tool/lib/)只设了脚本里的 solib-search-path,MIEngine 覆盖后只剩 <exe-dir>:,子目录里的 .so 符号挂不上,栈帧 ??。

    正确做法(语言无关):

    • F5 下,solib 搜索路径只能通过 launch.json 的 additionalSOLibSearchPath 字段喂给 MIEngine(分号分隔,Windows 转 ;、Unix 用 : 由 MIEngine 处理)。多目录项目要列全:${workspaceFolder}/out;${workspaceFolder}/out/tool/lib。
    • CLI 链路(不经 MIEngine,自己驱动 gdb)则靠 source 脚本里的 set solib-search-path,两条路径并存、同源(都源自同一份项目清单 solib_dirs),不可互相替代。
    • gdb solib-search-path 不下钻子目录——只按 solib basename 在每条路径下查。所以必须显式列出子目录。
    • 符号源 = A 上未 strip 的构建产物(build-id 与 B 部署件一致,PIE 下符号按运行时基址自动重定位)。不要用 add-symbol-file——不带加载地址时符号停在链接期地址,PIE 下断点永不命中。

    语言无关:核心是「MIEngine 会覆盖」这一行为契约,与你用什么语言生成 launch.json 无关。任何实现都必须同时维护「脚本里的路径(CLI 用)」和「additionalSOLibSearchPath 字段(F5 用)」,且二者同源。

    3.6 原理六:inferior 输出转发的语义陷阱与 pty 桥接

    3.6.1 MIEngine 的输出语义(源码核实,决定你该怎么设计转发)

    cppdbg 下 inferior 的 stdout/stderr 怎么进 Debug Console?ServerTransport.FilterLine(L44-54)逐行处理后:

    • 匹配 serverStarted 前/后都调用 LogText(prefix + ": " + line)(L52);
    • LogText → OutputStringEvent → DAP OutputEvent,category = stdout(StdOut 默认开启)。

    即:filterStdout: true 并不是「隐藏 debugServer stdout」,而是「把该 stdout 不当 MI 协议解析、转而以 stdout category 打到 Debug Console」。所以你的 debugServer 进程(隧道管理器)往 stdout 打的每一行都会混进 Debug Console——这就是为什么 READY marker「只打一行」很重要,且隧道管理器不应把 inferior 输出往自己 stdout 倒(会与 MI 噪声混在一起)。

    3.6.2 inferior 输出根本到不了 A(TCP 隧道模式)

    F5 TCP 隧道下,B 上 gdbserver 的 stdio 被重定向到 gdbserver.log,inferior 继承之;MIEngine 的 OutputEvent 只能进 Debug Console 且与 MI 噪声混合。filterStdout 只是「不当 MI 解析」并非「分流/隐藏」。要拿到「独立终端实时显示 + stdin 可交互」必须在 gdbserver 侧(B 侧)解决,不能靠 MIEngine 开关。

    3.6.3 唯一杠杆是 wrapper + pty 桥接(§3.4 的延伸)

    set remote exec-file 指向 wrapper,wrapper 把 inferior stdio 接到 B 侧一个 pty;一个本地(B 侧)桥接进程把 pty master 的数据经第二条 ssh -L 隧道回 A,A 侧一个独立终端任务(console 子命令)连本地转发端口,在 VSCode 集成终端实时显示 + 透传 stdin。

    pty 桥接必须满足三语义(语言无关,socat 等简单桥接实测做不到):

    1. 无客户端时持续 drain master 并丢弃 —— pty 缓冲写满(~500KB)会永久阻塞 inferior 的 write();简单 TCP 桥接无连接时不 drain 即挂死。主循环无条件读 master,无 client 时丢弃,inferior 永不因「没人看」阻塞。
    2. master 读 EIO/EOF 静默重试 —— inferior 退出 / slave 全关时读抛 EIO 或返回 b"";两分支都只 continue,等下一次 vRun 重新打开 slave,绝不退出桥接。
    3. 客户端随时断开/重连不影响 inferior —— pty 生命周期与 TCP 连接解耦:client 关闭只从列表移除,master/pty 常驻;重连即重新 accept。

    slave 保持 openpty 默认(canonical + ECHO + ISIG):远端回显、按行交付、Ctrl-C 经 ISIG 转 SIGINT 给 inferior(与真实终端一致)。A 侧本地终端 stdin 才 setraw 透传字节。

    语言无关:用 Python os/pty/select、Node child_process + pty.js、Rust nix::pty 都行——三语义是协议要求,不是语言特性。socat 无法满足 drain/重连语义;tmux 会改变 inferior 与 gdbserver 的父子关系,破坏 ptrace 跟踪,不可用。

    3.6.4 回退

    桥接不可用 / console 连不上 / CLI stdio-pipe 模式(无第二条隧道)→ console 回退 ssh tail -F <deploy>/console.log 单向显示;wrapper 见 pty 不存在走 console.log 回退。无 console 任务时桥接仍 drain 丢弃,inferior 不阻塞(输出丢失,pty 模式下 console.log 不写)。

    3.7 原理七:abort 抢救——远程调试特有的 stdout 丢失

    远程调试下 inferior 的 stdout 不是 TTY(gdbserver 经 RSP O packet 转发,或经 §3.6 的 pty)。glibc 对非 TTY 的 stdout 默认全缓冲(4KB block),abort()(glog CHECK/LOG(FATAL)/assert → SIGABRT)由信号直接杀进程,stdout 用户态 buffer 里未 flush 的内容丢失。stderr 无缓冲,故 glog FATAL stacktrace 不丢;丢的是程序自身 printf 走 stdout 的部分。

    抢救(语言无关,注入到连接 .gdb 末尾):

    set breakpoint pending on
    break abort
    commands
      silent
      call (void)fflush(0)
      printf "\n[rdbg] abort() entry caught: fflush(0) flushed stdout buffer; stack points at CHECK site\n"
    end
    handle SIGABRT stop nopass
    catch signal SIGABRT
    commands
      silent
      call (void)fflush(0)
      printf "\n[rdbg] SIGABRT caught (fallback): attempted fflush(0)\n"
    end
    • 主 break abort:在 abort() 入口、信号未 raise 前拦下,栈完整直指 CHECK 现场,call (void)fflush(0) 稳定抢救缓冲。set breakpoint pending on 让 libc 加载后自动落位(脚本生成时 inferior 未启动、abort 符号未定义)。写 fflush(0) 不写 fflush(NULL)——NULL 是宏不是符号,GDB 表达式不识别。
    • 兜底 catch signal SIGABRT:拦直接 raise(SIGABRT) 绕过 abort() 的路径;此时栈已进信号投递路径,call 可能失败但不影响其余流程。handle SIGABRT stop nopass 对抗 cpptools(cppdbg) 默认的 nostop——否则信号直接递交 inferior 杀进程、GDB 不停、catchpoint 永不触发(F5 命中 abort 直接退出的根因)。
    • 两处命中都只 fflush + printf 提示、不自动 continue:bt 看现场后手动 continue。

    彻底治本(不依赖 catchpoint,所有场景受益): 在 exe 的 main 入口 setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IOLBF, 0);(行缓冲)。注意 setvbuf 操作的是进程级全局 FILE*——即便调用点在 .so,对整个 inferior 的 stdout/stderr 同样生效;exe 上 nm|grep setvbuf 为空不代表没生效(调用编进了 .so)。这是常见误判。

    语言无关:抢救块是 GDB 命令文本,由你的工具生成到连接 .gdb 即可;setvbuf 是被调试程序侧的 C 代码改动,与工具语言无关。

    3.7.1 A/B 系统库镜像(break abort 的远程前提)

    远程调试时 GDB 的 solib 符号来自 A 本地文件(set sysroot / 即 A 的 /lib/...)。A/B 发行版不同(glibc 版本差异)时 build-id 不符,GDB 地址校验失败拒读符号 → break abort 永不解析、call fflush 报 no symbol、bt 里 libc 帧 ??。

    解法(语言无关):部署时把 ldd 列出的 B 系统库镜像到 A 的 .debug/b-sysroot/(按 B 上绝对路径布局,md5 缓存增量拉取,B 无需装 binutils),连接脚本里 set sysroot <镜像> 覆盖缺省 /;项目自有 .so 不镜像(仍由 solib-search-path 命中本地未 strip 副本,DWARF 不丢)。GDB 对不存在的 sysroot 目录不回退原始路径,故镜像未生成时保持 /。

    3.7.2 排查第一动作:B 上纯跑

    调试会话「零输出」≠ 程序无输出。在 --multi + vRun + abort 抢救块场景,inferior 的 stderr(glog FATAL)常不回显到 UI,造成「无声 abort」假象。第一动作:ssh 到 B,在部署目录里按 wrapper 同款 LD_LIBRARY_PATH 与 cwd 不挂 gdb 纯跑同一命令,拿退出码与 stdout/stderr:

    • EXIT=134(128+SIGABRT)即确认 abort——问题在程序本身/环境,与 gdb/缓冲/链路无关;看 stderr 的 glog stacktrace 直接拿 CHECK 现场。
    • EXIT=0 且有正常输出 → 仅 gdb 下复现,查 fork 子进程时序、抢救块 continue 放行、inferior-tty 转发等调试器侧因素。

    3.8 原理八:带版本号 SONAME 库的部署命名

    带版本号的库(如 libFoo)本地是 symlink 链:

    libFoo.so      -> libFoo.so.1     (symlink)
    libFoo.so.1    -> libFoo.so.1.0   (symlink)
    libFoo.so.1.0                        (真实文件, SONAME=libFoo.so.1)
    

    ld.so 按 SONAME(libFoo.so.1)查找,本地靠 symlink 解析。若按真实文件名 libFoo.so.1.0 部署到 B,B 上既无 .so.1 文件也无 symlink → ld.so not found,程序退出。其他库(真实文件名 == SONAME)不受影响,唯独带版本号的库中招。

    解法(语言无关): 远端文件名一律取 SONAME(readelf -d <so> | grep SONAME 得到),与 staging 命名一致,保证 B 上文件名 == SONAME;带版本号库无需在 B 上重建 symlink 链。排障:B 上 LD_LIBRARY_PATH=<deploy> ldd <exe> | grep 'not found',缺的库 readelf -d 确认文件名是否 == SONAME。

    3.9 原理九:孤儿清理的判活规则

    gdbserver --multi 断连后驻留等待新连接(设计与生俱来,不是 bug)。异常退出会堆积孤儿。回环调试(A==B)尤甚。

    判活规则(语言无关,不依赖「归属」):

    • --multi localhost:PORT 的 gdbserver:看端口是否有 ESTABLISHED 连接(无连接 = 孤儿)。注意 ss -tn state established 在某些版本是 4 列布局(无 State 列),按列下标判 ESTABLISHED 会全漏 → 活跃会话误判孤儿 → 误杀。用正则 127.0.0.1:{port}\b 整行扫。
    • 一次性 gdbserver [-] <exe>:看 PPid==1(init 收养 = 原启动它的 ssh 管道已退出)为孤儿,PPid!=1 保留。
    • 活跃会话一律不动。

    日常清理按归属(只清自己,不误杀他人);治脚本自身残留用 --all 按判活清全量孤儿。这条对所有实现通用:清理要么按部署目录锚定(归属),要么按端口/PPid 判活,二者不要混。

    3.10 原理十:配置的单一事实来源

    整条链路有两类事实,必须各有唯一来源、且工具只读消费:

    • 项目侧事实(产物名/构建命令/build 目录/solib_dirs/镜像排除项)→ 一个入库的项目清单文件(本仓为 rdbg.json,JSONC,容忍 // 注释与尾逗号)。
    • B 主机信息(用户/IP/私钥/部署目录/程序参数)→ launch.json 顶层自定义 _ 前缀字段(VSCode 不识别会忽略,不报错不弹窗),工具只读。

    字段级优先级:命令行参数 > 清单/launch.json > 内置默认。CLI 与 F5 共用同一份 launch.json。切换调试对象 = 改 program 路径末段 exe 名(F5,set remote exec-file 用此 basename)/ --target(CLI)。

    语言无关:核心是「双事实来源会失同步」这一反模式——任何实现都应把动态信息(端口除外,端口由 debugServer 进程生灭)收敛到入库静态文件,工具只读不改,避免「生成器覆盖手改」的冲突。


    4. 时序契约(语言无关,实现任何语言版本都要满足)

    1. preLaunchTask: 构建已解耦(外部)→校验产物→分离调试信息→部署到 B→审计 build-id→镜像 B 系统库;退出。
    2. F5 → cppdbg 启动本地 gdb(符号源=本地未 strip 产物)
    3. MIEngine 拉起 debugServerPath 进程(隧道管理器),逐行过滤 stdout 等 serverStarted 正则
    4. 隧道管理器:
       a. 动态选 Bport
       b. B 上后台化 gdbserver --multi localhost:Bport(exec+子shell 重定向,§3.3)
       c. 建 ssh -N -L 127.0.0.1:Aport:localhost:Bport
       d. 轮询 A 侧 Aport 是否 LISTEN(绝不 connect,§3.2)
       e. LISTEN 后生成 .gdb_remote_connect.gdb(含 set remote exec-file <wrapper>
          + target extended-remote 127.0.0.1:Aport + sysroot/solib/abort 抢救块)
       f. stdout 打 READY marker(一行,flush)
    5. MIEngine serverStarted 命中 → 放行 setupCommands
    6. setupCommands: set sysroot / set solib-search-path(被覆盖,真正生效靠 additionalSOLibSearchPath,§3.5)
       / set substitute-path → source .gdb_remote_connect.gdb → gdb 连隧道本地端口
    7. -exec-run 经 vRun → gdbserver exec wrapper → wrapper 接 pty + 设环境 → exec inferior
    8. F6/运行中增删断点走 -exec-interrupt(§1.1/§1.2 已判 remote,可靠)
    9. (可选)preLaunchTask 并行起 rdbg-console → 连第二条 ssh -L 的 cport → 独立终端显示 inferior stdio
    10. 会话结束: MIEngine killOnClose 杀隧道管理器进程树(含 ssh 隧道)
        gdbserver --multi 断连后驻留 → postDebugTask 或 clean 清理(§3.9)
    

    红线(违反任一会出问题):

    • 禁止在 gdb 连接前 connect 探测 RSP 端口(§3.2)。
    • F5 下 solib 路径必须进 additionalSOLibSearchPath,不能只靠脚本(§3.5)。
    • 远端常驻进程后台化必须用 exec+子shell 重定向(§3.3)。
    • gdbserver 绑 localhost: 不绑 :port(§3.3)。
    • debugServer stdout 只打一行 READY marker,其余别污染(§3.6.1)。
    • 部署带版本号库用 SONAME 命名(§3.8)。

    5. 与上游 issue 的对应关系

    上游 issue 现象 本文对应原理 本方案如何化解
    vscode-cpptools#7719 extended-remote 连接下不能下断点/暂停,gdbserver 收不到任何 set BP/pause §1.1 §1.2 §3.1 debugServerPath 让 MIEngine 判 remote,走 -exec-interrupt
    vscode-cpptools#833 / #12890 远程 gdb 无法暂停/运行中下断点 §1.2 §3.1 同上;判 remote 后中断命令可靠
    MIEngine#844 MIMode gdb 误判 inferior 类型,pause 给本地 pid 发 SIGTRAP 可能误杀无辜进程 §1.1 §1.2 填字段让 IsLocalGdbTarget()==false,不进 CmdBreakUnix/kill -5 分支
    vscode-cpptools#11949 远程 solib 符号不加载 §3.5 additionalSOLibSearchPath 列全子目录
    #2639 non-stop 模式下 continue 只恢复单线程、UI 卡 (避坑) 保持 all-stop + MIEngine 自带的 set mi-async on(每次会话 MIEngine 自设),不改 gdb 异步档位

    给社区的可以这样讲: 你不需要等 MIEngine 修 IsLocalGdb* 的分类器(#844 提议的 info target 纠正至今未合)——debugServerPath + serverStarted 是 MIEngine 既有能力的合法用法,填上即等效「让 IsLocalGdbTarget 返回 false」,从分类层根除 kill -5 误杀与 -exec-interrupt 不发的问题。代价只是一个你自己掌控生命周期的 debugServer 进程(隧道管理器),且换来了动态端口与静态 launch.json 两个额外好处。


    6. 迁移到其他语言/项目的清单

    实现语言无关,拷贝时只需替换脚本语言:

    1. 隧道管理器(debugServer 进程):选端口 → 远端起 gdbserver → 建 ssh -L → 轮询 LISTEN → 生成连接 .gdb → 打 READY marker → 常驻/清理。任何能起子进程、读 ss/ss -tln、写文件的运行时都能实现。
    2. wrapper(B 侧):设环境 → 接 pty(或回退 console.log)→ exec inferior。
    3. pty 桥接(B 侧,可选):满足 §3.6.3 三语义。
    4. 部署/strip:objcopy 分离调试信息(gnu-debuglink)、rsync 增量镜像、带版本号库按 SONAME 命名。
    5. 配置:项目清单 + launch.json 顶层 _ 字段,工具只读。
    6. 连接 .gdb 内容:set sysroot / set solib-search-path(CLI 用) / set substitute-path / set remote exec-file <wrapper> / target extended-remote 127.0.0.1:<动态端口> / abort 抢救块。
    7. launch.json 字段:debugServerPath 指隧道管理器、serverStarted 指 READY 正则、filterStdout: true、additionalSOLibSearchPath 列全 solib 目录、setupCommands 末尾 source 连接 .gdb。

    验证手段可独立于 VSCode:以 DAP 协议直接驱动 cpptools 调试适配器复现 F5(本仓 tools/f5_probe.py 即此思路),断言断点命中且源码定位正确。F5 异常时先跑它定位环节,避免在 VSCode GUI 反复试错。


    7. 术语速查(让其他语言实现者对齐)

    术语 含义
    A / B A=安全机(持源码+符号+gdb);B=不安全机(仅 stripped 二进制+gdbserver)
    RSP GDB Remote Serial Protocol,gdb↔gdbserver 的协议
    MI GDB Machine Interface,cppdbg↔gdb 的协议
    --multi gdbserver 模式,支持 vRun 多次拉起、断连后驻留
    vRun RSP 包,gdbserver 据此 fork+exec inferior(即 set remote exec-file 指向的目标)
    O packet RSP 包,gdbserver 把 inferior stdout 转发回 gdb 端
    solib shared library,gdb 用 solib-search-path 按 basename 查符号
    SONAME .so 的 DT_SONAME,ld.so 按此查找
    Debug Console VSCode 调试控制台,DAP OutputEvent 的落点
    integrated terminal VSCode 集成终端,独立于 Debug Console,inferior 输出的独立显示走这里

    本文档基于本仓库 apps/scripts/remote_debug/ 的实现(Python)整理,但所述原理与语言解耦。若你用 Node.js / Rust / Go 重新实现,只需遵循 §3 各条的机制契约与时序红线即可。欢迎在对应上游 issue 评论区补充指正。

  11. lingr7 commented on Sep 3, 2026

    @lingr7

    上面的方案,pty双向交互是一个不应该支持的功能,也无法支持,会导致wrapper和实际执行的程序之间有断点,导致静态库编译进可执行程序本身的场景无法下断点,
    用 vscode(F5)远程调试程序 A 时,入口函数 main 的断点不命中,程序体内任何一行断点都失效——下断点后状态一直停在 pending,程序直接跑飞。而用同样的方式调试程序 B 时,同样位置的断点完全正常。改成命令行(CLI)方式调试时,A、B 都正常。

    根因(有调试器日志取证,非推测)

    在早期的远程桥接方案里,调试器连接脚本把"远程待执行文件"指向了一个包装脚本(一个 #!/bin/sh 的 shell 脚本)。远程调试服务在拉起目标程序时,是 fork 后 exec 这个"待执行文件"——而它是个脚本,内核会先启动 /bin/sh(在常见发行版上即 dash)来解释脚本。

    于是第一个进程其实是 shell 解释器,调试器把它当成了被调试程序的主可执行文件,去读它的符号表(自然读不到业务符号)。随后脚本内部再 exec 切换到真正的目标程序,但此时调试器已经报过"无法为目标程序加载符号"——主模块的符号此后再没有被重新绑定到正在运行的进程上。

    后果:所有落在主模块上的断点(入口函数、以及被静态链接进主模块的业务代码行)在重设时报"函数未定义",全部转为 pending、永不命中。

    ▎ 现方案已废弃包装脚本:待执行文件直接指向真实目标程序,远程调试服务只用原生机制调整标准库缓冲、不改动 exec 链,此问题不再出现(保留取证记录备查)。

    为什么程序 B 不受影响、偏偏程序 A 中招

    两者的差异在于业务代码放在哪:

    • 程序 B:业务代码在动态共享库里。共享库的断点靠"库加载"事件重新绑定。上面那个 shell → 真实程序 的 exec 切换会触发一整轮库加载事件,共享库断点顺带被正常重绑,所以 B 不受影响。
    • 程序 A:是胖可执行文件——业务代码被静态链接进了主模块。主模块的断点不走"库加载"重绑这条路,exec 切换后没有任何机制把它绑回去,于是这一类断点全部失效。

    这是"胖可执行文件"与"普通可执行文件"在该链路下的本质差异:胖 exe 的业务行落在主模块,而主模块断点恰好是 exec 切换后无人重绑的那一类。

    新方案如下:
    机制受限于"GDB over ssh + gdbserver"这个底层模型,存在若干无法在当前架构内消除的本质缺陷:

    不支持被调试程序读 stdin。这是被认定为"本质限制"而非待办的缺陷:
    CLI(stdio pipe)模式下,gdbserver 的 stdio 被 GDB Remote Serial Protocol 作为传输通道占用,inferior 的 stdin 无从接入;
    F5(TCP 隧道)模式下,gdbserver –multi 的 inferior stdout/stderr 可落日志供 console 子命令 tail -F 单向只读,但 RSP 没有 inferior stdin 的反向通道。
    旧的 pty/wrapper 双向方案曾支持 stdin,但 set remote exec-file 须指向 wrapper 导致胖 exe断点全挂,已废弃。console.py 与 README 明确注明"RSP 本就无 inferior stdio 通道,stdin 交互无源"。
    仅支持 GDB,不支持 LLDB。整条链路(pygdbmi、MIEngine cppdbg、gdbserver、target extended-remote、checks.py 硬检 gdb/objcopy/readelf)全部针对 GDB,MIMode 固定 gdb。
    attach 模式受限:CLI 支持 –attach (gdbserver –attach),但 F5/TCP 隧道模式固定 gdbserver –multi,不支持 attach 到已运行进程。
    强依赖 ssh ControlMaster 与本地端口转发:硬编码 ControlMaster=auto + ControlPath + ControlPersist=300,隧道用 ssh -N -L。B 侧 sshd 禁用 ControlMaster 或防火墙限制 localhost 转发时机制直接失效;异常退出会留孤儿隧道(有 kill-stale/clean –all 兜底,按判活清理,但不杜绝)。
    无符号服务器 / 源码索引集成:符号源依赖 A 本地未 strip 产物 + solib-search-path + substitute-path,无 debuginfod 或源码索引服务器。
    A/B 系统库一致性强依赖:A/B glibc 版本不同会导致 build-id 不符、GDB 拒读符号,需镜像 B 系统库到 A 作 sysroot 缓解,但未消除。
    单会话语义:一个 F5 会话调试一个 target,不支持一会话内并发调试多进程;多人共享 B 同部署目录仅靠 sid+pid 隔离日志。
    非 TTY 下 stdout 全缓冲:glibc 全缓冲导致 abort 丢日志,靠 break abort + fflush(0) 抢救块缓解,属远程调试固有缺陷。
    这些缺陷共同源于一个根本取舍:为了保留 set remote exec-file 直指真 exe 的纯净 exec 链(保证断点精度与胖 exe 可调试),放弃了任何 inferior stdio 代理通道。在该模型下,stdin 不可逆地丧失;其余限制则是 GDB/gdbserver + ssh 隧道工具栈的固有边界

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    debuggerinvestigate: reproThis issue's repro steps needs to be investigated/confirmed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions