Rust in Everything
基于 probe-rs-tools 0.32.0

CLI 的 RPC 架构

主线位置:命令主线的骨架。一条 probe-rs run 讲过 run_app 在本进程里起一个 RPC 服务端、命令通过它操作目标;本页拆开这层 RPC:协议怎么定义、Session 放在哪、长时间的操作怎么一边跑一边往回推数据,以及开了 remote feature 之后同一套东西怎么跨机器。

flowchart TB
  subgraph client["客户端:命令的代码"]
    cmd["Cmd::run"] --> rc["RpcClient:postcard-rpc HostClient"]
  end
  subgraph server["服务端:RpcApp"]
    disp["define_dispatch:31 个 endpoint"] --> ctx["RpcContext:ConnectionState"]
    ctx --> store["ObjectStorage:Session、FlashLoader、RttClient"]
    disp --> blk["spawn_blocking:调同步的 probe-rs 库"]
    blk --> pub["topic:flash/progress、rtt、semihosting、info/data"]
  end
  rc -->|"本地:两条 tokio mpsc"| disp
  rc -->|"remote:websocket 或 unix socket"| disp
  pub --> rc

rpc/ 一共 6 373 行。远程服务端在 cmd/serve.rs(340 行),只在 remote feature 下编译;本机安装的 0.32.0 没开这个 feature(源码地图),probe-rs --help 里既没有 serve 子命令也没有 --host / --token 参数,所以本页的远程部分只读源码。

协议:endpoint 和 topic

协议用的是 postcard-rpc 0.12:请求和响应都是 postcard 编码的 Rust 类型,按「路径」区分,类型带 postcard_schema::Schema,两端编译期就对得上。全部定义在 rpc/functions.rs 的三张表里:

表内容
endpoints!(459–501)31 个请求 / 响应对:probe/list、probe/select、probe/attach;resume、create_rtt、rtt/down、stack_trace;flash/build、flash/flash、flash/erase、flash/verify、monitor;tests/list、tests/run;temp_file/new、temp_file/append;chips/list、chips/info、chips/load;target、info、reset、reset_and_halt;memory/read8 到 read64、memory/write8 到 write64
topics!(503–520)服务端到客户端 4 个:info/data、flash/progress、rtt、semihosting;客户端到服务端 1 个:cancel
define_dispatch!(522–583)每个 endpoint 的处理函数和执行方式:blocking(只有 probe/list)、async(大多数),spawn(monitor、tests/list、tests/run)

monitor 和测试是 spawn:处理函数被放进单独的任务,服务端的分发循环不被它占住,于是在 monitor 运行期间,客户端仍然能发 rtt/down(往 down channel 写数据,0.32 新增)和 cancel(Ctrl-C)。

响应统一是 RpcResult<T>,错误只是一个字符串(RpcError(String));客户端 send_resp 把它变回 anyhow 错误。所以 probe/select 返回的 NoProbesFound 到客户端只剩一句话,类型信息在边界上丢掉了。

Session 留在服务端

Session 不能序列化,也不该跨进程移动。服务端的做法是放进一个按类型擦除的对象仓库,只把一个 key 交给客户端(rpc/mod.rs):

  • Key<T> 是一个全局递增的 u64 加 PhantomData<T>,带 Schema,可以放进请求里。类型参数只在编译期区分「这是 Session 的 key」和「这是 FlashLoader 的 key」,线上就是一个整数。
  • ObjectStorage 是 HashMap<u64, Arc<Mutex<dyn Any + Send>>>,取的时候按 T downcast。
  • 每条连接一个 ConnectionState:对象仓库、一份服务端自己的 Registry、一个取消令牌,以及哪些 Session 是 dry-run 的集合。

probe/attach 的处理函数最后一行就是 ctx.set_session(session, common_options.dry_run()),返回的 Key<Session> 交回客户端(rpc/functions/probe.rs:296)。之后 flash/build 返回 Key<FlashLoader>,create_rtt 返回 RTT 客户端的 key,都是同一个套路。

probe-rs 库是同步的。处理函数要调库时走 run_blocking:

// probe-rs-tools/src/bin/probe-rs/rpc/functions.rs
        let token = self.cancellation_token();
        let (sender, publisher) = T::create(token);

        let ctx = self.clone();
        let blocking = tokio::task::spawn_blocking(move || task(ctx, request, sender));

        tokio::select! {
            _ =  publisher.publish(&self.sender) => unreachable!(),
            response = blocking => {
                response.unwrap()
            }
        }

同步代码在 tokio 的阻塞线程池里跑,手里拿着一个普通的 mpsc::Sender;另一边 publisher 把发进来的消息作为 topic 推给客户端。烧录进度、RTT 数据、semihosting 输出都是这样一边跑一边推的。Session 在仓库里由 tokio::sync::Mutex 保护,阻塞线程里用 blocking_lock 取。

客户端怎么收:send_and_read_stream 先订阅 topic,再发请求,两者 join,请求返回时通知流结束。monitor 的 RttEvent::Output 就这样到达客户端,defmt 在客户端解码。

本地:两条内存通道

RpcApp::create_server 建两条 tokio mpsc(深度由调用方给,本地是 16),一个 1 MiB 的接收缓冲区,返回「服务端、客户端到服务端的发送端、服务端到客户端的接收端」三样。本地模式下 run_app 把后两样直接交给 RpcClient::new_local_from_wire,中间没有 socket,但消息仍然完整地经过 postcard 编码和解码。

new_local_from_wire 唯一多做的事是把 is_localhost 设成 true,它影响文件怎么传:upload_file 在本地直接返回原路径;远程时先 temp_file/new 在服务端建临时文件,再按 512 KiB 一块 temp_file/append 传过去,同一个文件只传一次。ELF、ESP-IDF 的 bootloader 和分区表都走它。--chip-description-path 给的 YAML 则是读成字符串,用 chips/load 发给服务端的那份 Registry(util/cli.rs:89)。

哪些子命令走 RPC

Cli::run 拿到的是 RpcClient,但不是每个子命令都用它;is_remote_cmd 列出了「完全由 RPC 调用实现」的那些,注释是「TODO: refactor other commands」:

类别子命令说明
全走 RPC,可远程list、info、reset、read、write、chip、download、run、attach、erase、verify;mi 的一部分远程模式下只有这些能用,其他命令报 The subcommand is not supported in remote mode.
拿 RpcClient 但不在列表里debug本地可用,远程被拒
只借客户端里的 Registry,自己直接调库gdb、trace、itm、benchmark、profilesimple_attach 直接建 Session;gdb --dry-run 有效就是因为这个(gdb server)
完全不碰 RPCdap-server、completeDAP server 自己建 Session(DAP server)

不管走不走,run_app 都会先起本地服务端,命令结束后等它关闭;对 gdb 这类命令,那个服务端从头到尾是空转的。

两个 multicall 入口也分属两边(multicall 本身见源码地图):cargo-flash 的 main 调 run_app,走 RPC(cmd/cargo_flash/mod.rs:106);cargo-embed 用 simple_attach 直接建 Session(cmd/cargo_embed/mod.rs:233),不走。

remote:同一套协议换一根线

开了 remote 之后,全局参数多出 --host(环境变量 PROBE_RS_REMOTE_HOST)和 --token(PROBE_RS_REMOTE_TOKEN)(main.rs:76-94),run_app 改调 rpc::client::connect。服务端是 probe-rs serve,配置来自配置文件的 server 段(ServerConfig:用户列表、地址、端口,以及 cycle_power:启动时打开所有 USB hub 的端口供电控制,只在 Linux 上实现)。两种传输:

sequenceDiagram
  participant C as 客户端 connect
  participant S as probe-rs serve
  C->>S: GET /worker(websocket 升级)
  S-->>C: 101,头 Probe-Rs-Challenge:64 字节随机数的 base64
  C->>S: 第一帧:SHA-512(challenge 加 token)
  S->>S: 逐个用户算一遍,比对
  S->>S: create_server(16, 该用户的 ProbeAccess)
  C->>S: postcard-rpc 帧,websocket 二进制消息
  S-->>C: 响应与 topic
  • websocket(run_tcp):axum 默认监听 0.0.0.0:3000,/ 是一个列出探针的状态页,/worker 升级成 websocket。认证是上图的挑战应答(handle_socket),每个 postcard 帧是一条二进制消息。服务端本身不做 TLS;客户端在 wss:// 时用 rustls,但证书校验器是 NoCertificateVerification,源码注释是「TODO: implement something more secure」。挑战用 fastrand 生成,注释说明不需要密码学安全的随机数,「just something to make replays harder」。
  • unix socket(run_unix):地址写成 socket://<路径> 时启用,socket 文件权限设成 0o660,靠文件权限控制访问,没有 token 认证,ProbeAccess::All。帧格式是 4 字节小端长度加 postcard 帧(transport/unix.rs)。

每个用户可以配一个 ProbeAccess:All、Allow(列表) 或 Deny(列表),列表项是 VID:PID 或 VID:PID:序列号。服务端用 LimitedLister 包住探针枚举,不在许可范围内的探针既列不出来也打不开。这让一台插着多个探针的机器可以分给不同的人用。

远程时 defmt 解码仍在客户端,服务端只转发 RTT 原始字节,所以服务端不需要固件的调试信息;但烧录要用 ELF,flash/build 之前客户端会先把 ELF 上传过去。远程模式的延迟、websocket 上 RTT 的吞吐,待实测(需要两台机器和真探针)。

小结

  • 命令行内部是 postcard-rpc:31 个 endpoint、4 个出向 topic 和 1 个入向 topic,全在 rpc/functions.rs 的三张宏表里;错误在边界上变成字符串。
  • Session、FlashLoader、RTT 客户端都留在服务端的对象仓库里,客户端只拿 Key<T>;同步的库调用进 spawn_blocking,进度和日志作为 topic 一边跑一边推。
  • 本地模式是两条内存通道,消息照样编解码;is_localhost 只决定文件是传路径还是上传。
  • 只有 11 个子命令(加 mi 的一部分)完全走 RPC、能远程;gdb、dap-server、trace、benchmark、profile 和 cargo-embed 直接调库。
  • remote feature 下 probe-rs serve 提供 websocket(挑战应答认证、按用户限定探针)和 unix socket(靠文件权限)两种接入;本机的 0.32.0 没开这个 feature。

💬 关联讨论

发起讨论
加载中...