执行与集成 sidecar 服务器
这一阶段像给主程序配一组“外挂机器”,主要在幕后支撑执行和工具接入。exec-server 负责开门、选通道、收发 JSON-RPC(一种固定格式的命令消息),客户端把启动进程、读文件、发请求包装成简单操作。Noise relay 负责远程加密中转,stdio、WebSocket、本地 socket 像不同插头被统一接上。MCP 启动外部工具服务器并整理工具清单;HTTP 和网络代理则管转发、鉴权和出网规则,防止乱连。
Exec server 传输基础
这些文件定义 exec-server crate 接口,以及客户端和服务器共同使用的共享 JSON-RPC 传输基础组件。
exec-server/src/lib.rs源码 ↗
可以把这个文件想成一本书的目录和前台接待。项目里面有客户端、服务端、远程连接、文件系统、进程执行、协议消息、运行路径等很多模块;如果每个使用者都要自己去找这些内部文件,会很乱,也容易依赖到不该依赖的细节。这个文件先用 mod 声明这些内部模块,让编译器知道它们属于这个库;再用 pub use 把常用能力摆到库的入口处。这样别人只要引用 exec-server,就能拿到 ExecServerClient、文件系统接口、进程执行接口、协议参数、远程环境启动函数等核心工具。它本身不实现具体业务,也没有函数;它的重要性在于稳定“对外门牌号”。内部文件可以调整位置,但只要这里继续导出同样的名字,外部代码就不用跟着大改。
exec-server/src/connection.rs源码 ↗
这里像一个“通信接线员”。JSON-RPC 是一种把请求和回复写成 JSON 文本的通信格式;stdio 是程序的标准输入输出;WebSocket 是网页和服务器之间常用的长连接。这个文件把这些不同通道都变成同一种 JsonRpcConnection:外面只要往 outgoing_tx 放消息、从 incoming_rx 收事件即可。它会在后台开异步任务:一边读入并解析消息,一边把要发出的消息写出去。读到坏 JSON 时会报告“格式错误”,读写失败或对方关闭时会报告“断开”。如果连接背后带着一个子进程,它还会在结束时先温和通知退出,等一小会儿,不行再强杀整棵进程树,避免留下僵尸进程。文件后半部分是测试用的假 WebSocket 和用例,专门验证心跳、关闭、二进制消息和写入阻塞时不会丢消息。
JsonRpcTransport::from_child_process53–57 ↗
fn from_child_process(child_process: Child) -> Self
作用:把一个已经启动的子进程包装成连接的底层传输。这样连接结束时,系统知道还要照看并清理这个子进程。
数据流:进去的是一个子进程句柄 → 它调用 StdioTransport::spawn 建立子进程看护器 → 出来的是 JsonRpcTransport::Stdio,里面带着可终止的子进程传输。
调用关系:JsonRpcConnection::with_child_process 会用它给已有连接挂上子进程生命周期;它把真正启动看护任务的工作交给 StdioTransport::spawn。
调用图:调用 1 个内部函数(spawn);被 1 处调用(with_child_process)。
JsonRpcTransport::terminate59–64 ↗
fn terminate(&self)
作用:请求底层传输停止。普通连接什么都不用做;带子进程的连接会通知子进程退出。
数据流:进去的是当前 transport 自身 → 它判断是 Plain 还是 Stdio → Plain 不改动,Stdio 会调用内部 transport 的 terminate 发出终止信号。
调用关系:它是连接清理时的总开关;根据调用图,它会在 drop 清理阶段被用到,把退出请求传给 StdioTransport。
调用图:被 1 处调用(drop)。
StdioTransport::spawn78–86 ↗
fn spawn(child_process: Child) -> Self
作用:为一个 stdio 子进程建立“看门人”。看门人会监听:子进程自己退出了,还是外部要求它退出。
数据流:进去的是子进程句柄 → 它建立一个 watch 通道(一种广播最新状态的小信号线),保存终止发送端,并启动 supervisor → 出来的是可克隆的 StdioTransport。
调用关系:JsonRpcTransport::from_child_process 会调用它;它再把长期监控工作交给 spawn_stdio_child_supervisor。
调用图:调用 1 个内部函数(spawn_stdio_child_supervisor);被 1 处调用(from_child_process);外部调用 3 个(new, new, channel)。
StdioTransport::terminate88–90 ↗
fn terminate(&self)
作用:对外提供一个简单按钮:请停止这个 stdio 子进程。它隐藏了内部信号通道和只发送一次的细节。
数据流:进去的是 StdioTransport 自身 → 它找到共享的 handle → handle 发出终止请求,状态从“未请求”变成“已请求”。
调用关系:JsonRpcTransport::terminate 会把 Stdio 类型的终止请求交给它;它再委托 StdioTransportHandle::terminate。
StdioTransportHandle::terminate94–98 ↗
fn terminate(&self)
作用:真正发送“该退出了”的信号,并保证只发一次。这样多处同时清理时,不会重复敲同一个退出铃。
数据流:进去的是共享 handle → 它用原子布尔值(一种线程安全的真假开关)检查是否已经请求过 → 第一次会通过 watch 通道发送 true,之后再调用就不做事。
调用关系:StdioTransport::terminate 和 StdioTransportHandle::drop 都会走到这里;它发出的信号会被 wait_for_stdio_termination 看到。
StdioTransportHandle::drop102–104 ↗
fn drop(&mut self)
作用:兜底清理:当最后一个 handle 被丢弃时,自动请求子进程退出。避免连接对象没显式关闭时留下后台进程。
数据流:进去的是即将销毁的 handle → 它调用 terminate → 如果还没发过终止信号,就发送一次。
调用关系:这是 Rust 的 Drop 清理钩子;它把销毁对象这个时机转成一次 StdioTransportHandle::terminate 调用。
调用图:调用 1 个内部函数(terminate)。
spawn_stdio_child_supervisor107–120 ↗
fn spawn_stdio_child_supervisor(mut child_process: Child, mut terminate_rx: watch::Receiver<bool>)
作用:启动一个后台看护任务,专门盯着 stdio 子进程。它负责在“子进程自然结束”和“外部要求结束”两种情况里做正确清理。
数据流:进去的是子进程和终止信号接收端 → 它记录进程组 ID,并启动 tokio 异步任务 → 任务要么等待子进程退出后清理进程树,要么收到终止信号后走温和关闭流程。
调用关系:StdioTransport::spawn 创建 transport 时会调用它;它内部会用 wait_for_stdio_termination、terminate_stdio_child、kill_process_tree 和 log_stdio_child_wait_result。
wait_for_stdio_termination122–131 ↗
async fn wait_for_stdio_termination(terminate_rx: &mut watch::Receiver<bool>)
作用:等待那根“请退出”的信号线变成 true。信号线断掉时也返回,因为这通常表示没人再需要这个子进程了。
数据流:进去的是 watch 接收端 → 它循环查看当前值,若不是 true 就等待变化 → 出来时代表已请求终止,或发送端已关闭。
调用关系:spawn_stdio_child_supervisor 的后台任务会等待它;等到以后就进入 terminate_stdio_child 做真正的进程关闭。
调用图:外部调用 2 个(borrow, changed)。
terminate_stdio_child133–144 ↗
async fn terminate_stdio_child(child_process: &mut Child, process_group_id: Option<u32>)
作用:按“先礼后兵”的方式关闭 stdio 子进程。先温和终止,等两秒,还不走就强杀。
数据流:进去的是子进程和可选进程组 ID → 它先调用 terminate_process_tree 发温和终止 → 等待子进程退出;超时就调用 kill_process_tree 强制杀掉,再记录等待结果。
调用关系:spawn_stdio_child_supervisor 在收到终止信号后调用它;它会把具体平台相关的杀进程细节交给 terminate_process_tree 和 kill_process_tree。
调用图:调用 3 个内部函数(kill_process_tree, log_stdio_child_wait_result, terminate_process_tree);外部调用 2 个(wait, timeout)。
terminate_process_tree146–168 ↗
fn terminate_process_tree(child_process: &mut Child, process_group_id: Option<u32>)
作用:尽量温和地让一个子进程及它带出的子进程一起退出。像先请整桌人离场,而不是只赶走带头的人。
数据流:进去的是子进程和进程组 ID → 有进程组就按系统方式终止整组;没有或失败时就退回到 kill_direct_child → 结果是发出了终止请求,并可能记录警告。
调用关系:terminate_stdio_child 先调用它;在 Unix 上用进程组终止,在 Windows 上用 taskkill,失败时交给 kill_direct_child。
调用图:调用 3 个内部函数(kill_direct_child, kill_windows_process_tree, terminate_process_group);被 1 处调用(terminate_stdio_child);外部调用 1 个(warn!)。
kill_process_tree170–191 ↗
fn kill_process_tree(child_process: &mut Child, process_group_id: Option<u32>)
作用:强制杀掉子进程树,用在温和退出无效或子进程已经结束但还要清理残余时。
数据流:进去的是子进程和进程组 ID → 有进程组就尝试杀整组;没有进程组或 Windows 方法失败时杀直接子进程 → 结果是尽力清掉相关进程。
调用关系:terminate_stdio_child 超时后会调用它;spawn_stdio_child_supervisor 在子进程 wait 返回后也会调用它做兜底。
调用图:调用 3 个内部函数(kill_direct_child, kill_windows_process_tree, kill_process_group);被 1 处调用(terminate_stdio_child);外部调用 1 个(warn!)。
kill_direct_child193–197 ↗
fn kill_direct_child(child_process: &mut Child, action: &str)
作用:只对直接子进程发强制停止请求。它是进程组处理失败时的最后兜底。
数据流:进去的是子进程句柄和动作名字 → 它调用 start_kill → 成功就结束,失败就写调试日志。
调用关系:terminate_process_tree 和 kill_process_tree 都会在无法处理整棵进程树时调用它。
调用图:被 2 处调用(kill_process_tree, terminate_process_tree);外部调用 2 个(start_kill, debug!)。
kill_windows_process_tree200–215 ↗
fn kill_windows_process_tree(pid: u32) -> bool
作用:Windows 专用:用系统自带的 taskkill 命令杀掉某个进程和它的子进程。
数据流:进去的是进程 ID → 它执行 taskkill /PID ... /T /F,并关闭标准输入输出 → 出来是 true 或 false,表示命令是否成功。
调用关系:terminate_process_tree 和 kill_process_tree 在 Windows 上会调用它;失败时调用方会退回到 kill_direct_child。
调用图:被 2 处调用(kill_process_tree, terminate_process_tree);外部调用 3 个(null, new, warn!)。
log_stdio_child_wait_result217–221 ↗
fn log_stdio_child_wait_result(result: std::io::Result<std::process::ExitStatus>)
作用:记录等待子进程退出时发生的错误。正常退出不打扰日志,只有等失败时才写调试信息。
数据流:进去的是 wait 的结果 → 如果是错误就写 debug 日志 → 不返回业务数据,只留下诊断信息。
调用关系:terminate_stdio_child 在等待子进程结束后调用它;spawn_stdio_child_supervisor 的自然退出分支也会用它。
调用图:被 1 处调用(terminate_stdio_child);外部调用 1 个(debug!)。
JsonRpcConnection::from_stdio232–321 ↗
fn from_stdio(reader: R, writer: W, connection_label: String) -> Self
作用:从一对读写流创建 JSON-RPC 连接,常用于和命令行子进程通过标准输入输出通信。
数据流:进去的是 reader、writer 和连接名字 → 它建立发出队列、接收队列、断开状态,并启动读任务和写任务 → 出来的是 JsonRpcConnection,上层可发送消息、接收消息或断开事件。
调用关系:很多 stdio 场景和测试都会调用它,比如 connect_stdio_command、run_stdio_connection_with_io 等;读任务会用 send_malformed_message 和 send_disconnected,写任务会用 write_jsonrpc_line_message。
调用图:调用 3 个内部函数(send_disconnected, send_malformed_message, write_jsonrpc_line_message);被 7 处调用(process_events_are_delivered_in_seq_order_when_notifications_are_reordered, transport_disconnect_fails_sessions_and_rejects_new_sessions, wake_notifications_do_not_block_other_sessions, connect_stdio_command, rpc_client_matches_out_of_order_responses_by_request_id, spawn_test_connection, run_stdio_connection_with_io);外部调用 8 个(new, new, Message, format!, channel, spawn, vec!, channel)。
JsonRpcConnection::from_websocket323–328 ↗
fn from_websocket(stream: WebSocketStream<S>, connection_label: String) -> Self
作用:从 tokio-tungstenite 的 WebSocket 创建 JSON-RPC 连接。适合普通 WebSocket 客户端或服务端通道。
数据流:进去的是 WebSocket 流和连接名字 → 它不启用额外心跳,转交给通用的 from_websocket_stream → 出来的是统一的 JsonRpcConnection。
调用关系:connect_websocket 和多个 WebSocket 测试会调用它;真正的收发循环由 from_websocket_stream 实现。
调用图:被 4 处调用(connect_websocket, websocket_connection_accepts_binary_jsonrpc_message, websocket_connection_ignores_server_pong, websocket_connection_reports_server_close);外部调用 1 个(from_websocket_stream)。
JsonRpcConnection::from_axum_websocket330–332 ↗
fn from_axum_websocket(stream: AxumWebSocket, connection_label: String) -> Self
作用:从 Axum 框架的 WebSocket 创建 JSON-RPC 连接,并启用定时 ping 心跳。Axum 是 Rust 里的一个 Web 服务框架。
数据流:进去的是 Axum WebSocket 和连接名字 → 它带上默认心跳间隔,调用 from_websocket_stream → 出来的是会定期发 ping 的 JsonRpcConnection。
调用关系:它是 HTTP/Web 服务入口接到 WebSocket 后的适配层;底层仍交给 from_websocket_stream 统一处理。
调用图:外部调用 1 个(from_websocket_stream)。
JsonRpcConnection::from_websocket_stream334–458 ↗
fn from_websocket_stream(
mut websocket: T,
connection_label: String,
ping_interval: Option<Duration>,
) -> Self
作用:WebSocket 连接的核心工厂。它把“发消息、收消息、心跳、断开报告”放进同一个后台循环。
数据流:进去的是任意符合接口的 WebSocket、连接名字和可选 ping 间隔 → 它建立通道并启动一个异步任务,在发送队列、心跳定时器、收到的 WebSocket 帧之间轮流处理 → 出来的是 JsonRpcConnection。
调用关系:from_websocket、from_axum_websocket 和测试都会调用它;循环中会解析 JsonRpcWebSocketMessage,也会用 send_websocket_jsonrpc_message、send_disconnected、send_malformed_message。
调用图:被 2 处调用(websocket_connection_keeps_outbound_message_while_send_is_backpressured, websocket_connection_sends_configured_ping);外部调用 5 个(channel, select!, spawn, vec!, channel)。
JsonRpcConnection::with_child_process460–463 ↗
fn with_child_process(mut self, child_process: Child) -> Self
作用:给已经建好的连接挂上一个子进程。这样连接对象不仅管消息,也会管这个子进程的退出。
数据流:进去的是连接自身和子进程 → 它把 transport 从 Plain 改成 Stdio 子进程传输 → 出来的是更新后的连接。
调用关系:创建 stdio 命令连接时会在连接建好后调用它;它使用 JsonRpcTransport::from_child_process 来启动子进程看护。
调用图:调用 1 个内部函数(from_child_process)。
Message::parse_jsonrpc_frame479–492 ↗
fn parse_jsonrpc_frame(self) -> Result<JsonRpcWebSocketFrame, serde_json::Error>
作用:把 tungstenite WebSocket 消息解释成 JSON-RPC 帧。文本和二进制会尝试解析,关闭帧会标记为断开,其它心跳类帧会忽略。
数据流:进去的是一个 WebSocket Message → 文本走 JSON 字符串解析,二进制走 JSON 字节解析,Close 变成 Close,Ping/Pong/Frame 变成 Ignore → 出来是消息、关闭、忽略,或 JSON 解析错误。
调用关系:from_websocket_stream 收到 WebSocket 帧后会靠这个 trait 方法判断下一步;它让不同 WebSocket 类型能共用同一套连接循环。
调用图:外部调用 2 个(from_slice, from_str)。
Message::from_text494–496 ↗
fn from_text(text: String) -> Self
作用:把一段已经序列化好的 JSON 文本包装成 tungstenite 的文本 WebSocket 消息。
数据流:进去的是字符串 → 它放进 Message::Text → 出来的是可直接发送的 WebSocket 文本帧。
调用关系:send_websocket_jsonrpc_message 通过这个 trait 方法生成要发出的帧;这样发送逻辑不用关心具体 WebSocket 消息类型。
调用图:外部调用 1 个(Text)。
Message::ping498–500 ↗
fn ping() -> Self
作用:生成一个空内容的 WebSocket ping。ping 像“你还在吗”的敲门声,用来保持连接活跃。
数据流:进去不需要业务数据 → 它创建空字节内容并包装成 Message::Ping → 出来的是可发送的 ping 帧。
调用关系:from_websocket_stream 的心跳定时分支会调用这个 trait 方法发送保活消息。
调用图:外部调用 2 个(Ping, new)。
AxumWebSocketMessage::parse_jsonrpc_frame504–517 ↗
fn parse_jsonrpc_frame(self) -> Result<JsonRpcWebSocketFrame, serde_json::Error>
作用:把 Axum 的 WebSocket 消息解释成 JSON-RPC 帧。它和 tungstenite 版本做同样的事,只是消息类型不同。
数据流:进去的是 Axum WebSocket 消息 → 文本或二进制尝试解析成 JSONRPCMessage,Close 标记关闭,Ping/Pong 忽略 → 出来是 Message、Close、Ignore 或解析错误。
调用关系:from_axum_websocket 最终走 from_websocket_stream;这个方法让通用循环能读懂 Axum 的消息格式。
调用图:外部调用 2 个(from_slice, from_str)。
AxumWebSocketMessage::from_text519–521 ↗
fn from_text(text: String) -> Self
作用:把 JSON 文本包装成 Axum 的 WebSocket 文本消息,用来发给浏览器或 HTTP 客户端。
数据流:进去的是字符串 → 它转换为 AxumWebSocketMessage::Text → 出来的是可发送的 Axum WebSocket 帧。
调用关系:send_websocket_jsonrpc_message 通过 trait 调它;通用发送函数因此能同时支持 Axum 和 tungstenite。
调用图:外部调用 1 个(Text)。
AxumWebSocketMessage::ping523–525 ↗
fn ping() -> Self
作用:生成 Axum WebSocket 的空 ping 帧,用来做连接保活。
数据流:进去不需要业务数据 → 它创建空字节 ping → 出来的是 AxumWebSocketMessage::Ping。
调用关系:from_websocket_stream 在启用 ping_interval 时会定时调用它发送心跳。
调用图:外部调用 2 个(Ping, new)。
send_disconnected528–537 ↗
async fn send_disconnected(
incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>,
disconnected_tx: &watch::Sender<bool>,
reason: Option<String>,
)
作用:统一报告“连接断了”。它既更新一个可观察的断开状态,也往事件队列里塞一条断开事件。
数据流:进去的是事件发送队列、断开状态发送端和可选原因 → 它把 disconnected 设为 true,再发送 Disconnected 事件 → 外部等待者会看到连接已断。
调用关系:from_stdio 的读写任务在 EOF、读写失败时会调用它;from_websocket_stream 的各类断开分支也使用同样的报告方式。
调用图:被 1 处调用(from_stdio);外部调用 1 个(send)。
send_malformed_message539–548 ↗
async fn send_malformed_message(
incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>,
reason: Option<String>,
)
作用:统一报告“收到的消息格式不对”。连接不一定马上断,但上层会知道对方发来了坏消息。
数据流:进去的是事件发送队列和可选原因 → 没给原因就补一个默认说明 → 发出 MalformedMessage 事件。
调用关系:from_stdio 解析一行 JSON 失败时会调用它;from_websocket_stream 解析 WebSocket 里的 JSON-RPC 失败时也用它。
调用图:被 1 处调用(from_stdio);外部调用 1 个(send)。
write_jsonrpc_line_message550–562 ↗
async fn write_jsonrpc_line_message(
writer: &mut BufWriter<W>,
message: &JSONRPCMessage,
) -> std::io::Result<()>
作用:把一个 JSON-RPC 消息写成“一行 JSON 加换行”。这是 stdio 通信常见的分隔方式:一行就是一条消息。
数据流:进去的是缓冲写入器和消息 → 它先序列化成 JSON 字符串,再写字符串、写换行、刷新缓冲区 → 成功返回空结果,失败返回 IO 错误。
调用关系:JsonRpcConnection::from_stdio 的写任务每拿到一条 outgoing 消息就调用它;它内部依赖 serialize_jsonrpc_message。
调用图:调用 1 个内部函数(serialize_jsonrpc_message);被 1 处调用(from_stdio);外部调用 2 个(flush, write_all)。
send_websocket_jsonrpc_message564–585 ↗
async fn send_websocket_jsonrpc_message(
websocket_writer: &mut W,
connection_label: &str,
message: &JSONRPCMessage,
) -> Result<(), String>
作用:把 JSON-RPC 消息发到 WebSocket 上,并把序列化或发送失败变成好读的错误文字。
数据流:进去的是 WebSocket 写端、连接名字和消息 → 它先序列化消息,再包装成文本 WebSocket 帧并发送 → 成功返回 Ok,失败返回包含连接名的错误字符串。
调用关系:from_websocket_stream 的发送分支会用它;它调用 serialize_jsonrpc_message 和具体消息类型的 from_text。
调用图:调用 1 个内部函数(serialize_jsonrpc_message);外部调用 3 个(from_text, send, format!)。
serialize_jsonrpc_message587–589 ↗
fn serialize_jsonrpc_message(message: &JSONRPCMessage) -> Result<String, serde_json::Error>
作用:把内存里的 JSON-RPC 消息变成 JSON 字符串。所有向外发送的消息都要先过这一步。
数据流:进去的是 JSONRPCMessage → 它调用 serde_json 转字符串 → 出来是 JSON 文本,或序列化错误。
调用关系:write_jsonrpc_line_message 和 send_websocket_jsonrpc_message 都依赖它;它是 stdio 和 WebSocket 发送路径的共同小工具。
调用图:被 2 处调用(send_websocket_jsonrpc_message, write_jsonrpc_line_message);外部调用 1 个(to_string)。
tests::websocket_connection_sends_configured_ping612–627 ↗
async fn websocket_connection_sends_configured_ping() -> anyhow::Result<()>
作用:测试启用心跳后,WebSocket 连接真的会按配置发 ping。这样能防止保活机制被改坏。
数据流:进去的是测试创建的一对 WebSocket → 它用较短 ping 间隔建连接,然后等待服务端收到消息 → 断言收到的是 Ping。
调用关系:它调用 websocket_pair 建测试连接,并直接使用 from_websocket_stream 设置心跳间隔。
调用图:调用 1 个内部函数(from_websocket_stream);外部调用 4 个(from_secs, assert!, websocket_pair, timeout)。
tests::websocket_connection_ignores_server_pong630–645 ↗
async fn websocket_connection_ignores_server_pong() -> anyhow::Result<()>
作用:测试收到 Pong 时不会被当成业务消息。Pong 是心跳回复,不该打扰 JSON-RPC 事件流。
数据流:进去的是一对测试 WebSocket → 服务端发 Pong → 客户端连接的 incoming_rx 在短时间内应收不到事件。
调用关系:它通过 from_websocket 创建连接,用 websocket_pair 搭环境,验证 parse_jsonrpc_frame 的 Ignore 行为。
调用图:调用 1 个内部函数(from_websocket);外部调用 3 个(assert!, websocket_pair, Pong)。
tests::websocket_connection_reports_server_close648–660 ↗
async fn websocket_connection_reports_server_close() -> anyhow::Result<()>
作用:测试服务端关闭 WebSocket 时,连接会向上报告断开事件。
数据流:进去的是一对测试 WebSocket → 服务端主动 close → 客户端 incoming_rx 应收到 Disconnected 且没有错误原因。
调用关系:它调用 from_websocket 建连接;验证 from_websocket_stream 收到 Close 后会走 send_disconnected。
调用图:调用 1 个内部函数(from_websocket);外部调用 2 个(assert!, websocket_pair)。
tests::websocket_connection_accepts_binary_jsonrpc_message663–683 ↗
async fn websocket_connection_accepts_binary_jsonrpc_message() -> anyhow::Result<()>
作用:测试 WebSocket 的二进制帧里放 JSON-RPC 也能被接受。这样对方不只限于发文本帧。
数据流:进去的是测试连接和一个 JSON-RPC 请求 → 服务端把请求序列化成字节并以 Binary 发送 → 客户端应收到同样的 JSONRPCMessage。
调用关系:它用 from_websocket 和 websocket_pair;重点覆盖 Message::parse_jsonrpc_frame 的 Binary 分支。
调用图:调用 1 个内部函数(from_websocket);外部调用 6 个(Request, Integer, assert!, websocket_pair, to_vec, Binary)。
tests::websocket_connection_keeps_outbound_message_while_send_is_backpressured686–713 ↗
async fn websocket_connection_keeps_outbound_message_while_send_is_backpressured() -> anyhow::Result<()>
作用:测试写入暂时被卡住时,待发送消息不会丢。背压就是“对方或底层暂时接不住,发送方要等一下”。
数据流:进去的是一个可控假 WebSocket,初始不可写 → 连接发送一条 JSON-RPC,等待写入阻塞,再插入一个 Pong 干扰,最后放开写入 → 出站接收端应拿到原消息。
调用关系:它调用 ControlledWebSocket::new、from_websocket_stream、test_jsonrpc_message;验证发送分支在阻塞时不会被无关入站 Pong 打断。
调用图:调用 1 个内部函数(from_websocket_stream);外部调用 4 个(assert!, new, test_jsonrpc_message, Pong)。
tests::websocket_pair715–728 ↗
async fn websocket_pair() -> anyhow::Result<(
WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
WebSocketStream<tokio::net::TcpStream>,
)>
作用:为测试快速造出一对真实连上的 WebSocket:一个客户端,一个服务端。
数据流:进去无业务输入 → 它绑定本地临时端口,后台接受连接并升级为 WebSocket,同时客户端连接过去 → 出来是一对可互相通信的 WebSocket 流。
调用关系:多个 WebSocket 测试都调用它,避免每个测试重复写建连接的样板代码。
调用图:外部调用 5 个(bind, format!, spawn, accept_async, connect_async)。
tests::test_jsonrpc_message730–737 ↗
fn test_jsonrpc_message() -> JSONRPCMessage
作用:生成一条固定的测试 JSON-RPC 请求。这样测试之间用同一份简单消息。
数据流:进去不需要输入 → 它构造 id 为 1、method 为 test 的请求 → 出来是 JSONRPCMessage::Request。
调用关系:websocket_connection_keeps_outbound_message_while_send_is_backpressured 使用它来准备待发送消息。
调用图:外部调用 2 个(Request, Integer)。
tests::ControlledWebSocket::new757–788 ↗
fn new(
write_ready: bool,
) -> (
Self,
ControlledWebSocketHandle,
futures_mpsc::UnboundedReceiver<Message>,
)
作用:创建一个测试专用的假 WebSocket,可以人为控制“能不能写”。这让测试能稳定复现写入阻塞。
数据流:进去的是初始是否可写的布尔值 → 它建立入站、出站通道和几个线程安全开关/唤醒器 → 出来是假 WebSocket、本端控制句柄、以及观察出站消息的接收端。
调用关系:背压测试会调用它;生成的 ControlledWebSocket 被交给 from_websocket_stream,控制句柄用于送入消息和放开写入。
tests::ControlledWebSocketHandle::send_inbound792–796 ↗
fn send_inbound(&self, message: Message) -> anyhow::Result<()>
作用:从测试控制端往假 WebSocket 里塞一条“收到的消息”。就像假装网络对面发来了东西。
数据流:进去的是一条 Message → 它把 Ok(message) 放进入站通道 → from_websocket_stream 的读取分支之后能收到它。
调用关系:背压测试用它发送 Pong,确认无关入站帧不会导致出站 JSON-RPC 丢失。
调用图:外部调用 1 个(unbounded_send)。
tests::ControlledWebSocketHandle::set_write_ready798–801 ↗
fn set_write_ready(&self)
作用:把假 WebSocket 从“写不动”切到“可以写”,并叫醒等待写入的任务。
数据流:进去不需要额外数据 → 它把 write_ready 开关设为 true,并唤醒 write_waker → 被卡住的 poll_ready 会再次运行并允许发送。
调用关系:背压测试在确认写入已阻塞后调用它,让 from_websocket_stream 继续把消息发出去。
tests::ControlledWebSocketHandle::wait_for_blocked_write803–817 ↗
async fn wait_for_blocked_write(&self) -> anyhow::Result<()>
作用:等待假 WebSocket 确认已经卡在写入上。测试用它保证时机准确,而不是靠猜时间。
数据流:进去的是控制句柄自身 → 它在最多一秒内轮询 write_blocked 标志,不满足就登记唤醒器等待 → 成功返回空结果,超时返回错误。
调用关系:背压测试发送消息后调用它;它和 ControlledWebSocket::poll_ready 里的 write_blocked 标志配合。
tests::ControlledWebSocket::poll_ready823–832 ↗
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>
作用:实现假 WebSocket 的“现在能不能写”。这是 Sink 接口的一部分,Sink 可以理解为异步发送口。
数据流:进去的是任务上下文和假 WebSocket 状态 → 如果 write_ready 为 true 就返回 Ready;否则标记写入被阻塞、唤醒等待者并登记当前任务 → 出来是可写或继续等待。
调用关系:from_websocket_stream 发送 WebSocket 消息时会间接触发它;背压测试靠它制造发送卡住的场景。
调用图:外部调用 2 个(waker, Ready)。
tests::ControlledWebSocket::start_send834–839 ↗
fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error>
作用:实现假 WebSocket 真正接收一条要发出的消息,并把它放到测试可观察的出站通道里。
数据流:进去的是要发送的 Message → 它写入 outbound_tx → 测试端的 outbound_rx 可以拿到这条消息。
调用关系:当 poll_ready 表示可写后,from_websocket_stream 的发送动作会间接调用它;背压测试最后检查这里送出的内容。
调用图:外部调用 1 个(unbounded_send)。
tests::ControlledWebSocket::poll_flush841–846 ↗
fn poll_flush(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>>
作用:实现假 WebSocket 的刷新动作,但测试里不需要真实刷新,所以总是立刻成功。
数据流:进去的是上下文 → 它不改动状态 → 直接返回 Ready(Ok)。
调用关系:Sink 发送流程可能要求 flush;这个实现让 from_websocket_stream 能把假 WebSocket 当正常 Sink 使用。
调用图:外部调用 1 个(Ready)。
tests::ControlledWebSocket::poll_close848–853 ↗
fn poll_close(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>>
作用:实现假 WebSocket 的关闭动作,测试里同样直接成功。
数据流:进去的是上下文 → 它不做额外清理 → 返回 Ready(Ok)。
调用关系:这是 Sink 接口的必要部分;让 ControlledWebSocket 满足 WebSocket 发送端需要的完整形状。
调用图:外部调用 1 个(Ready)。
tests::ControlledWebSocket::poll_next859–861 ↗
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>>
作用:实现假 WebSocket 的“读取下一条入站消息”。这是 Stream 接口的一部分,Stream 可以理解为异步消息来源。
数据流:进去的是任务上下文和假 WebSocket → 它从 inbound_rx 里轮询下一条消息 → 出来是下一条入站 Message、等待中,或通道结束。
调用关系:from_websocket_stream 的接收分支会间接调用它;测试通过 ControlledWebSocketHandle::send_inbound 往这个通道喂消息。
调用图:外部调用 1 个(new)。
exec-server/src/client.rs源码 ↗
可以把这个文件理解成“远程执行服务器的总机”。上层想让远端跑一个进程、读它的输出、给它写输入、发信号、操作文件,都会先打到这里。它底层用 JSON-RPC(一种用 JSON 表示请求和回复的通信格式)和服务器说话,并维护每个进程自己的会话。服务器发来的输出、退出、关闭通知可能会乱序到达,这里会按序号重新排好,再通知等待的人。它还处理连接初始化、懒连接、断线后的统一报错、正在等待的进程和 HTTP 流的失败收尾。文件末尾的测试用假服务器验证这些关键行为:初始化要正确、断线要能唤醒等待者、输出不能乱序、懒客户端断线后能重连。
ExecServerClientConnectOptions::default108–114 ↗
fn default() -> Self
作用:给连接参数准备一套默认值,让调用者不必每次都手写客户端名字、初始化超时等常用配置。
数据流:进去没有参数 → 它填入默认客户端名、默认初始化等待时间,并表示不恢复旧会话 → 出来一个可直接用于连接的配置对象。
调用关系:它通常在创建 ExecServerClient 连接时被拿来当基础配置;后面的 initialize 会读取这些值去和服务器握手。
ExecServerClientConnectOptions::from128–134 ↗
fn from(value: StdioExecServerConnectArgs) -> Self
作用:把更具体的连接参数转换成客户端初始化需要的通用参数,避免同一套字段到处重复拼装。
数据流:进去是一份远程或 stdio 连接参数 → 它取出客户端名、初始化超时和可选的恢复会话 ID → 出来是 ExecServerClientConnectOptions。
调用关系:它站在连接入口和 initialize 之间,负责把不同运输方式的参数统一成初始化握手能看懂的格式。
RemoteExecServerConnectArgs::new138–146 ↗
fn new(websocket_url: String, client_name: String) -> Self
作用:快速创建一个远程 WebSocket 连接参数包,适合只知道服务器地址和客户端名字的场景。
数据流:进去是 WebSocket 地址和客户端名字 → 它补上默认连接超时、默认初始化超时,并默认不恢复会话 → 出来是一份完整的远程连接参数。
调用关系:它通常由外部配置或调用者使用,生成的参数随后会被转换成 ExecServerClientConnectOptions 或用于实际建立连接。
Inner::drop200–202 ↗
fn drop(&mut self)
作用:当客户端内部对象被销毁时,停止后台读消息的任务,避免连接已经没用了后台任务还继续跑。
数据流:进去是即将被丢弃的 Inner → 它调用 abort 取消 reader_task → 出来没有返回值,但后台读取循环会被终止。
调用关系:这是资源收尾动作;ExecServerClient 的最后一个引用消失时触发,防止通知读取任务泄漏。
调用图:外部调用 1 个(abort)。
LazyRemoteExecServerClient::new218–224 ↗
fn new(transport_params: ExecServerTransportParams) -> Self
作用:创建一个“用到时才连接”的远程客户端外壳,避免程序启动时就立刻连服务器。
数据流:进去是运输参数,比如 WebSocket 地址 → 它保存参数,准备一个空的客户端缓存,并放一把只允许一个连接任务进入的信号量锁 → 出来是懒客户端。
调用关系:测试和远程功能入口会创建它;真正需要环境信息、HTTP 或文件操作时,get 才会被调用来建立或复用连接。
调用图:被 3 处调用(remote_websocket_client_replaces_disconnected_client_with_fresh_session, remote_with_transport, remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion);外部调用 3 个(new, new, new)。
LazyRemoteExecServerClient::get226–258 ↗
async fn get(&self) -> Result<ExecServerClient, ExecServerError>
作用:拿到一个可用的 ExecServerClient;如果还没连就连接,如果旧连接断了且支持重连就换新的。
数据流:进去是懒客户端自身保存的缓存和连接参数 → 它先查缓存,再用信号量防止多个任务同时重连,必要时调用 connect_for_transport 建新连接 → 出来是可用客户端,或连接错误。
调用关系:http_request、http_request_stream、environment_info 以及多种远程文件操作都会先走它;它会调用 connected_client、cached_client,并在需要时把工作交给外部的 connect_for_transport。
调用图:调用 2 个内部函数(cached_client, connected_client);被 13 处调用(environment_info, http_request, http_request_stream, canonicalize, copy, create_directory, get_metadata, read_directory, read_file, read_file_stream (+3 more));外部调用 3 个(connect_for_transport, clone, matches!)。
LazyRemoteExecServerClient::connected_client260–263 ↗
fn connected_client(&self) -> Option<ExecServerClient>
作用:从缓存里找一个还没有断线的客户端,避免把坏连接继续交给调用者。
数据流:进去是懒客户端的缓存 → 它读取 cached_client,并检查客户端是否已断开 → 出来是可用客户端,或者 None。
调用关系:get 在连接前后都会调用它,用来做快速复用和防止重复重连。
调用图:调用 1 个内部函数(cached_client);被 1 处调用(get)。
LazyRemoteExecServerClient::cached_client265–270 ↗
fn cached_client(&self) -> Option<ExecServerClient>
作用:安全地读取当前缓存的客户端副本。
数据流:进去是内部互斥锁保护的 Option 客户端 → 它加锁、取出并克隆里面的客户端引用 → 出来是 Some(client) 或 None。
调用关系:connected_client 和 get 都依赖它;它只负责读缓存,不判断连接是否健康。
调用图:被 2 处调用(connected_client, get)。
LazyRemoteExecServerClient::http_request274–279 ↗
fn http_request(
&self,
params: crate::HttpRequestParams,
) -> BoxFuture<'_, Result<crate::HttpRequestResponse, ExecServerError>>
作用:让懒客户端也能像普通 HTTP 客户端一样发一次性 HTTP 请求。
数据流:进去是 HTTP 请求参数 → 它先调用 get 拿到真实客户端,再把请求转交给真实客户端 → 出来是 HTTP 响应或 exec-server 错误。
调用关系:这是 HttpClient 接口的一部分;上层发 HTTP 请求时不用关心连接是否已经建立。
调用图:调用 1 个内部函数(get)。
LazyRemoteExecServerClient::http_request_stream281–289 ↗
fn http_request_stream(
&self,
params: crate::HttpRequestParams,
) -> BoxFuture<
'_,
Result<(crate::HttpRequestResponse, crate::HttpResponseBodyStream), ExecServerE
作用:发送 HTTP 请求,并把响应体按流式数据一点点交给调用者,适合大响应。
数据流:进去是 HTTP 请求参数 → 它调用 get 获得真实客户端,再转交流式 HTTP 请求 → 出来是响应头信息和一个响应体数据流,或错误。
调用关系:这是 HttpClient 接口的流式版本;它依赖 get 保证底层连接存在。
调用图:调用 1 个内部函数(get)。
LazyRemoteExecServerClient::environment_info293–295 ↗
ExecServerClient::initialize339–376 ↗
async fn initialize(
&self,
options: ExecServerClientConnectOptions,
) -> Result<InitializeResponse, ExecServerError>
作用:和 exec-server 做初始化握手,确认双方可以开始通信,并记录服务器分配的会话 ID。
数据流:进去是客户端名、初始化超时和可选恢复会话 ID → 它在超时时间内发送 initialize 请求,收到响应后保存 session_id,再发 initialized 通知 → 出来是初始化响应或超时/协议错误。
调用关系:connect 创建底层 RPC 客户端后立刻调用它;它成功后,这个客户端才算真正可用,并会调用 notify_initialized 完成握手。
调用图:调用 1 个内部函数(notify_initialized);外部调用 1 个(timeout)。
ExecServerClient::exec378–380 ↗
async fn exec(&self, params: ExecParams) -> Result<ExecResponse, ExecServerError>
作用:请求服务器启动一个远端进程。
数据流:进去是执行参数,比如命令和环境 → 它把参数交给通用 call,并使用执行方法名 → 出来是服务器返回的进程启动响应或错误。
调用关系:这是上层启动进程的主要入口;底层所有发送请求、断线检查和错误转换都交给 call。
调用图:调用 1 个内部函数(call)。
ExecServerClient::environment_info382–384 ↗
async fn environment_info(&self) -> Result<EnvironmentInfo, ExecServerError>
作用:向服务器询问远端环境的基本信息。
数据流:进去没有业务参数 → 它通过 call 发送 environment_info 方法 → 出来是 EnvironmentInfo 或错误。
调用关系:LazyRemoteExecServerClient::environment_info 会转到这里;它复用通用请求通道 call。
调用图:调用 1 个内部函数(call)。
ExecServerClient::read386–388 ↗
ExecServerClient::write390–403 ↗
async fn write(
&self,
process_id: &ProcessId,
chunk: Vec<u8>,
) -> Result<WriteResponse, ExecServerError>
作用:把一段字节写进远端进程的标准输入,就像往本地命令里输入文字。
数据流:进去是进程 ID 和字节块 → 它组装 WriteParams,并通过 call 发送 → 出来是写入响应或错误。
调用关系:Session::write 会调用它;它把会话级的写入请求变成 JSON-RPC 请求。
ExecServerClient::signal405–420 ↗
async fn signal(
&self,
process_id: &ProcessId,
signal: ProcessSignal,
) -> Result<(), ExecServerError>
作用:给远端进程发送信号,比如请求中断或终止。
数据流:进去是进程 ID 和信号类型 → 它组装 SignalParams,通过 call 发送,并丢弃只表示成功的响应 → 出来是成功的空结果或错误。
调用关系:Session::signal 会调用它;底层仍由 call 负责通信和断线判断。
ExecServerClient::terminate422–433 ↗
ExecServerClient::fs_read_file435–440 ↗
async fn fs_read_file(
&self,
params: FsReadFileParams,
) -> Result<FsReadFileResponse, ExecServerError>
作用:读取远端文件的完整内容。
数据流:进去是读文件参数 → 它通过 call 发给文件读取方法 → 出来是文件内容响应或错误。
调用关系:远程文件系统功能会调用它;它只是把具体文件操作挂到统一 RPC 通道上。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_open442–444 ↗
async fn fs_open(&self, params: FsOpenParams) -> Result<FsOpenResponse, ExecServerError>
作用:在远端打开一个文件,通常用于后续分块读取。
数据流:进去是打开文件参数 → 它通过 call 发出 open 请求 → 出来是文件句柄或相关响应。
调用关系:需要按块读大文件的流程会先调用它,然后再调用 fs_read_block,最后 fs_close。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_read_block446–451 ↗
async fn fs_read_block(
&self,
params: FsReadBlockParams,
) -> Result<FsReadBlockResponse, ExecServerError>
作用:从已经打开的远端文件里读取一块数据。
数据流:进去是文件句柄、偏移或大小等参数 → 它通过 call 请求服务器读一块 → 出来是该块内容或错误。
调用关系:它通常跟 fs_open 和 fs_close 配套使用,适合不想一次读完整文件的场景。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_close453–458 ↗
async fn fs_close(
&self,
params: FsCloseParams,
) -> Result<FsCloseResponse, ExecServerError>
作用:关闭远端已经打开的文件句柄,释放服务器那边的资源。
数据流:进去是关闭文件参数 → 它通过 call 发出 close 请求 → 出来是关闭响应或错误。
调用关系:分块读取结束后会调用它;它帮助远端避免文件句柄一直占着。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_write_file460–465 ↗
async fn fs_write_file(
&self,
params: FsWriteFileParams,
) -> Result<FsWriteFileResponse, ExecServerError>
作用:把内容写入远端文件。
数据流:进去是路径、内容和写入选项 → 它通过 call 发送写文件请求 → 出来是写入响应或错误。
调用关系:远程文件编辑或生成文件时会用它;通信细节仍由 call 统一处理。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_create_directory467–472 ↗
async fn fs_create_directory(
&self,
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, ExecServerError>
作用:在远端创建目录。
数据流:进去是目录路径和创建选项 → 它通过 call 请求服务器创建目录 → 出来是创建响应或错误。
调用关系:需要准备远端工作目录时会调用它;它是文件系统 RPC 方法之一。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_get_metadata474–479 ↗
async fn fs_get_metadata(
&self,
params: FsGetMetadataParams,
) -> Result<FsGetMetadataResponse, ExecServerError>
作用:查询远端路径的元信息,比如是不是文件、大小、修改时间等。
数据流:进去是目标路径参数 → 它通过 call 请求 metadata → 出来是元信息响应或错误。
调用关系:上层判断路径状态时会用它;它走同一个客户端请求通道。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_canonicalize481–486 ↗
async fn fs_canonicalize(
&self,
params: FsCanonicalizeParams,
) -> Result<FsCanonicalizeResponse, ExecServerError>
作用:把远端路径规范化,得到服务器眼里的标准路径。
数据流:进去是路径参数 → 它通过 call 发送 canonicalize 请求 → 出来是规范化后的路径响应或错误。
调用关系:远程文件系统在比较或展示路径前可能调用它;Lazy 客户端的文件操作也会间接走到这里。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_read_directory488–493 ↗
async fn fs_read_directory(
&self,
params: FsReadDirectoryParams,
) -> Result<FsReadDirectoryResponse, ExecServerError>
作用:列出远端目录里的内容。
数据流:进去是目录读取参数 → 它通过 call 请求目录列表 → 出来是目录项响应或错误。
调用关系:浏览远端文件树时会用它;它和其他 fs_* 方法一起组成远端文件接口。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_remove495–500 ↗
async fn fs_remove(
&self,
params: FsRemoveParams,
) -> Result<FsRemoveResponse, ExecServerError>
作用:删除远端文件或目录。
数据流:进去是删除参数 → 它通过 call 请求服务器移除目标 → 出来是删除响应或错误。
调用关系:清理远端文件时会调用它;错误和断线处理仍集中在 call。
调用图:调用 1 个内部函数(call)。
ExecServerClient::fs_copy502–504 ↗
async fn fs_copy(&self, params: FsCopyParams) -> Result<FsCopyResponse, ExecServerError>
作用:在远端复制文件或目录。
数据流:进去是源路径、目标路径等复制参数 → 它通过 call 发送 copy 请求 → 出来是复制响应或错误。
调用关系:远程文件系统复制动作会用它;它是统一 RPC 文件操作的一员。
调用图:调用 1 个内部函数(call)。
ExecServerClient::register_session506–519 ↗
async fn register_session(
&self,
process_id: &ProcessId,
) -> Result<Session, ExecServerError>
作用:为一个远端进程建立本地会话,让后续输出通知能找到正确的等待者。
数据流:进去是进程 ID → 它创建 SessionState,把它登记到 Inner 的会话表里 → 出来是 Session,里面带着客户端、进程 ID 和状态。
调用关系:启动进程后,上层会调用它来订阅这个进程的输出;它依赖 Inner::insert_session 防止重复登记或断线后登记。
ExecServerClient::unregister_session521–523 ↗
async fn unregister_session(&self, process_id: &ProcessId)
作用:取消某个进程的本地会话登记,表示不再需要接收它的通知。
数据流:进去是进程 ID → 它让 Inner 从会话表删除对应项 → 出来没有返回值。
调用关系:Session::unregister 会调用它;终止或不再关注进程时用来清理路由表。
调用图:被 1 处调用(unregister)。
ExecServerClient::session_id525–531 ↗
fn session_id(&self) -> Option<String>
作用:取出初始化时服务器分配给这个客户端的会话 ID。
数据流:进去是客户端自身 → 它读取内部读写锁保护的 session_id 并克隆 → 出来是 Some(id) 或 None。
调用关系:测试和上层状态展示会用它确认连接握手成功;这个值由 initialize 写入。
ExecServerClient::is_disconnected533–535 ↗
fn is_disconnected(&self) -> bool
作用:判断这个客户端是否已经断线。
数据流:进去是客户端内部状态 → 它检查自己的断线标记,也询问底层 RpcClient 是否断开 → 出来是布尔值。
调用关系:LazyRemoteExecServerClient::connected_client 间接依赖它,测试里的 wait_for_disconnect 也会轮询它。
调用图:被 1 处调用(wait_for_disconnect)。
ExecServerClient::connect537–591 ↗
async fn connect(
connection: JsonRpcConnection,
options: ExecServerClientConnectOptions,
) -> Result<Self, ExecServerError>
作用:用已有的 JSON-RPC 连接创建完整客户端,并启动后台读取服务器通知的任务。
数据流:进去是 JsonRpcConnection 和连接选项 → 它创建 RpcClient、启动 reader_task 处理通知和断线事件、初始化 Inner,随后调用 initialize → 出来是可用的 ExecServerClient 或错误。
调用关系:测试直接调用它,其他运输方式的连接函数也会以类似方式建立客户端;后台任务会把通知交给 handle_server_notification,断线时调用失败收尾流程。
调用图:调用 1 个内部函数(new);被 3 处调用(process_events_are_delivered_in_seq_order_when_notifications_are_reordered, transport_disconnect_fails_sessions_and_rejects_new_sessions, wake_notifications_do_not_block_other_sessions);外部调用 1 个(new_cyclic)。
ExecServerClient::notify_initialized593–599 ↗
async fn notify_initialized(&self) -> Result<(), ExecServerError>
作用:在 initialize 请求成功后,告诉服务器“客户端这边也准备好了”。
数据流:进去没有业务参数 → 它发送 initialized 通知,内容是空 JSON 对象 → 出来是成功或 JSON 序列化错误。
调用关系:只由 initialize 调用,是初始化握手的第二步。
调用图:被 1 处调用(initialize);外部调用 1 个(json!)。
ExecServerClient::call601–629 ↗
async fn call(&self, method: &str, params: &P) -> Result<T, ExecServerError>
作用:这是所有普通请求的统一出口,负责发 JSON-RPC 请求、提前拒绝断线后的新工作,并把底层错误换成客户端错误。
数据流:进去是方法名和可序列化参数 → 它先检查是否已断线,再调用底层 RpcClient;如果运输关闭,就记录统一断线消息 → 出来是反序列化后的响应或 ExecServerError。
调用关系:exec、read、write、signal、terminate 和所有 fs_* 方法都调用它;它会用 ExecServerError::from、is_transport_closed_error、disconnected_message、record_disconnected 统一错误路径。
调用图:调用 4 个内部函数(from, disconnected_message, is_transport_closed_error, record_disconnected);被 17 处调用(environment_info, exec, fs_canonicalize, fs_close, fs_copy, fs_create_directory, fs_get_metadata, fs_open, fs_read_block, fs_read_directory (+7 more));外部调用 1 个(Disconnected)。
ExecServerError::from633–642 ↗
fn from(value: RpcCallError) -> Self
作用:把底层 RPC 调用错误转换成这个客户端公开使用的错误类型。
数据流:进去是 RpcCallError → 它按关闭、JSON 错误、服务器错误三类重新包装 → 出来是 ExecServerError。
调用关系:ExecServerClient::call 在底层请求失败时调用它,让上层只需要面对一种错误枚举。
调用图:被 1 处调用(call);外部调用 1 个(Json)。
SessionState::new646–657 ↗
SessionState::subscribe659–661 ↗
fn subscribe(&self) -> watch::Receiver<u64>
作用:订阅这个进程的“有变化了”提醒。
数据流:进去是会话状态 → 它从 wake_tx 创建一个新的接收端 → 出来是 watch::Receiver,调用者可等待变化。
调用关系:Session::subscribe_wake 会调用它;handle_server_notification 收到输出或状态变化后会通过 note_change 唤醒这些订阅者。
调用图:外部调用 1 个(subscribe)。
SessionState::subscribe_events663–665 ↗
fn subscribe_events(&self) -> ExecProcessEventReceiver
作用:订阅这个进程的事件流,比如输出、退出、关闭、失败。
数据流:进去是会话状态 → 它向事件日志申请一个接收器 → 出来是 ExecProcessEventReceiver。
调用关系:Session::subscribe_events 会调用它;publish_ordered_event 和 set_failure 会把事件发布给这些接收器。
调用图:调用 1 个内部函数(subscribe)。
SessionState::note_change667–670 ↗
SessionState::publish_ordered_event679–714 ↗
fn publish_ordered_event(&self, event: ExecProcessEvent) -> bool
作用:按序发布进程事件,防止服务器通知乱序导致用户先看到“关闭”再看到最后一段输出。
数据流:进去是一个进程事件 → 如果没有序号就直接发布;如果有序号,就放进待发布表,等前面的序号都齐了再连续发布 → 出来是是否真正发布了 Closed 事件。
调用关系:handle_server_notification 和 set_failure 会调用它;当它报告 Closed 已发布后,通知处理会移除这个进程的会话路由。
调用图:调用 2 个内部函数(seq, publish);被 1 处调用(set_failure);外部调用 3 个(lock, new, matches!)。
SessionState::set_failure716–728 ↗
async fn set_failure(&self, message: String)
作用:把这个会话标记为失败,并唤醒所有等输出的人。
数据流:进去是失败消息 → 它只在第一次设置失败内容,推进唤醒计数,并发布 Failed 事件 → 出来没有返回值,但轮询和事件订阅者都会看到失败。
调用关系:断线收尾时 fail_all_sessions 会调用它;Session::read 遇到运输关闭时也会用它生成一致的失败状态。
调用图:调用 1 个内部函数(publish_ordered_event);外部调用 3 个(borrow, send, Failed)。
SessionState::failed_response730–736 ↗
async fn failed_response(&self) -> Option<ReadResponse>
作用:如果会话已经失败,生成一个像正常 read 一样能返回的失败响应。
数据流:进去是会话状态 → 它读取 failure 字段;如果有消息,就调用 synthesized_failure 包装成 ReadResponse → 出来是 Some(response) 或 None。
调用关系:Session::read 一开始会调用它,这样断线后的读取不会一直卡住,而是立刻得到关闭且失败的结果。
SessionState::synthesized_failure738–748 ↗
Session::process_id752–754 ↗
fn process_id(&self) -> &ProcessId
作用:返回这个会话对应的远端进程 ID。
数据流:进去是 Session → 它借用内部保存的 process_id → 出来是进程 ID 引用。
调用关系:上层需要知道会话绑定哪个进程时会调用它;它不触发网络通信。
Session::subscribe_wake756–758 ↗
fn subscribe_wake(&self) -> watch::Receiver<u64>
作用:让调用者等待这个进程出现新变化。
数据流:进去是 Session → 它转到 SessionState::subscribe → 出来是唤醒接收器。
调用关系:事件循环或轮询器会用它等待 read 的时机;真正的唤醒来自 note_change 或 set_failure。
Session::subscribe_events760–762 ↗
fn subscribe_events(&self) -> ExecProcessEventReceiver
作用:让调用者接收这个进程的实时事件流。
数据流:进去是 Session → 它转到 SessionState::subscribe_events → 出来是事件接收器。
调用关系:流式消费进程输出的上层会用它;事件由 handle_server_notification 经 publish_ordered_event 发布。
Session::read764–792 ↗
async fn read(
&self,
after_seq: Option<u64>,
max_bytes: Option<usize>,
wait_ms: Option<u64>,
) -> Result<ReadResponse, ExecServerError>
作用:读取当前会话对应进程的输出和状态,并在断线时返回一个可理解的失败结果。
数据流:进去是读取起点、最大字节数和等待时间 → 它先看本地是否已失败;没有失败就调用 ExecServerClient::read;如果发现运输关闭,就设置失败并合成关闭响应 → 出来是 ReadResponse 或非断线错误。
调用关系:这是会话级读取入口;它调用客户端 read,也使用 disconnected_message 和 is_transport_closed_error 保持断线处理一致。
调用图:调用 3 个内部函数(read, disconnected_message, is_transport_closed_error);外部调用 1 个(clone)。
Session::write794–796 ↗
async fn write(&self, chunk: Vec<u8>) -> Result<WriteResponse, ExecServerError>
作用:向这个会话对应的进程写入输入数据。
数据流:进去是字节块 → 它带上自己的 process_id 调用 ExecServerClient::write → 出来是写入响应或错误。
调用关系:上层不必每次传进程 ID;Session 替它把请求交给客户端。
调用图:调用 1 个内部函数(write)。
Session::signal798–800 ↗
async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError>
作用:给这个会话对应的进程发送信号。
数据流:进去是信号 → 它带上自己的 process_id 调用 ExecServerClient::signal → 出来是成功或错误。
调用关系:它是会话级便捷方法,把具体通信交给客户端。
调用图:调用 1 个内部函数(signal)。
Session::terminate802–805 ↗
async fn terminate(&self) -> Result<(), ExecServerError>
作用:终止这个会话对应的远端进程。
数据流:进去没有额外参数 → 它调用 ExecServerClient::terminate,并把响应简化成空成功 → 出来是成功或错误。
调用关系:上层想结束进程时调用它;实际 RPC 请求由客户端完成。
调用图:调用 1 个内部函数(terminate)。
Session::unregister807–809 ↗
async fn unregister(&self)
作用:从客户端路由表里移除这个会话。
数据流:进去是 Session → 它用自己的 process_id 调用 ExecServerClient::unregister_session → 出来没有返回值。
调用关系:会话不再需要接收通知时调用;它把清理动作交给客户端内部会话表。
调用图:调用 1 个内部函数(unregister_session)。
Inner::disconnected_error813–818 ↗
fn disconnected_error(&self) -> Option<ExecServerError>
作用:如果连接已经断开,生成一份统一的断线错误。
数据流:进去是 Inner → 它查看 disconnected 里是否已有断线消息 → 出来是 Some(Disconnected) 或 None。
调用关系:Inner::insert_session 会调用它来拒绝断线后的新会话;ExecServerClient::call 也有类似的预检逻辑。
调用图:被 1 处调用(insert_session);外部调用 1 个(get)。
Inner::set_disconnected820–825 ↗
fn set_disconnected(&self, message: String) -> Option<String>
作用:第一次记录连接断开的标准原因,后面再来的断线原因不覆盖它。
数据流:进去是一条断线消息 → 它尝试写入 OnceLock;如果成功返回这条消息,若之前已有值则返回 None → 出来表示是否由本次调用完成记录。
调用关系:record_disconnected 会调用它;这样所有后续错误都能使用同一个断线说法。
调用图:外部调用 1 个(set)。
Inner::get_session827–829 ↗
fn get_session(&self, process_id: &ProcessId) -> Option<Arc<SessionState>>
作用:根据进程 ID 找到本地会话状态,用于把全局通知分发给正确进程。
数据流:进去是进程 ID → 它从会话表快照里查找并克隆状态引用 → 出来是对应 SessionState 或 None。
调用关系:handle_server_notification 收到进程输出、退出、关闭通知时会用它定位目标会话。
调用图:外部调用 1 个(load)。
Inner::insert_session831–853 ↗
async fn insert_session(
&self,
process_id: &ProcessId,
session: Arc<SessionState>,
) -> Result<(), ExecServerError>
作用:把新进程会话登记进路由表,并防止断线后或重复进程 ID 的错误登记。
数据流:进去是进程 ID 和 SessionState → 它加写锁,先检查断线,再检查是否已存在,最后复制会话表并插入新项 → 出来是成功或协议/断线错误。
调用关系:ExecServerClient::register_session 调用它;它保护通知分发所需的 process_id 到 SessionState 映射。
调用图:调用 1 个内部函数(disconnected_error);外部调用 6 个(new, load, store, Protocol, clone, format!)。
Inner::remove_session855–864 ↗
Inner::take_all_sessions866–872 ↗
disconnected_message875–880 ↗
is_transport_closed_error882–893 ↗
record_disconnected895–904 ↗
fn record_disconnected(inner: &Arc<Inner>, message: String) -> String
作用:记录断线状态,并返回最终应该使用的标准断线消息。
数据流:进去是 Inner 和候选断线消息 → 它尝试通过 set_disconnected 写入;如果已经有人写过,就读取已有消息 → 出来是统一断线消息。
调用关系:ExecServerClient::call 和后台 reader_task 断线路径都会用它;它保证并发情况下大家说的是同一个断线原因。
调用图:被 1 处调用(call)。
fail_all_sessions906–915 ↗
async fn fail_all_sessions(inner: &Arc<Inner>, message: String)
作用:连接断开时,让所有还在等待的进程会话都收到失败通知。
数据流:进去是 Inner 和失败消息 → 它取走全部会话,对每个会话调用 set_failure → 出来没有返回值,但所有会话都会被标失败并唤醒。
调用关系:fail_all_in_flight_work 调用它;它负责进程会话这一类未完成工作的收尾。
调用图:被 1 处调用(fail_all_in_flight_work)。
fail_all_in_flight_work918–921 ↗
async fn fail_all_in_flight_work(inner: &Arc<Inner>, message: String)
作用:连接断开时,统一失败所有依赖这条共享连接的进行中工作。
数据流:进去是 Inner 和断线消息 → 它先失败所有进程会话,再失败所有 HTTP 响应体流 → 出来没有返回值。
调用关系:后台 reader_task 在通知处理失败或连接断开时调用它;它把清理分派给 fail_all_sessions 和 HTTP 流清理方法。
调用图:调用 1 个内部函数(fail_all_sessions)。
handle_server_notification923–983 ↗
async fn handle_server_notification(
inner: &Arc<Inner>,
notification: JSONRPCNotification,
) -> Result<(), ExecServerError>
作用:处理服务器主动推来的通知,并分发给正确的进程会话或 HTTP 流。
数据流:进去是一条 JSON-RPC 通知 → 它按 method 解析参数:输出、退出、关闭会转成进程事件并按序发布;HTTP body 增量交给 HTTP 流处理;未知通知只记录调试信息 → 出来是成功或解析/协议错误。
调用关系:ExecServerClient::connect 启动的 reader_task 会调用它;它是服务器通知进入本地会话系统的主要入口。
调用图:外部调用 3 个(debug!, Output, from_value)。
tests::read_jsonrpc_line1039–1049 ↗
async fn read_jsonrpc_line(lines: &mut tokio::io::Lines<BufReader<R>>) -> JSONRPCMessage
作用:测试用的小工具:从按行传输的连接里读一条 JSON-RPC 消息。
数据流:进去是异步按行读取器 → 它最多等 1 秒读一行,再把 JSON 文本解析成消息 → 出来是 JSONRPCMessage,失败则让测试报错。
调用关系:多个 stdio/duplex 测试里的假服务器会调用它,用来检查客户端发来的初始化通知等消息。
tests::write_jsonrpc_line1051–1060 ↗
tests::accept_websocket1062–1067 ↗
async fn accept_websocket(listener: &TcpListener) -> WebSocketStream<TcpStream>
作用:测试用的小工具:接受一个 TCP 连接并升级成 WebSocket。
数据流:进去是监听器 → 它等待客户端连入,然后完成 WebSocket 握手 → 出来是 WebSocketStream。
调用关系:远程懒客户端重连测试会调用它来扮演服务器端。
调用图:外部调用 2 个(accept, accept_async)。
tests::read_jsonrpc_websocket1069–1089 ↗
async fn read_jsonrpc_websocket(websocket: &mut WebSocketStream<TcpStream>) -> JSONRPCMessage
作用:测试用的小工具:从 WebSocket 帧里读出 JSON-RPC 消息。
数据流:进去是 WebSocket 流 → 它循环读帧,忽略 ping/pong,把文本或二进制帧解析成 JSON-RPC 消息 → 出来是 JSONRPCMessage。
调用关系:complete_websocket_initialize 会调用它检查客户端的 initialize 请求和 initialized 通知。
调用图:外部调用 6 个(from_secs, next, panic!, from_slice, from_str, timeout)。
tests::write_jsonrpc_websocket1091–1100 ↗
tests::complete_websocket_initialize1102–1137 ↗
async fn complete_websocket_initialize(
websocket: &mut WebSocketStream<TcpStream>,
session_id: &str,
expected_resume_session_id: Option<&str>,
)
作用:测试用流程:帮假 WebSocket 服务器完成一次初始化握手。
数据流:进去是 WebSocket、要返回的 session_id 和期望的恢复会话 ID → 它读取 initialize 请求、校验参数、写回响应,再确认客户端发送 initialized 通知 → 出来没有返回值。
调用关系:远程懒客户端重连测试调用它两次,分别模拟第一次连接和重连后的新会话。
调用图:外部调用 7 个(Response, assert_eq!, read_jsonrpc_websocket, write_jsonrpc_websocket, panic!, from_value, to_value)。
tests::wait_for_disconnect1139–1150 ↗
async fn wait_for_disconnect(client: &ExecServerClient)
作用:测试用等待器:等客户端观察到断线状态。
数据流:进去是客户端引用 → 它在 1 秒内循环检查 is_disconnected,中间让出任务调度 → 出来没有业务返回,超时则测试失败。
调用关系:远程懒客户端重连测试在关闭第一次 WebSocket 后调用它,确保客户端已经知道旧连接坏了。
调用图:调用 1 个内部函数(is_disconnected);外部调用 3 个(from_secs, yield_now, timeout)。
tests::connect_stdio_command_initializes_json_rpc_client1154–1173 ↗
async fn connect_stdio_command_initializes_json_rpc_client()
作用:测试通过 stdio 启动的 exec-server 是否能完成 JSON-RPC 初始化。
数据流:进去没有外部输入 → 它启动一个 shell 假服务器,假服务器读初始化请求后返回 sessionId → 出来通过断言确认客户端保存了该 sessionId。
调用关系:它调用 connect_stdio_command;验证 stdio 运输方式能走完整初始化流程。
调用图:外部调用 5 个(from_secs, new, assert_eq!, connect_stdio_command, vec!)。
tests::connect_for_transport_initializes_stdio_command1177–1196 ↗
async fn connect_for_transport_initializes_stdio_command()
作用:测试通用运输参数入口能正确创建 stdio 命令连接。
数据流:进去没有外部输入 → 它构造 StdioCommand 运输参数,调用 connect_for_transport,并检查 session_id → 出来是测试通过或失败。
调用关系:它覆盖 connect_for_transport 到 stdio 初始化的路径,避免只有专用入口可用。
调用图:外部调用 4 个(new, assert_eq!, connect_for_transport, vec!)。
tests::connect_stdio_command_initializes_json_rpc_client_on_windows1200–1220 ↗
async fn connect_stdio_command_initializes_json_rpc_client_on_windows()
作用:在 Windows 上测试 stdio 命令初始化流程,使用 PowerShell 假服务器。
数据流:进去没有外部输入 → 它启动 PowerShell 脚本模拟服务器,返回 sessionId,再断言客户端保存成功 → 出来是测试结果。
调用关系:它和 Unix 版本测试目标相同,但使用 Windows 可执行命令,保证跨平台行为一致。
调用图:外部调用 5 个(from_secs, new, assert_eq!, connect_stdio_command, vec!)。
tests::dropping_stdio_client_terminates_spawned_process1224–1267 ↗
async fn dropping_stdio_client_terminates_spawned_process()
作用:测试客户端被丢弃时,stdio 启动的服务器进程和它的子进程会被清掉。
数据流:进去没有外部输入 → 它启动脚本写出父子进程 PID,确认它们存在,随后 drop 客户端并等待进程退出 → 出来是测试断言结果。
调用关系:它调用 connect_stdio_command、read_pid_file、wait_for_process_exit,验证资源收尾不是只停客户端对象。
调用图:外部调用 9 个(from_secs, new, assert!, connect_stdio_command, read_pid_file, wait_for_process_exit, format!, tempdir, vec!)。
tests::malformed_stdio_message_terminates_spawned_process1271–1298 ↗
async fn malformed_stdio_message_terminates_spawned_process()
作用:测试 stdio 服务器发出坏 JSON 时,连接失败并且已启动的进程会被终止。
数据流:进去没有外部输入 → 它启动一个输出 not-json 的假服务器,确认连接返回错误,再读取 PID 并等待进程退出 → 出来是测试结果。
调用关系:它覆盖初始化失败时的清理路径,防止坏服务器导致子进程泄漏。
调用图:外部调用 9 个(from_secs, new, assert!, connect_stdio_command, read_pid_file, wait_for_process_exit, format!, tempdir, vec!)。
tests::read_pid_file1301–1312 ↗
async fn read_pid_file(path: &Path) -> u32
作用:测试用工具:等脚本把进程 ID 写进文件,然后读出来。
数据流:进去是 PID 文件路径 → 它反复尝试读取文件,读到后解析成数字;超时就让测试失败 → 出来是 u32 进程 ID。
调用关系:进程清理相关测试用它拿到假服务器进程的 PID。
调用图:外部调用 4 个(from_millis, panic!, read_to_string, sleep)。
tests::wait_for_process_exit1315–1323 ↗
async fn wait_for_process_exit(pid: u32)
作用:测试用工具:等待某个系统进程退出。
数据流:进去是进程 ID → 它反复调用 process_exists 检查进程是否还活着,中间短暂睡眠 → 出来没有返回值,若一直不退出则测试失败。
调用关系:stdio 进程清理测试用它确认父进程和子进程真的被终止。
调用图:外部调用 4 个(from_millis, process_exists, panic!, sleep)。
tests::process_exists1326–1332 ↗
fn process_exists(pid: u32) -> bool
作用:测试用工具:在 Unix 上判断某个 PID 对应的进程是否存在。
数据流:进去是进程 ID → 它执行 kill -0,这个命令不杀进程,只检查进程是否可访问 → 出来是真或假。
调用关系:wait_for_process_exit 和 dropping_stdio_client_terminates_spawned_process 用它检查进程状态。
调用图:外部调用 1 个(new)。
tests::shell_quote1335–1338 ↗
fn shell_quote(path: &Path) -> String
作用:测试用工具:把路径安全地放进 shell 脚本字符串里。
数据流:进去是路径 → 它转成字符串,并转义单引号 → 出来是带单引号包裹的 shell 安全文本。
调用关系:stdio 进程测试拼接 shell 脚本时调用它,避免临时目录路径里的特殊字符破坏命令。
调用图:外部调用 2 个(to_string_lossy, format!)。
tests::process_events_are_delivered_in_seq_order_when_notifications_are_reordered1341–1481 ↗
async fn process_events_are_delivered_in_seq_order_when_notifications_are_reordered()
作用:测试服务器通知乱序到达时,客户端仍按序发布进程事件。
数据流:进去没有外部输入 → 它搭建内存里的假 JSON-RPC 连接,注册进程会话,故意按 4、1、3、2 的顺序发送通知 → 出来通过断言确认收到的事件是 1、2、3、4。
调用关系:它直接调用 ExecServerClient::connect 和 register_session,重点验证 SessionState::publish_ordered_event 的排序作用。
调用图:调用 3 个内部函数(connect, from_stdio, from);外部调用 15 个(new, from_secs, new, Notification, Response, assert_eq!, read_jsonrpc_line, write_jsonrpc_line, default, channel (+5 more))。
tests::transport_disconnect_fails_sessions_and_rejects_new_sessions1484–1567 ↗
async fn transport_disconnect_fails_sessions_and_rejects_new_sessions()
作用:测试连接断开后,已有会话会失败,新会话会被拒绝。
数据流:进去没有外部输入 → 它建立假连接和会话,然后让服务器端断开;接着检查事件流收到 Failed,read 返回合成失败响应,新 register_session 返回 Disconnected → 出来是测试断言结果。
调用关系:它覆盖 reader_task 的断线处理、fail_all_in_flight_work、SessionState::set_failure 和 Inner::insert_session 的断线拒绝逻辑。
调用图:调用 3 个内部函数(connect, from_stdio, from);外部调用 14 个(new, from_secs, Response, assert!, assert_eq!, read_jsonrpc_line, write_jsonrpc_line, default, channel, panic! (+4 more))。
tests::remote_websocket_client_replaces_disconnected_client_with_fresh_session1570–1618 ↗
async fn remote_websocket_client_replaces_disconnected_client_with_fresh_session()
作用:测试懒远程客户端在 WebSocket 断开后会创建新连接,并且并发 get 不会各连一条。
数据流:进去没有外部输入 → 它启动假 WebSocket 服务器,第一次连接后关闭,再接受第二次连接;客户端先 get 一次,等断线后并发 get 两次 → 出来断言两个结果共用同一个新客户端且 session_id 是新的。
调用关系:它调用 LazyRemoteExecServerClient::new、get、accept_websocket、complete_websocket_initialize 和 wait_for_disconnect,验证懒连接和重连锁。
调用图:调用 1 个内部函数(new);外部调用 10 个(from_secs, bind, assert!, assert_eq!, accept_websocket, complete_websocket_initialize, wait_for_disconnect, format!, join!, spawn)。
tests::wake_notifications_do_not_block_other_sessions1621–1721 ↗
async fn wake_notifications_do_not_block_other_sessions()
作用:测试一个进程大量通知不会阻塞另一个进程的唤醒。
数据流:进去没有外部输入 → 它建立两个会话,给 noisy 进程发送大量输出通知,再给 quiet 进程发送退出通知 → 出来断言 quiet 的唤醒能及时收到。
调用关系:它覆盖通知分发的热路径,验证 watch 唤醒和会话路由不会因为某个会话太吵而拖住其他会话。
调用图:调用 3 个内部函数(connect, from_stdio, from);外部调用 14 个(new, from_secs, Notification, Response, assert_eq!, read_jsonrpc_line, write_jsonrpc_line, default, channel, panic! (+4 more))。
exec-server/src/server/transport.rs源码 ↗
这个文件像服务器的“前台接待”。程序启动时会给它一个监听地址,比如 ws://127.0.0.1:1234,或者 stdio。它先把这个地址看懂:是开一个 WebSocket 网络服务,还是直接用命令行进程的标准输入输出通信。WebSocket 是一种长连接网络协议,适合客户端连到一个端口上持续收发消息;stdio 则像两根管子,父进程通过输入输出把消息塞进来、读出去。文件还做了几个安全和可用性的小动作:WebSocket 启动后会把实际地址打印出来,方便调用者知道连哪里;提供 /readyz 健康检查;拒绝带 Origin 请求头的连接,降低被网页跨站误连的风险。真正的消息内容不在这里处理,而是被包装成 JsonRpcConnection 后交给 ConnectionProcessor。
ExecServerListenUrlParseError::fmt43–54 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:这个函数把“监听地址写错了”这种错误变成人能看懂的文字。这样命令行或日志里不会只出现冷冰冰的内部错误名。
数据流:进去的是一个错误类型,里面带着用户输入的监听地址 → 它判断错误是“不支持的格式”还是“WebSocket 地址格式不对” → 出来的是写进格式化器里的提示文字,告诉用户应该写 ws://IP:PORT 或 stdio。
调用关系:当 parse_listen_url 解析失败并把错误往外传时,外层如果要显示这个错误,就会走到这里。它不修复错误,只负责把错误说明白。
调用图:外部调用 1 个(write!)。
parse_listen_url59–78 ↗
fn parse_listen_url(
listen_url: &str,
) -> Result<ExecServerListenTransport, ExecServerListenUrlParseError>
作用:这个函数负责看懂用户写的监听地址。它把一段字符串分成两类:用 WebSocket 监听某个 IP 和端口,或者用 stdio 通信。
数据流:进去的是 listen_url 这段文字 → 如果是 stdio 或 stdio://,就认定为标准输入输出;如果以 ws:// 开头,就把后面的 IP:端口解析成网络地址;其他格式都算不支持 → 出来的是一个明确的通信方式,或者一个说明哪里不对的解析错误。
调用关系:run_transport 启动通信前会先调用它。它像门牌识别员,先决定后面该走 WebSocket 服务启动流程,还是 stdio 单连接流程。
调用图:被 1 处调用(run_transport);外部调用 2 个(UnsupportedListenUrl, matches!)。
run_transport80–90 ↗
async fn run_transport(
listen_url: &str,
runtime_paths: ExecServerRuntimePaths,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
作用:这是本文件的总入口:给它一个监听地址,它会选择并启动对应的通信方式。调用者不用关心底层到底是网络端口还是标准输入输出。
数据流:进去的是用户配置的 listen_url 和运行时路径 runtime_paths → 它先调用 parse_listen_url 解析地址 → 如果结果是 WebSocket,就启动网络监听;如果是 stdio,就接上标准输入输出 → 最后返回成功,或者把启动、解析过程中遇到的错误传出去。
调用关系:它由更外层的 run_main 调用,是启动 exec-server 通信层的分岔口。它自己不处理请求内容,而是把工作分派给 run_websocket_listener 或 run_stdio_connection。
调用图:调用 3 个内部函数(parse_listen_url, run_stdio_connection, run_websocket_listener);被 1 处调用(run_main)。
run_stdio_connection92–96 ↗
async fn run_stdio_connection(
runtime_paths: ExecServerRuntimePaths,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
作用:这个函数启动 stdio 模式,也就是让服务器通过当前进程的标准输入和标准输出收发消息。它适合被另一个程序作为子进程拉起来使用。
数据流:进去的是 runtime_paths → 它取得系统提供的 stdin 和 stdout,也就是输入管道和输出管道 → 再把这两根管子交给 run_stdio_connection_with_io → 出来是连接运行结束后的成功结果或错误。
调用关系:run_transport 判断监听方式是 stdio 时会调用它。它只是把真实的系统输入输出准备好,具体怎么包装连接和跑处理器,交给 run_stdio_connection_with_io。
调用图:调用 1 个内部函数(run_stdio_connection_with_io);被 1 处调用(run_transport);外部调用 2 个(stdin, stdout)。
run_stdio_connection_with_io98–117 ↗
async fn run_stdio_connection_with_io(
reader: R,
writer: W,
runtime_paths: ExecServerRuntimePaths,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
作用:这个函数把任意一对可读、可写的异步通道包装成一条 JSON-RPC 连接,然后交给请求处理器。JSON-RPC 是一种用 JSON 文本表示“调用某个方法并返回结果”的通信格式。
数据流:进去的是 reader、writer 两个异步输入输出对象,以及 runtime_paths → 它创建 ConnectionProcessor,再用 JsonRpcConnection::from_stdio 把输入输出包装成一条名为 exec-server stdio 的连接 → 然后让 processor.run_connection 一直处理这条连接上的消息 → 处理结束后返回成功。
调用关系:run_stdio_connection 会调用它。这里开始把“字节流管道”变成项目内部能理解的 JsonRpcConnection,随后真正的请求执行交给 ConnectionProcessor。
调用图:调用 2 个内部函数(from_stdio, new);被 1 处调用(run_stdio_connection);外部调用 1 个(info!)。
run_websocket_listener119–141 ↗
async fn run_websocket_listener(
bind_address: SocketAddr,
runtime_paths: ExecServerRuntimePaths,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
作用:这个函数启动 WebSocket 服务器,监听指定 IP 和端口,等待客户端连进来。它还设置健康检查和安全拦截规则。
数据流:进去的是 bind_address 和 runtime_paths → 它绑定 TCP 端口,拿到实际监听地址,创建 ConnectionProcessor,并把地址打印到标准输出 → 接着搭建路由:根路径用于 WebSocket 连接,/readyz 用于健康检查,所有请求先经过 Origin 拦截中间件 → 最后交给 axum 服务器持续接收请求;如果绑定或运行失败,就返回错误。
调用关系:run_transport 判断监听方式是 WebSocket 时会调用它。它负责把网络服务搭起来;有客户端访问时,/readyz 会走 readiness_handler,WebSocket 升级会走 websocket_upgrade_handler,每个请求还会先经过 reject_requests_with_origin_header。
调用图:调用 1 个内部函数(new);被 1 处调用(run_transport);外部调用 9 个(new, bind, any, get, serve, info!, from_fn, println!, stdout)。
readiness_handler148–150 ↗
async fn readiness_handler() -> StatusCode
作用:这个函数回答“服务器是不是活着、能不能接请求”。它用于健康检查接口 /readyz。
数据流:进去没有业务数据 → 它直接返回 HTTP 200 OK 状态码 → 调用方据此知道这个进程已经在监听并能响应请求。
调用关系:run_websocket_listener 把 /readyz 路由交给它。监控程序或启动脚本可以访问这个路径,确认 WebSocket 服务已经准备好。
reject_requests_with_origin_header152–166 ↗
async fn reject_requests_with_origin_header(
request: Request<Body>,
next: Next,
) -> Result<Response, StatusCode>
作用:这个函数是 WebSocket 服务前面的一道安全门。只要请求带了 Origin 请求头,就拒绝,避免普通网页在浏览器里偷偷连到这个本地服务。
数据流:进去的是一个 HTTP 请求和下一个处理步骤 next → 它检查请求头里有没有 Origin;如果有,就记录警告并返回 403 Forbidden;如果没有,就把请求继续交给后面的路由 → 出来的是拒绝响应,或后续处理得到的正常响应。
调用关系:run_websocket_listener 把它装成中间件,也就是所有 WebSocket 服务器请求都会先经过它。它通过检查后,才轮到 readiness_handler 或 websocket_upgrade_handler 等具体处理函数。
websocket_upgrade_handler168–183 ↗
async fn websocket_upgrade_handler(
websocket: WebSocketUpgrade,
ConnectInfo(peer_addr): ConnectInfo<SocketAddr>,
State(state): State<ExecServerWebSocketState>,
) -> impl IntoResponse
作用:这个函数处理真正的 WebSocket 客户端连接。它把普通 HTTP 请求升级成长连接,然后把这条连接交给 ConnectionProcessor 处理消息。
数据流:进去的是 WebSocket 升级对象、客户端地址 peer_addr,以及共享的服务器状态 state → 它先记录有客户端连入 → 然后在升级成功后,把 WebSocket 流包装成 JsonRpcConnection,并带上包含客户端地址的连接名字 → 最后调用 state.processor.run_connection 持续处理这条连接上的 JSON-RPC 消息。
调用关系:run_websocket_listener 把根路径 / 的请求交给它。它是网络客户端进入业务处理器的桥:前面由 axum 接收连接,后面由 ConnectionProcessor 处理具体请求。
调用图:外部调用 2 个(on_upgrade, info!)。
Noise relay 远程连接
这些文件在 exec-server 传输之上,为远程环境和执行器流叠加已认证的 rendezvous 和 relay 行为。
exec-server/src/noise_relay/mod.rs源码 ↗
Noise relay 可以理解成一条加密的转发通道:两边通过 WebSocket 传消息,里面再套一层 Noise 加密协议。这个文件不负责真正收发每一条消息,而是像“入口牌”和“交通规则牌”。它公开了建立 Noise harness 连接需要用到的类型和函数,让别的地方不用知道内部文件怎么分工。它还设置了 WebSocket(一种长连接通信方式)的最大消息大小,先把太大的包挡在门外,防止还没来得及检查加密格式和 protobuf(一种结构化消息格式)时就占用过多内存。另一个关键点是序号:每条 relay 消息都会拿到一个不断递增的编号,这个编号也关系到 Noise 加密里隐含的 nonce(一次性数字,重复会有安全风险)。所以序号到顶后不会绕回 0,而是直接报协议错误。
noise_relay_websocket_config20–24 ↗
fn noise_relay_websocket_config() -> WebSocketConfig
作用:生成所有 Noise relay 端点都要使用的 WebSocket 限制配置。它的作用是提前限制单个帧和单条消息的最大大小,避免对方发超大数据把服务端内存拖垮。
数据流:进去时不需要外部输入 → 它从默认 WebSocket 配置开始,把最大帧大小和最大消息大小都设成 256KB → 出来的是一份带安全上限的 WebSocketConfig,供建立连接时使用;它不修改别的状态。
调用关系:当系统要连接 Noise rendezvous 或启动远程环境时,connect_noise_rendezvous 和 run_remote_environment 会拿这份配置去创建 WebSocket。这个函数只负责给出统一规则,底层默认值来自外部库的 default。
调用图:被 2 处调用(connect_noise_rendezvous, run_remote_environment);外部调用 1 个(default)。
take_next_sequence26–34 ↗
fn take_next_sequence(next_seq: &mut u32) -> Result<u32, ExecServerError>
作用:从一个可变的序号计数器里取出当前序号,并把计数器加一。它特别防止序号超过 u32 上限后回到 0,因为那会让加密消息的顺序和一次性数字变得不安全。
数据流:进去的是一个可修改的 next_seq 数字 → 它先记下当前值作为本次要用的序号,再尝试把 next_seq 加 1 → 如果加成功,返回刚才取出的序号并更新计数器;如果已经到最大值,返回协议错误,计数器不会安全地继续绕回使用。
调用关系:spawn_noise_virtual_stream 在生成或发送 Noise 虚拟流消息时会调用它,为每条消息分配明确的顺序号。这个函数不再把工作交给别的本地函数,只在溢出时构造 ExecServerError::Protocol 来阻止危险情况继续发生。
调用图:被 1 处调用(spawn_noise_virtual_stream)。
exec-server/src/noise_relay/harness.rs源码 ↗
这个文件解决的是“隔着一个不可信中转站安全通信”的问题。中转站按 stream_id 转发消息,但它不负责验证双方身份,也不该看到 JSON-RPC 明文。这里的做法像先在公共大厅里认准房间号,然后两个人用只有彼此能懂的暗号确认身份,确认后再传纸条。noise_harness_connection_from_websocket 会先生成一个新的 stream_id,向中转站声明要用这条虚拟通道,然后用 Noise 握手(一种加密握手协议,用来确认对方身份并生成加密通道)和注册表给出的执行器公钥完成认证。握手成功前,任何业务数据都会被拒绝,避免误走明文或未认证通道。握手后,发出的 JSON-RPC 消息会先切块、加密、编号,再塞进中转帧;收到的数据会先按编号排序去重,再解密,最后重新拼成完整 JSON-RPC 消息。重置帧是明文控制消息,所以这里会保留“连接被重置”的信号,但不会相信对方带来的原因文本。
noise_harness_connection_from_websocket69–401 ↗
fn noise_harness_connection_from_websocket(
stream: T,
args: NoiseHarnessConnectionArgs,
) -> JsonRpcConnection
作用:把一条普通 WebSocket 变成一条带身份校验和加密保护的 JsonRpcConnection。调用方拿到它后,可以像用普通 JSON-RPC 连接一样收发消息,但底层实际会经过 Noise 加密和中转帧封装。
数据流:输入是一条 WebSocket 流,以及执行器注册信息、环境 ID、公钥、授权字符串等连接参数。它先生成新的 stream_id,建立发送和接收队列,再启动一个后台任务:先构造握手绑定信息,发送 resume 和 handshake 帧,等执行器回应并完成 Noise 握手;之后,把外部送进 outgoing_tx 的 JSON-RPC 消息切块、加密、编号并写到 WebSocket,同时把 WebSocket 收到的二进制中转帧解码、过滤 stream_id、排序、解密、拼回 JSON-RPC 消息后送进 incoming_rx。输出是一个 JsonRpcConnection;它还会在断开、格式错误或握手失败时向接收队列发出对应事件,并更新 disconnected_rx。
调用关系:这是本文件的主入口。上层代码在需要通过 Noise relay 连接执行器时调用它。它会用 noise_channel_prologue 生成握手要绑定的上下文,用 InitiatorHandshake::start 开始握手,用 encode_relay_message_frame 和 decode_relay_message_frame 在 WebSocket 字节和中转帧之间转换。连接运行中,它把收到的数据交给 receive_data 做排序、解密和 JSON-RPC 重组;遇到严重错误时,会调用 send_disconnected 或 send_malformed 通知上层。
调用图:调用 5 个内部函数(start, noise_channel_prologue, send_disconnected, decode_relay_message_frame, encode_relay_message_frame);外部调用 15 个(new_v4, debug!, default, default, handshake, resume, format!, info!, channel, select! (+5 more))。
receive_data406–431 ↗
async fn receive_data(
inbound_ciphertexts: &mut OrderedCiphertextFrames,
transport: &mut NoiseTransport,
decoder: &mut JsonRpcMessageDecoder,
data: RelayData,
incoming_tx: &mpsc::
作用:处理一帧已经到达 harness 的加密数据:先把它放到正确顺序,再解密,最后尽量拼出完整 JSON-RPC 消息。它存在的原因是中转层的数据帧和 JSON-RPC 消息边界不一样,不能收到一帧就直接当成一条消息。
数据流:输入包括一个按序缓存器 inbound_ciphertexts、一条 NoiseTransport 加密通道、一个 JSON-RPC 解码器、收到的 RelayData,以及要发事件的 incoming_tx。它先把 RelayData 的序号和密文交给缓存器,只有轮到正确顺序的密文才会被取出;然后用 NoiseTransport 解密;解密出的明文字节交给 JsonRpcMessageDecoder,可能得到零条、一条或多条完整 JSON-RPC 消息;每条完整消息都会作为 JsonRpcConnectionEvent::Message 发给 incoming_tx。结果是返回成功或协议错误,同时可能向连接的接收端送出新消息。
调用关系:它是主连接循环里“收到 Data 帧之后”的下一站。noise_harness_connection_from_websocket 负责识别和解析中转帧,一旦确认是当前 stream_id 的数据帧,就把真正的数据交给 receive_data。receive_data 再把排序交给 OrderedCiphertextFrames,把解密交给 NoiseTransport,把消息拼接交给 JsonRpcMessageDecoder,最后把成品消息交回 JsonRpcConnection 的事件通道。
调用图:调用 3 个内部函数(decrypt, push, push);外部调用 2 个(send, Message)。
send_malformed433–437 ↗
async fn send_malformed(incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>, reason: String)
作用:向连接使用者报告“收到的东西格式不对”。它是一个小工具函数,用来统一发送 MalformedMessage 事件。
数据流:输入是 incoming_tx 事件发送通道和一段错误原因文字。它把原因包装成 JsonRpcConnectionEvent::MalformedMessage,然后异步发送出去。输出没有正式返回值;如果接收端已经没了,发送失败会被忽略,因为连接本身已经不值得继续处理。
调用关系:它被主 WebSocket 后台任务在发现坏帧、非法握手后数据、错误文本帧等情况时使用。它不修复连接,只是把“对方发来的数据不合规”这个事实告诉上层,让上层按异常连接处理。
调用图:外部调用 1 个(send)。
send_disconnected439–450 ↗
async fn send_disconnected(
incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>,
disconnected_tx: &watch::Sender<bool>,
reason: String,
)
作用:向连接使用者报告“这条连接已经断了”,并带上一个安全处理过的原因。它同时通知事件队列和断开状态监听器,避免不同观察者看到不一致状态。
数据流:输入是 incoming_tx 事件通道、disconnected_tx 状态通道,以及断开原因字符串。它先把 disconnected_tx 设为 true,表示连接已经断开;再发送 JsonRpcConnectionEvent::Disconnected,里面带上原因。输出没有正式返回值;如果没人接收,发送失败会被忽略。
调用关系:它由 noise_harness_connection_from_websocket 在握手失败、WebSocket 提前结束、收到 reset、解析失败等无法继续通信的地方调用。它相当于统一的“拉下闸门”动作:既让等待断开状态的人知道连接没了,也让读取消息事件的人收到断开说明。
调用图:被 1 处调用(noise_harness_connection_from_websocket);外部调用 1 个(send)。
exec-server/src/noise_relay/executor_stream.rs源码 ↗
可以把这里的虚拟流想成一条加密隧道里的“分车道”。真实的 relay 连接只有一条,但里面可能跑很多条 JSON-RPC 对话。这个文件做的事就是:收到外面的加密数据时,先按序号排好,解密,再拆成完整的 JSON-RPC 消息,丢给上层处理;上层要发消息时,则把 JSON-RPC 消息切成小块,加密,加序号,再塞回真实连接发出去。NoiseTransport 是加密状态,里面有收发用的编号,所以读写两边用互斥锁(一把锁,防止两个任务同时改同一份数据)共享它,但代码特意不在等待网络时拿着锁,避免卡住别的任务。文件还用 instance_id 区分“同一个 stream_id 的新旧两次连接”,防止迟到的关闭通知误删新连接。
NoiseVirtualStream::disconnect56–61 ↗
fn disconnect(self, reason: Option<String>)
作用:这个函数用来主动把一条虚拟流标记为断开,并通知正在处理 JSON-RPC 的那一边。有人会在 relay 告诉我们这条流被重置、或者本地决定放弃这条流时用它。
数据流:进去的是这个虚拟流本身,以及一个可选的断开原因文字。它先通过 watch 通道广播“已经断开”,再尝试往消息队列里塞一个 Disconnected 事件。出来没有返回值;结果是等待这条连接的处理逻辑会知道连接已经不能继续用了。
调用关系:它是虚拟流生命周期里的收尾动作。它不做加密解密,也不碰真实网络发送,只是把断开的事实告诉本地 JSON-RPC 处理流程;内部用 send 和 try_send 把通知送出去。
NoiseVirtualStream::receive_data65–87 ↗
fn receive_data(&mut self, data: RelayData) -> Result<(), ExecServerError>
作用:这个函数处理从 relay 收到的一段加密数据。它负责把可能乱序到达的数据排回正确顺序,解密,并交给 JSON-RPC 处理队列。
数据流:进去的是一包 RelayData,里面有序号和加密内容。函数先把它交给 OrderedCiphertextFrames 排序,拿到可以按顺序处理的密文;然后用 NoiseTransport 解密成明文;接着用 JsonRpcMessageDecoder 把明文拼出完整的 JSON-RPC 消息;最后把每条消息作为 Message 事件尝试放进 incoming_tx。出来是成功或错误;如果解密失败、格式不对,或者本地队列满了/关了,就返回协议错误。
调用关系:它位于共享读取循环和单条虚拟流之间。真实连接的读循环收到某个 stream_id 的数据后,会把数据交给这条虚拟流的 receive_data;receive_data 再把完整消息交给上层 JSON-RPC 连接处理。它调用两层 push:一层排密文顺序,一层拼 JSON-RPC 消息。
spawn_noise_virtual_stream94–189 ↗
fn spawn_noise_virtual_stream(
stream_id: String,
instance_id: u64,
processor: ConnectionProcessor,
physical_outgoing_tx: mpsc::Sender<Vec<u8>>,
closed_stream_tx: mpsc::Sender<Clos
作用:这个函数创建并启动一条新的 Noise 虚拟流。握手完成后,系统调用它,把加密通道接到 JSON-RPC 处理器上,同时启动后台写入任务。
数据流:进去的是 stream_id、instance_id、连接处理器、发往真实 relay 的发送通道、关闭通知通道,以及已经握手好的 NoiseTransport。它创建 JSON-RPC 的收发队列和断开通知,把 NoiseTransport 包进共享锁里;然后启动一个写任务:从 JSON-RPC 出站队列收消息,先 frame_jsonrpc_message 打包,再按固定大小切块,用 take_next_sequence 分配序号,用 encrypt 加密,最后 encode_relay_message_frame 后发到真实连接。它还启动处理器任务运行 run_connection。出来的是 NoiseVirtualStream,也就是读半边;调用者以后用它接收 relay 发来的数据。
调用关系:它是这条虚拟流的装配工。上游在 Noise 握手成功后调用它;它一边把 JsonRpcConnection 交给 processor.run_connection 跑业务,一边让后台 writer 负责把业务发出的 JSON-RPC 消息变成 relay 能传的加密帧。写任务结束时会尽力发 reset,并把 ClosedNoiseVirtualStream 发回去,告诉外层可以清理这条流,而且用 instance_id 避免误清理复用的 stream_id。
调用图:调用 5 个内部函数(encrypt, frame_jsonrpc_message, take_next_sequence, encode_relay_message_frame, run_connection);外部调用 15 个(clone, new, new, clone, send, try_send, default, default, data, reset (+5 more))。
exec-server/src/remote.rs源码 ↗
可以把这个文件想成远程执行器的“上线接待员”。它先整理配置,比如环境 ID 和登记服务地址;然后带着认证信息去环境登记服务注册自己的 Noise 公钥。Noise 是一种加密握手协议,这里用来保证后面传输的内容不是明文裸奔。登记成功后,服务端会返回一个中转站地址和注册编号。exec-server 再去连接这个中转站,并把真正处理请求的 ConnectionProcessor 交给 relay 层运行。连接断了会自动重试,等待时间会逐步变长;如果中转站说旧注册链接不认了,它会重新注册。文件里还特别注意两件事:调试输出会隐藏认证信息,避免泄漏令牌;错误响应只截取一小段,避免日志里塞进太多或太敏感的内容。
EnvironmentRegistryClient::fmt35–40 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:给 EnvironmentRegistryClient 生成调试用文字,但故意不把认证信息打印出来。这样排查问题时能看到登记服务地址,又不会把令牌之类的秘密写进日志。
数据流:输入是这个客户端对象和格式化器 → 它把 base_url 写进调试结构,把 auth_provider 用“<redacted>”代替 → 输出是一段安全的调试文本,不改动客户端本身。
调用关系:这是 Rust 的 Debug 显示流程会自动用到的函数。它不参与联网,只是在有人打印客户端调试信息时保护敏感信息。
调用图:外部调用 1 个(debug_struct)。
EnvironmentRegistryClient::new44–53 ↗
fn new(base_url: String, auth_provider: SharedAuthProvider) -> Result<Self, ExecServerError>
作用:创建一个用来访问环境登记服务的客户端。它会先把服务地址整理干净,并且关闭自动跳转,避免认证头被带到不该去的地址。
数据流:输入是登记服务 base_url 和认证提供者 → 它调用 normalize_base_url 去掉多余空白和结尾斜杠,再创建 reqwest HTTP 客户端并禁止重定向 → 输出是可复用的 EnvironmentRegistryClient,里面带着地址、认证方式和 HTTP 工具。
调用关系:run_remote_environment 启动远程环境时会创建它,测试也会用它验证注册行为。后续注册环境和校验 harness 密钥都会靠这个客户端发 HTTP 请求。
调用图:调用 1 个内部函数(normalize_base_url);被 5 处调用(validate_harness_key_does_not_expose_error_body, validate_harness_key_requires_explicit_valid_response, run_remote_environment, register_environment_does_not_follow_redirects_with_auth_headers, register_environment_posts_with_auth_provider_headers);外部调用 2 个(builder, none)。
EnvironmentRegistryClient::register_environment57–100 ↗
async fn register_environment(
&self,
environment_id: &str,
executor_public_key: &NoiseChannelPublicKey,
) -> Result<EnvironmentRegistryRegistrationResponse, ExecServerErro
作用:把当前执行器注册到环境登记服务,并拿回中转站地址。简单说,就是告诉云端“我这个执行器上线了,这是我的加密公钥,请给我一个接头地点”。
数据流:输入是 environment_id 和执行器的 Noise 公钥 → 它拼出注册接口地址,带上认证头,发送安全协议名和公钥 → 收到响应后解析 JSON,并检查返回的环境 ID 和安全协议是否和请求一致 → 输出是注册结果,包含中转 WebSocket 地址和注册编号;如果内容不对就报协议错误。
调用关系:run_remote_environment 在启动时先调用它;如果后面连接中转站时发现注册链接被拒,也会再次调用它。它把响应解析工作交给 EnvironmentRegistryClient::parse_json_response。
调用图:调用 2 个内部函数(parse_json_response, endpoint_url);外部调用 6 个(to_auth_headers, post, debug!, Protocol, format!, info!)。
EnvironmentRegistryClient::parse_json_response102–120 ↗
async fn parse_json_response(
&self,
response: reqwest::Response,
) -> Result<R, ExecServerError>
作用:统一处理环境登记服务的 HTTP 响应。成功时把 JSON 变成程序能用的结构,失败时把状态码和错误内容变成更清楚的 ExecServerError。
数据流:输入是一次 HTTP 响应 → 如果状态码表示成功,它直接读取 JSON;如果是未认证或无权限,它走 environment_registry_auth_error;其他失败则走 environment_registry_http_error → 输出是解析好的对象,或者一个带状态和消息的错误。
调用关系:EnvironmentRegistryClient::register_environment 用它来处理注册接口的返回。这样注册函数不用自己重复写各种 HTTP 错误判断。
调用图:调用 2 个内部函数(environment_registry_auth_error, environment_registry_http_error);被 1 处调用(register_environment);外部调用 3 个(status, text, matches!)。
RegistryHarnessKeyValidator::validate_harness_key160–205 ↗
async fn validate_harness_key(
&self,
harness_public_key: &NoiseChannelPublicKey,
authorization: &str,
) -> Result<(), ExecServerError>
作用:确认远端连接方的 harness 公钥是否被登记服务允许使用当前执行器。Noise 握手证明“对方确实持有这把钥匙”,而这个函数再问登记服务“这把钥匙有没有权限”。
数据流:输入是 harness 公钥和一段短期授权字符串 → 它带着执行器注册编号、公钥和授权字符串请求 validate 接口 → 如果 HTTP 失败会返回认证或登记服务错误;如果响应里的 valid 不是 true,就返回协议错误 → 成功时不返回数据,只表示这把钥匙通过检查。
调用关系:它实现 HarnessKeyValidator 这套接口,会被 run_multiplexed_environment 在收到远端连接、完成初步加密握手后调用。它依赖 EnvironmentRegistryClient 发请求,也用 endpoint_url 拼地址。
调用图:调用 1 个内部函数(endpoint_url);外部调用 4 个(EnvironmentRegistryAuth, Protocol, format!, matches!)。
RemoteEnvironmentConfig::fmt218–225 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:给远程环境配置生成调试文本,并隐藏认证提供者。这样日志里能看到环境 ID 和名字,但不会泄露认证细节。
数据流:输入是配置对象和格式化器 → 它写出 base_url、environment_id、name,把 auth_provider 写成“<redacted>” → 输出安全的调试展示,不改变配置。
调用关系:当有人打印 RemoteEnvironmentConfig 的调试信息时自动使用。tests::debug_output_redacts_auth_provider 专门检查它不会把敏感信息打出来。
调用图:外部调用 1 个(debug_struct)。
RemoteEnvironmentConfig::new229–241 ↗
fn new(
base_url: String,
environment_id: String,
auth_provider: SharedAuthProvider,
) -> Result<Self, ExecServerError>
作用:创建远程运行所需的配置,并检查环境 ID 不是空的。它相当于把用户给的配置先过一遍门禁,避免后面拿空 ID 去注册。
数据流:输入是登记服务地址、环境 ID 和认证提供者 → 它调用 normalize_environment_id 修剪并校验环境 ID,然后填入默认名称 codex-exec-server → 输出 RemoteEnvironmentConfig;如果环境 ID 为空则输出配置错误。
调用关系:命令入口 run_exec_server_command 会用它准备远程模式配置,远程连接相关测试也会用它。真正运行时,这个配置会交给 run_remote_environment。
调用图:调用 1 个内部函数(normalize_environment_id);被 4 处调用(run_exec_server_command, reconnect_reuses_registration_until_url_is_rejected, debug_output_redacts_auth_provider, remote_environment_routes_encrypted_exec_server_rpc)。
run_remote_environment250–320 ↗
async fn run_remote_environment(
config: RemoteEnvironmentConfig,
runtime_paths: ExecServerRuntimePaths,
) -> Result<(), ExecServerError>
作用:这是远程执行模式的主循环。它负责注册、连接中转站、启动加密多路通信,并在断线后自动重连。
数据流:输入是远程配置和运行时路径 → 它先确保 TLS 加密库准备好,创建登记客户端和连接处理器,生成一次性的执行器身份密钥 → 注册环境拿到中转地址 → 不断尝试连接 WebSocket;连上后把连接交给 run_multiplexed_environment;失败后按 1 秒、2 秒、4 秒直到最多 30 秒的节奏等待重试 → 正常情况下这个函数一直运行,只有遇到不可恢复错误才返回错误。
调用关系:它是本文件把各个零件串起来的核心:用 EnvironmentRegistryClient::new 建客户端,用 EnvironmentRegistryClient::register_environment 注册,用 noise_relay_websocket_config 配置 WebSocket,再把已连接的通道交给 run_multiplexed_environment。
调用图:调用 5 个内部函数(generate, noise_relay_websocket_config, run_multiplexed_environment, new, new);外部调用 8 个(from_secs, ensure_rustls_crypto_provider, debug!, info!, matches!, sleep, connect_async_with_config, warn!)。
normalize_environment_id322–330 ↗
fn normalize_environment_id(environment_id: String) -> Result<String, ExecServerError>
作用:清理并检查环境 ID。它防止用户只输入空格或空字符串,免得后面请求登记服务时出现难懂的错误。
数据流:输入是原始 environment_id 字符串 → 它去掉前后空白 → 如果结果为空就生成配置错误;否则输出清理后的 ID。
调用关系:RemoteEnvironmentConfig::new 会调用它。也就是说,配置刚创建时就把环境 ID 问题挡住,而不是等到联网注册时才爆炸。
调用图:被 1 处调用(new);外部调用 1 个(EnvironmentRegistryConfig)。
normalize_base_url343–351 ↗
fn normalize_base_url(base_url: String) -> Result<String, ExecServerError>
作用:清理环境登记服务的基础地址。它去掉前后空白和末尾多余斜杠,让后面拼接口地址时不会出现双斜杠或空地址。
数据流:输入是原始 base_url → 它 trim 掉空白,再去掉结尾的“/” → 如果结果为空就返回配置错误;否则输出整理后的地址。
调用关系:EnvironmentRegistryClient::new 创建客户端时调用它。endpoint_url 后面会基于这个干净地址拼出具体接口。
调用图:被 1 处调用(new);外部调用 1 个(EnvironmentRegistryConfig)。
endpoint_url353–355 ↗
fn endpoint_url(base_url: &str, path: &str) -> String
作用:把基础地址和接口路径拼成完整网址。它处理路径开头的斜杠,避免拼出格式不稳定的 URL。
数据流:输入是 base_url 和 path → 它去掉 path 开头多余的“/”,再用一个斜杠连接两段 → 输出完整接口 URL 字符串。
调用关系:EnvironmentRegistryClient::register_environment 用它拼注册接口,RegistryHarnessKeyValidator::validate_harness_key 用它拼校验接口。
调用图:被 2 处调用(register_environment, validate_harness_key);外部调用 1 个(format!)。
environment_registry_auth_error357–362 ↗
fn environment_registry_auth_error(status: StatusCode, body: &str) -> ExecServerError
作用:把登记服务的认证失败响应变成更明确的错误。也就是把“401/403”这类状态码翻译成“认证失败,并附上可读原因”。
数据流:输入是 HTTP 状态码和响应正文 → 它先用 registry_error_message 提取错误消息,提不到就写“empty error body” → 输出 ExecServerError::EnvironmentRegistryAuth,里面包含状态码和说明。
调用关系:EnvironmentRegistryClient::parse_json_response 在遇到未认证或无权限时调用它。它把错误消息提取工作交给 registry_error_message。
调用图:调用 1 个内部函数(registry_error_message);被 1 处调用(parse_json_response);外部调用 2 个(EnvironmentRegistryAuth, format!)。
environment_registry_http_error364–388 ↗
fn environment_registry_http_error(status: StatusCode, body: &str) -> ExecServerError
作用:把登记服务的普通 HTTP 失败响应整理成程序统一认识的错误。它会尽量从 JSON 里取错误代码和消息,取不到就截取原始正文的一小段。
数据流:输入是 HTTP 状态码和响应正文 → 它尝试按登记服务的错误格式解析 JSON;如果有 error.code 和 error.message 就使用它们;如果没有,就调用 preview_error_body 取一个短预览 → 输出 ExecServerError::EnvironmentRegistryHttp,包含状态码、可选错误代码和消息。
调用关系:EnvironmentRegistryClient::parse_json_response 在非认证类失败时调用它。它负责把外部服务杂乱的错误响应变成内部稳定的错误形状。
调用图:被 1 处调用(parse_json_response)。
registry_error_message390–396 ↗
fn registry_error_message(body: &str) -> Option<String>
作用:从登记服务错误正文里提取一条给人看的消息。它优先读标准 JSON 错误字段,失败时退而求其次截取正文预览。
数据流:输入是响应正文字符串 → 它尝试解析成 RegistryErrorBody,并取出 error.message → 如果解析失败或没有 message,就调用 preview_error_body → 输出可选的错误消息;正文为空时可能没有输出。
调用关系:environment_registry_auth_error 用它生成认证失败说明。它和 preview_error_body 配合,保证错误消息尽量有用但不过度暴露内容。
调用图:被 1 处调用(environment_registry_auth_error)。
preview_error_body398–404 ↗
fn preview_error_body(body: &str) -> Option<String>
作用:安全地截取错误正文的一小段。这样日志或错误里能看到线索,但不会把超长内容全部塞进去。
数据流:输入是响应正文 → 它先去掉前后空白;如果为空就返回 None;否则最多取 ERROR_BODY_PREVIEW_BYTES 个字符 → 输出这段短预览。
调用关系:registry_error_message 和 environment_registry_http_error 会在没有更标准错误消息时使用它。它是错误展示的兜底工具。
tests::StaticRegistryAuthProvider::add_auth_headers428–437 ↗
fn add_auth_headers(&self, headers: &mut HeaderMap)
作用:测试用的固定认证提供者。它模拟真实认证逻辑,给请求加上固定的 Authorization 和账户 ID 请求头。
数据流:输入是一个可修改的 HTTP 头集合 → 它插入“Bearer registry-token”和“workspace-123”两个固定值 → 输出体现在传入的 headers 被改好了,没有单独返回值。
调用关系:tests::static_registry_auth_provider 会把它包装成共享认证提供者。注册相关测试用它确认 EnvironmentRegistryClient 确实把认证头带到了登记请求里。
调用图:外部调用 2 个(insert, from_static)。
tests::static_registry_auth_provider440–442 ↗
fn static_registry_auth_provider() -> SharedAuthProvider
作用:快速创建测试用认证提供者,避免每个测试重复写包装代码。
数据流:输入为空 → 它创建 StaticRegistryAuthProvider,并放进 Arc 共享指针里,也就是一份可以被多处安全共用的对象 → 输出 SharedAuthProvider。
调用关系:多个测试函数会调用它,包括注册请求带认证头、禁止跳转携带认证头、调试输出脱敏等测试。
调用图:外部调用 1 个(new)。
tests::register_environment_posts_with_auth_provider_headers445–483 ↗
async fn register_environment_posts_with_auth_provider_headers()
作用:测试注册环境时,客户端会用正确地址、正确请求体、正确认证头发 POST 请求,并能读回成功响应。
数据流:输入来自测试内部搭建的假登记服务器和生成的执行器公钥 → 测试配置 mock 服务期待某个 POST 请求,创建客户端并调用 register_environment → 输出是断言通过或失败;通过表示注册请求格式和响应解析都符合预期。
调用关系:它直接验证 EnvironmentRegistryClient::new 和 EnvironmentRegistryClient::register_environment 的配合。StaticRegistryAuthProvider 提供固定认证头,wiremock 假服务器扮演登记服务。
调用图:调用 2 个内部函数(generate, new);外部调用 10 个(given, start, new, assert_eq!, static_registry_auth_provider, json!, body_partial_json, header, method, path)。
tests::register_environment_does_not_follow_redirects_with_auth_headers486–521 ↗
async fn register_environment_does_not_follow_redirects_with_auth_headers()
作用:测试客户端遇到重定向时不会自动跟过去。重点是防止认证头被带到跳转目标,避免令牌泄漏。
数据流:输入是测试搭好的假服务器:注册接口返回 302 跳转,跳转目标声明不应该收到带认证头的请求 → 测试创建客户端并调用 register_environment → 输出是断言得到 HTTP 错误,而不是成功跟随跳转。
调用关系:它验证 EnvironmentRegistryClient::new 里禁用重定向这件事确实生效。这个测试保护的是安全边界,而不只是普通功能。
调用图:调用 2 个内部函数(generate, new);外部调用 9 个(given, start, new, assert!, static_registry_auth_provider, format!, header, method, path)。
tests::debug_output_redacts_auth_provider524–536 ↗
fn debug_output_redacts_auth_provider()
作用:测试远程配置的调试输出会隐藏认证信息。它确保日志里出现的是“<redacted>”,而不是账户或令牌内容。
数据流:输入是一个用固定认证提供者创建的 RemoteEnvironmentConfig → 测试把它格式化成调试字符串 → 检查字符串包含“<redacted>”且不包含“workspace-123” → 输出是测试通过或失败。
调用关系:它验证 RemoteEnvironmentConfig::fmt 的脱敏行为,也间接使用 RemoteEnvironmentConfig::new 和 tests::static_registry_auth_provider。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert!, static_registry_auth_provider, format!)。
exec-server/src/client_transport.rs源码 ↗
这个文件解决的是“客户端到底怎么连到执行服务器”的问题。执行服务器可能在远端,通过 WebSocket 连;也可能要先经过一个 rendezvous 中转点,再用 Noise(一种双方验证身份并加密通信的握手协议)保护内容;还可能就是本机启动的一个子进程,通过 stdin/stdout(标准输入输出,像两根管子)对话。这里先根据参数选路,再把不同的路都包装成同一种 JsonRpcConnection,最后交给 ExecServerClient::connect 做共同的初始化。一个重要细节是:Noise 连接每次都会重新向 provider 拿连接包,这样重连时 URL 和授权是新的,但客户端身份不变。另一个细节是:本地命令的 stderr 会被单独读出来写日志,避免子进程报错时没人看见。
ExecServerClient::connect_for_transport34–80 ↗
async fn connect_for_transport(
transport_params: crate::client_api::ExecServerTransportParams,
) -> Result<Self, ExecServerError>
作用:这是连接入口里的“分岔路口”。调用方只要给出想用哪种交通方式,它就选择对应的连接函数,并补上统一的客户端名字和默认超时时间。
数据流:进去的是一份传输参数,里面说明要走 WebSocket、Noise rendezvous,还是本地 stdio 命令。它按类型拆开参数:WebSocket 就直接带 URL 去连;Noise 就先向 provider 要新的连接包;stdio 就准备启动命令。出来的是已经完成初始化的 ExecServerClient,或者返回说明哪里失败的 ExecServerError。
调用关系:它负责把外部的高层选择翻译成具体连接动作。它会把工作分别交给 ExecServerClient::connect_websocket、ExecServerClient::connect_noise_rendezvous 或 ExecServerClient::connect_stdio_command;这些函数连上线后,再进入共同的客户端初始化流程。
调用图:外部调用 3 个(connect_noise_rendezvous, connect_stdio_command, connect_websocket)。
ExecServerClient::connect_websocket82–106 ↗
async fn connect_websocket(
args: RemoteExecServerConnectArgs,
) -> Result<Self, ExecServerError>
作用:这个函数用普通 WebSocket 连接远端 exec-server。WebSocket 可以理解成一条长期打开的网络通道,双方可以持续互发消息。
数据流:进去的是 WebSocket URL、连接超时时间、初始化超时时间、客户端名字等参数。它先确保 TLS 加密库可用,然后在限定时间内尝试连 URL;连上后检查 URL 里是不是标着 role=harness,如果是就按 rendezvous harness 通道包装,否则按普通 JSON-RPC 通道包装。最后把这条通道交给统一的 connect 初始化,出来一个 ExecServerClient。
调用关系:它通常由 ExecServerClient::connect_for_transport 在选择 WebSocket 方案时调用。内部会用 is_rendezvous_harness_url 判断通道类型;随后调用 harness_connection_from_websocket 或 JsonRpcConnection::from_websocket 做包装,最后交给 ExecServerClient::connect 完成后续握手和初始化。
调用图:调用 3 个内部函数(is_rendezvous_harness_url, from_websocket, harness_connection_from_websocket);外部调用 6 个(connect, ensure_rustls_crypto_provider, into, format!, timeout, connect_async)。
ExecServerClient::connect_noise_rendezvous111–176 ↗
async fn connect_noise_rendezvous(
args: NoiseRendezvousConnectArgs,
) -> Result<Self, ExecServerError>
作用:这个函数通过 rendezvous 中转服务连接 exec-server,并在 WebSocket 外面再套一层 Noise 加密和身份验证。它适合需要确认对方身份、并且不想让中转点看到明文内容的场景。
数据流:进去的是一整包 rendezvous 连接资料:WebSocket URL、环境 ID、执行器注册 ID、对方公钥、授权信息、自己的身份密钥、超时时间等。它先确保 TLS 加密库可用,再带着专用 WebSocket 配置去连接中转 URL;日志和错误里会去掉 URL 的查询参数,避免泄露授权信息。连上后,它用 Noise 参数把 WebSocket 包成加密通道,再交给统一初始化,出来是可用的 ExecServerClient。
调用关系:它由 ExecServerClient::connect_for_transport 在 NoiseRendezvous 方案下调用。它会使用 noise_relay_websocket_config 配置 WebSocket,再用 noise_harness_connection_from_websocket 建立加密的 harness 连接,最后把连接交给 ExecServerClient::connect。
调用图:调用 1 个内部函数(noise_relay_websocket_config);外部调用 6 个(connect, ensure_rustls_crypto_provider, noise_harness_connection_from_websocket, format!, timeout, connect_async_with_config)。
ExecServerClient::connect_stdio_command178–216 ↗
async fn connect_stdio_command(
args: StdioExecServerConnectArgs,
) -> Result<Self, ExecServerError>
作用:这个函数不是去连网络,而是在本机启动一个 exec-server 命令,然后通过它的标准输入和标准输出跟它说话。可以把它想成打开一个子程序,再用两根管子和它交换 JSON-RPC 消息。
数据流:进去的是要启动的程序、参数、环境变量、工作目录,以及初始化选项。它先用 stdio_command_process 组装命令,再把 stdin、stdout、stderr 都接成管道并启动子进程;如果拿不到 stdin 或 stdout,就返回协议错误。stderr 会被后台任务逐行读取并写入日志。最后它把 stdout/stdin 包成 JsonRpcConnection,并把子进程挂在连接上,交给统一初始化,出来一个 ExecServerClient。
调用关系:它由 ExecServerClient::connect_for_transport 在 StdioCommand 方案下调用。它先请 stdio_command_process 准备命令对象,然后用 JsonRpcConnection::from_stdio 把子进程的输入输出变成通信通道,最后调用 ExecServerClient::connect 完成客户端初始化。
调用图:调用 2 个内部函数(stdio_command_process, from_stdio);外部调用 7 个(new, connect, piped, debug!, into, spawn, warn!)。
is_rendezvous_harness_url219–227 ↗
fn is_rendezvous_harness_url(websocket_url: &str) -> bool
作用:这个小函数用来判断一个 WebSocket URL 是不是 rendezvous harness 专用入口。它看的标志很简单:查询参数里是否有 role=harness。
数据流:进去的是一段 URL 字符串。它先找问号后面的查询参数;没有问号就直接认为不是。然后把参数按 & 拆开,再看有没有 key 是 role、value 是 harness 的一项。出来的是 true 或 false。
调用关系:它只被 ExecServerClient::connect_websocket 使用。connect_websocket 用这个判断结果决定:这条 WebSocket 是按普通 JSON-RPC 包装,还是按 harness rendezvous 通道包装。
调用图:被 1 处调用(connect_websocket)。
stdio_command_process229–239 ↗
fn stdio_command_process(stdio_command: &StdioExecServerCommand) -> Command
作用:这个函数把“要运行哪个程序、带什么参数、在哪个目录运行、带哪些环境变量”整理成一个可启动的命令对象。它是启动本地 exec-server 前的准备工。
数据流:进去的是 StdioExecServerCommand,里面有程序名、参数、环境变量和可选工作目录。它创建一个 tokio::process::Command,塞入参数和环境变量,必要时设置当前目录;在 Unix 系统上还会给它设置新的进程组,方便之后把这一组进程一起处理。出来的是准备好的 Command。
调用关系:它被 ExecServerClient::connect_stdio_command 调用。connect_stdio_command 先靠它把命令准备好,再接上 stdin/stdout/stderr 管道并真正 spawn 启动子进程。
调用图:被 1 处调用(connect_stdio_command);外部调用 1 个(new)。
MCP sidecar 启动
这些文件为 MCP 服务器构建运行时上下文和启动机制,然后管理每个服务器的 RMCP 客户端生命周期。
codex-mcp/src/runtime.rs源码 ↗
MCP 可以理解成一套让模型调用外部工具的协议。一个 MCP 服务器可能跑在本机,也可能跑在远端环境里;可能通过标准输入输出通信,也可能通过 HTTP 通信。这个文件的核心作用,就是在真正启动或连接服务器之前,把“该用哪个运行环境”这件事判断清楚,避免把远端服务器当成本地跑,或让远端标准输入输出服务器拿到一个含糊的相对目录。McpRuntimeContext 像一张运行环境登记表加一个本地目录备忘录:它从 EnvironmentManager 里查环境,不存在时按规则报错或允许 HTTP 走本地默认方式。SandboxState 是发给支持沙箱的服务器看的状态说明,告诉它权限配置、沙箱策略、工作目录等信息。emit_duration 则像一个公共秒表,把启动、刷新、列工具等操作的耗时交给全局指标系统;如果指标系统没开,它就安静跳过。
McpRuntimeContext::new44–52 ↗
fn new(
environment_manager: Arc<EnvironmentManager>,
local_stdio_fallback_cwd: PathBuf,
) -> Self
作用:创建一个 MCP 运行时上下文。调用方把“环境登记表”和“本地标准输入输出服务器缺省工作目录”交进来,后面解析服务器配置时就靠它们做判断。
数据流:输入是一份共享的 EnvironmentManager(环境管理器,像登记所有可用运行地点的名单)和一个目录路径 → 函数把它们原样装进 McpRuntimeContext → 输出一个可复制使用的上下文对象,不额外启动服务器,也不改动外部状态。
调用关系:它是准备阶段的入口小零件。列 MCP 状态、读取 MCP 资源、创建客户端以及多种测试都会先调用它,把后续 make_rmcp_client 等流程需要的运行信息打包好。
调用图:被 13 处调用(list_mcp_server_status, read_mcp_resource, no_local_runtime_fails_local_stdio_but_keeps_local_http_server, explicit_remote_stdio_and_http_accept_named_environment, local_http_does_not_require_local_stdio_availability, local_stdio_accepts_local_environment_when_available, local_stdio_requires_local_stdio_availability, remote_stdio_requires_absolute_cwd, unknown_explicit_environment_is_rejected, list_accessible_connectors_from_mcp_tools_with_mcp_manager (+3 more))。
McpRuntimeContext::local_stdio_fallback_cwd54–56 ↗
fn local_stdio_fallback_cwd(&self) -> PathBuf
作用:取出本地 stdio MCP 服务器的备用工作目录。stdio 指“标准输入输出”,就像程序通过键盘输入和屏幕输出那条管道通信;如果服务器配置里没写目录,就用这个兜底目录。
数据流:输入是已有的运行时上下文 → 函数读取里面保存的 local_stdio_fallback_cwd 并复制一份路径 → 输出这份路径副本,不会让调用方直接改到上下文里的原始值。
调用关系:make_rmcp_client 在准备本地 stdio 客户端时会问它要这个兜底目录。它不自己判断服务器能不能跑,只负责把事先保存好的目录安全交出去。
调用图:被 1 处调用(make_rmcp_client);外部调用 1 个(clone)。
McpRuntimeContext::resolve_server_environment58–89 ↗
fn resolve_server_environment(
&self,
server_name: &str,
config: &codex_config::McpServerConfig,
) -> Result<Option<Arc<Environment>>, String>
作用:根据某个 MCP 服务器的配置,判断它应该绑定到哪个运行环境,或者为什么不能绑定。它能把“本地、远端、HTTP、stdio”这些情况分清楚,提前给出清楚的错误。
数据流:输入是服务器名字和服务器配置,函数还会读取上下文里的环境管理器 → 先按配置里的环境 ID 查登记表;查到了就返回这个环境,但远端 stdio 还要检查工作目录必须是绝对路径;查不到时,如果是本地 HTTP 可以返回 None 表示不用专门环境,如果是本地 stdio 或未知远端环境就返回错误文字 → 输出要么是一个可用环境,要么是 None,要么是一条说明原因的错误。
调用关系:make_rmcp_client 在真正创建 MCP 客户端前会调用它。它把远端 stdio 的目录检查交给 ensure_remote_stdio_cwd,把环境是否属于本地的判断交给配置对象的 is_local_environment。
调用图:调用 2 个内部函数(ensure_remote_stdio_cwd, is_local_environment);被 1 处调用(make_rmcp_client);外部调用 1 个(format!)。
ensure_remote_stdio_cwd92–111 ↗
fn ensure_remote_stdio_cwd(
server_name: &str,
config: &codex_config::McpServerConfig,
) -> Result<(), String>
作用:专门检查远端 stdio MCP 服务器有没有写清楚工作目录,而且这个目录必须是绝对路径。绝对路径就是从根目录开始的完整地址,避免远端机器不知道“相对当前目录”到底指哪里。
数据流:输入是服务器名字和服务器配置 → 如果服务器不是 stdio 通信方式,就直接通过;如果是 stdio,就看 cwd 是否存在且是否为绝对路径 → 输出成功或一条明确错误,比如缺少目录,或者目录是 relative 这种相对路径。
调用关系:它只被 McpRuntimeContext::resolve_server_environment 调用,是环境解析流程里的安全检查员。这样主流程不用塞满目录判断细节,远端 stdio 的特殊规则集中在这里。
调用图:被 1 处调用(resolve_server_environment);外部调用 1 个(format!)。
emit_duration113–117 ↗
fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)])
作用:记录某个操作花了多长时间。它是一个轻量的指标上报入口,方便统计 MCP 启动、列工具、刷新缓存等动作的耗时。
数据流:输入是指标名、耗时和一组标签(标签就是给数据贴的小说明,比如属于哪个服务器) → 函数先找全局指标系统;如果找到了,就把耗时记进去;如果没找到,就什么也不做 → 输出为空,最多影响外部的监控数据。
调用关系:它被工具缓存刷新、列工具、服务器启动任务等地方调用。它不参与业务判断,只像公共秒表一样横跨多个流程,让这些流程能统一上报耗时。
调用图:被 4 处调用(write_cached_codex_apps_tools_if_needed, hard_refresh_codex_apps_tools_cache, listed_tools, start_server_task);外部调用 1 个(global)。
tests::stdio_server131–155 ↗
fn stdio_server(environment_id: &str) -> McpServerConfig
作用:为测试快速造一个 stdio 类型的 MCP 服务器配置。这样每个测试不用重复写一大段相同配置。
数据流:输入是环境 ID 字符串 → 函数填好命令、参数、开关、超时、工具列表等默认值,并把通信方式设为 stdio → 输出一份 McpServerConfig 测试配置。
调用关系:它是测试里的小工厂函数,被多个测试用来制造本地或远端 stdio 服务器样本。tests::http_server 也借它生成基础配置,再改成 HTTP。
tests::http_server157–168 ↗
fn http_server(environment_id: &str) -> McpServerConfig
作用:为测试快速造一个 HTTP 类型的 MCP 服务器配置。HTTP 可以理解成通过网址请求通信,而不是通过标准输入输出管道通信。
数据流:输入是环境 ID 字符串 → 函数先复用 tests::stdio_server 的默认配置,再把传输方式替换成指向 http://127.0.0.1:1 的 HTTP 配置 → 输出一份 HTTP MCP 服务器测试配置。
调用关系:它服务于测试本地 HTTP 和远端 HTTP 的分支。通过复用 tests::stdio_server,测试能只关注传输方式差异,而不是重复维护整份配置。
调用图:外部调用 1 个(stdio_server)。
tests::local_stdio_requires_local_stdio_availability171–187 ↗
fn local_stdio_requires_local_stdio_availability()
作用:验证一个关键规则:本地 stdio MCP 服务器必须真的有本地运行环境可用。没有本地环境时,不能假装能启动。
数据流:测试先创建一个没有任何环境的环境管理器,再创建运行时上下文和本地 stdio 服务器配置 → 调用环境解析函数 → 预期得到错误,并检查错误文字正好说明“local stdio MCP server stdio requires a local environment”。
调用关系:它覆盖 McpRuntimeContext::resolve_server_environment 中“本地 stdio 但没有本地环境”的失败分支。测试用 tests::stdio_server 造配置,用断言确认错误没有被吞掉。
调用图:调用 2 个内部函数(new, without_environments);外部调用 5 个(new, from, assert_eq!, stdio_server, panic!)。
tests::local_http_does_not_require_local_stdio_availability190–203 ↗
fn local_http_does_not_require_local_stdio_availability()
作用:验证另一个例外规则:本地 HTTP MCP 服务器即使没有本地 stdio 运行环境,也可以继续解析成功。因为 HTTP 可以直接用普通 HTTP 客户端访问,不一定要本地执行环境。
数据流:测试创建一个空的环境管理器和运行时上下文,再创建本地 HTTP 服务器配置 → 调用环境解析函数 → 预期成功,并且结果是 None,表示不需要专门绑定一个环境对象。
调用关系:它覆盖 McpRuntimeContext::resolve_server_environment 里本地 HTTP 的宽松分支。它和本地 stdio 失败测试成对出现,说明两种传输方式的规则不同。
调用图:调用 2 个内部函数(new, without_environments);外部调用 5 个(new, from, assert!, http_server, panic!)。
tests::unknown_explicit_environment_is_rejected206–221 ↗
fn unknown_explicit_environment_is_rejected()
作用:验证如果配置里明确写了一个不存在的远端环境 ID,系统会拒绝,而不是随便找个默认环境代替。
数据流:测试创建没有环境的管理器,再创建环境 ID 为 remote 的 stdio 服务器配置 → 调用环境解析函数 → 预期返回错误,并检查错误说明这个服务器引用了未知环境 ID。
调用关系:它保护 McpRuntimeContext::resolve_server_environment 的错误处理逻辑。这样用户配置写错远端环境名时,会在启动前得到明确提示。
调用图:调用 2 个内部函数(new, without_environments);外部调用 5 个(new, from, assert_eq!, stdio_server, panic!)。
tests::explicit_remote_stdio_and_http_accept_named_environment224–251 ↗
async fn explicit_remote_stdio_and_http_accept_named_environment()
作用:验证当远端环境确实存在时,远端 stdio 和远端 HTTP 服务器都能绑定到这个命名环境。也顺带确认远端 stdio 只要给了绝对工作目录就能通过。
数据流:测试先创建一个带远端地址的测试环境管理器,再创建运行时上下文 → 为 stdio 服务器补上临时目录这个绝对路径,同时创建 HTTP 服务器配置 → 分别调用环境解析函数 → 两次都预期成功,并且都拿到 Some(environment)。
调用关系:它覆盖 McpRuntimeContext::resolve_server_environment 的成功路径,也会间接走到 ensure_remote_stdio_cwd 的通过分支。它说明远端环境不是被禁止,而是必须配置得足够明确。
调用图:调用 2 个内部函数(new, create_for_tests);外部调用 8 个(new, from, assert!, http_server, stdio_server, panic!, temp_dir, unreachable!)。
tests::local_stdio_accepts_local_environment_when_available254–267 ↗
async fn local_stdio_accepts_local_environment_when_available()
作用:验证只要本地运行环境存在,本地 stdio MCP 服务器就能正常解析成功。
数据流:测试创建一个带默认本地环境的测试环境管理器,再创建运行时上下文和本地 stdio 配置 → 调用环境解析函数 → 预期成功,并且返回一个实际环境对象。
调用关系:它和 tests::local_stdio_requires_local_stdio_availability 对应,分别证明同一条规则的失败和成功两面。被测核心仍然是 McpRuntimeContext::resolve_server_environment。
调用图:调用 2 个内部函数(new, default_for_tests);外部调用 5 个(new, from, assert!, stdio_server, panic!)。
tests::remote_stdio_requires_absolute_cwd270–295 ↗
async fn remote_stdio_requires_absolute_cwd()
作用:验证远端 stdio MCP 服务器不能使用相对工作目录。这样可以避免远端机器上“relative”到底相对哪里这种不确定问题。
数据流:测试创建一个可用远端环境,再创建远端 stdio 服务器配置,并故意把 cwd 设成 relative → 调用环境解析函数 → 预期失败,并检查错误文字指出必须使用绝对路径且展示收到的相对路径。
调用关系:它重点覆盖 ensure_remote_stdio_cwd 的失败分支,不过入口仍是 McpRuntimeContext::resolve_server_environment。这个测试保证目录安全规则不会被以后改代码时误删。
调用图:调用 2 个内部函数(new, create_for_tests);外部调用 6 个(new, from, assert_eq!, stdio_server, panic!, unreachable!)。
rmcp-client/src/stdio_server_launcher.rs源码 ↗
MCP 服务器有些不是网络服务,而是一个命令行程序:客户端往它的 stdin(标准输入)写请求,它从 stdout(标准输出)回消息。这个文件就像“叫车平台”:先根据配置准备命令、参数、环境变量和工作目录,再决定车从本地派,还是让远端 executor 派。启动成功后,它统一返回 StdioServerTransport,让 rmcp 继续收发 JSON-RPC 消息;JSON-RPC 可以简单理解成一行行 JSON 格式的请求和回复。文件还很重视收尾:关闭通道或对象被丢弃时,会尽量终止背后的进程,避免 MCP 服务器变成没人管的“孤儿进程”。本地启动会直接接管子进程管道,并把 stderr 里的报错日志记录下来;远端启动则会把命令、环境变量等转成 executor API 能接受的 UTF-8 字符串,并用更谨慎的环境变量策略,避免把不该泄露的变量传过去。
StdioServerTransport::send106–117 ↗
fn send(
&mut self,
item: TxJsonRpcMessage<RoleClient>,
) -> impl Future<Output = std::result::Result<(), Self::Error>> + Send + 'static
作用:把客户端要发给 MCP 服务器的一条消息写进当前通信通道。调用者不用知道背后是本地子进程,还是远端 executor 进程。
数据流:进去的是一条 rmcp 要发送的 JSON-RPC 消息 → 函数查看内部通道类型,如果是本地就交给本地子进程管道,如果是远端就交给 executor 适配通道 → 出来的是发送成功或 io 错误,通道本身继续可用。
调用关系:它是 StdioServerTransport 对 rmcp Transport 接口的实现。rmcp 在需要给 MCP 服务器发 initialize、tools/list 等请求时会走到这里;这个函数只做分流,不改变消息含义。
StdioServerTransport::receive119–127 ↗
fn receive(&mut self) -> impl Future<Output = Option<RxJsonRpcMessage<RoleClient>>> + Send
作用:从 MCP 服务器那边等下一条返回消息或通知。它把本地和远端两种进程来源伪装成同一种“收消息”的方式。
数据流:进去的是当前 transport 的内部状态 → 函数按内部类型去对应通道读取下一条 JSON-RPC 消息 → 出来是收到的消息,或者通道结束时返回空。
调用关系:它也是 rmcp Transport 接口的一部分。rmcp 的客户端主循环会反复用它读服务器输出;远端情况下,真正的字节事件已经由 ExecutorProcessTransport 转回 rmcp 期待的消息流。
StdioServerTransport::close129–135 ↗
async fn close(&mut self) -> std::result::Result<(), Self::Error>
作用:关闭和 MCP 服务器的连接,并先确保背后的服务器进程被终止。这样不会只关了管道,却留下程序还在后台跑。
数据流:进去的是一个正在使用或准备关闭的 transport → 先调用进程句柄的 terminate 结束本地或远端进程,再关闭对应的底层通道 → 出来是成功,或某一步产生的 io 错误。
调用关系:rmcp 或上层客户端在结束会话时会调用它。它把清理工作先交给 StdioServerProcessHandle::terminate,再把关闭动作交给本地 TokioChildProcess 或远端 ExecutorProcessTransport。
调用图:调用 1 个内部函数(terminate)。
StdioServerTransport::process_handle139–141 ↗
fn process_handle(&self) -> StdioServerProcessHandle
作用:取出一份服务器进程的句柄,方便别的代码在 transport 之外也能主动终止这个进程。
数据流:进去的是当前 transport → 函数复制一份共享的进程句柄引用 → 出来的是新的 StdioServerProcessHandle;它没有启动或关闭任何进程。
调用关系:这是给 crate 内部其他模块用的辅助入口。它通过 clone 复制的是共享句柄,不是复制进程本身,所以多个地方可以指向同一个待清理的服务器进程。
调用图:外部调用 1 个(clone)。
StdioServerCommand::new147–161 ↗
fn new(
program: OsString,
args: Vec<OsString>,
env: Option<HashMap<OsString, OsString>>,
env_vars: Vec<McpServerEnvVar>,
cwd: Option<PathBuf>,
) -> Self
作用:把启动 MCP 服务器需要的命令信息打包成一个对象。这样后面不管本地启动还是远端启动,都拿同一份“启动说明书”。
数据流:进去的是程序名、参数、环境变量、配置里的特殊环境变量列表、工作目录 → 函数原样放进 StdioServerCommand → 出来是一个可传给 launcher 的命令对象。
调用关系:它由 new_stdio_client 创建 stdio 客户端时调用。之后 LocalStdioServerLauncher 或 ExecutorStdioServerLauncher 会拆开这个对象,按自己的方式启动进程。
调用图:被 1 处调用(new_stdio_client)。
LocalStdioServerLauncher::new181–183 ↗
fn new(fallback_cwd: PathBuf) -> Self
作用:创建一个“本地启动器”,用于把 MCP 服务器作为当前程序的子进程启动。传入的 fallback_cwd 是配置没写工作目录时的备用目录。
数据流:进去的是一个备用工作目录路径 → 函数保存到 LocalStdioServerLauncher 里 → 出来是可重复使用的本地启动器。
调用关系:make_rmcp_client 会用它创建本地 MCP 客户端;相关测试也用它确认进程组清理、关闭时终止服务器、资源读写等行为。
调用图:被 4 处调用(make_rmcp_client, drop_kills_wrapper_process_group, shutdown_kills_initialized_stdio_server_with_in_flight_operation, rmcp_client_can_list_and_read_resources)。
LocalStdioServerLauncher::launch187–193 ↗
fn launch(
&self,
command: StdioServerCommand,
) -> BoxFuture<'static, io::Result<StdioServerTransport>>
作用:按统一的 StdioServerLauncher 接口启动一个本地 MCP stdio 服务器。它返回异步任务,让调用者可以等待启动结果。
数据流:进去的是打包好的 StdioServerCommand → 函数复制备用工作目录,并把真正启动工作放进异步块 → 出来是一个 future,执行后会得到 StdioServerTransport 或 io 错误。
调用关系:这是上层通过 trait 调用本地启动器时进入的函数。它不自己铺开细节,而是把实际工作交给 LocalStdioServerLauncher::launch_server。
调用图:外部调用 2 个(clone, launch_server)。
LocalStdioServerLauncher::launch_server237–296 ↗
fn launch_server(
command: StdioServerCommand,
fallback_cwd: PathBuf,
) -> io::Result<StdioServerTransport>
作用:真正把 MCP 服务器命令作为本机子进程启动,并把它的 stdin/stdout 包成 rmcp 能用的 transport。
数据流:进去的是命令对象和备用工作目录 → 函数合成环境变量、确定工作目录、解析程序路径、配置 stdin/stdout/stderr 管道并启动子进程;同时后台读取 stderr 写日志 → 出来是包含本地 transport 和进程句柄的 StdioServerTransport,失败则返回 io 错误。
调用关系:它由 LocalStdioServerLauncher::launch 调用。它会用 program_resolver::resolve 找到可执行程序,用 create_env_for_mcp_server 生成环境变量,用 StdioServerProcessHandle::local 建立清理句柄,并借助 TokioChildProcess 建立 rmcp 通道。
调用图:调用 3 个内部函数(resolve, local, create_env_for_mcp_server);外部调用 10 个(new, piped, builder, new, info!, kill_on_drop, process_group, Local, spawn, warn!)。
LocalProcessTerminator::new300–316 ↗
fn new(process_group_id: u32) -> Self
作用:根据系统平台保存一个之后能用来杀掉本地进程的标识。Unix 上主要保存进程组 ID,Windows 上保存进程 ID。
数据流:进去的是一个数字形式的进程或进程组标识 → 函数按当前操作系统放进对应字段;不支持的平台则忽略它 → 出来是 LocalProcessTerminator。
调用关系:本地服务器启动成功后,launch_server 会用子进程 ID 创建它。之后 StdioServerProcessHandle::terminate 或 drop 清理时会靠它真正发出终止动作。
LocalProcessTerminator::terminate352–352 ↗
fn terminate(&self)
作用:尽力结束本地 MCP 服务器进程,以及它可能拉起来的子进程。它是防止后台残留进程的最后保险。
数据流:进去的是之前保存的进程标识 → Unix 上先温和终止整个进程组,等一小段时间还在就强杀;Windows 上调用 taskkill 强制杀进程树;其他平台什么也不做 → 出来没有返回值,但可能记录警告日志。
调用关系:它被 StdioServerProcessHandle::terminate 和 StdioServerProcessHandleInner::drop 间接使用。Unix 路径会调用 terminate_process_group,必要时在线程里延迟调用 kill_process_group。
调用图:调用 1 个内部函数(terminate_process_group);外部调用 4 个(null, new, spawn, warn!)。
StdioServerProcessHandle::local356–364 ↗
fn local(program_name: String, terminator: Option<LocalProcessTerminator>) -> Self
作用:为本地启动的 MCP 服务器创建一个可共享的进程句柄。这个句柄知道程序名,也知道怎样终止它。
数据流:进去的是程序名和可选的 LocalProcessTerminator → 函数把这些放进 Arc 共享对象,并把 terminated 标记设为 false → 出来是 StdioServerProcessHandle。
调用关系:它由 LocalStdioServerLauncher::launch_server 在本地子进程启动后调用。后续 close 或对象释放时,会通过这个句柄清理本地进程。
调用图:被 1 处调用(launch_server);外部调用 3 个(new, new, Local)。
StdioServerProcessHandle::executor366–374 ↗
fn executor(program_name: String, process: Arc<dyn ExecProcess>) -> Self
作用:为 executor 启动的远端 MCP 服务器创建一个可共享的进程句柄。它保存的是 executor 返回的进程控制对象。
数据流:进去的是程序名和一个 ExecProcess 引用 → 函数包装成共享句柄,并标记尚未终止 → 出来是 StdioServerProcessHandle。
调用关系:它由 ExecutorStdioServerLauncher::launch_server 在远端进程启动成功后调用。之后 terminate 会通过 ExecProcess 的异步接口请求远端结束进程。
调用图:被 1 处调用(launch_server);外部调用 3 个(new, new, Executor)。
StdioServerProcessHandle::terminate376–395 ↗
async fn terminate(&self) -> io::Result<()>
作用:主动终止 MCP 服务器进程,而且保证同一个句柄不会重复终止多次。这个“只做一次”的保护能避免重复杀进程造成混乱。
数据流:进去的是进程句柄 → 函数先用原子布尔值检查是否已经终止;本地进程就调用 LocalProcessTerminator,远端进程就 await executor 的 terminate → 出来是成功或 io 错误;如果远端终止失败,会把已终止标记改回 false,允许以后重试。
调用关系:StdioServerTransport::close 会调用它。它是本地清理和远端清理的统一入口,具体动作再分派给 LocalProcessTerminator 或 ExecProcess。
调用图:被 1 处调用(close);外部调用 1 个(other)。
StdioServerProcessHandleInner::drop399–429 ↗
fn drop(&mut self)
作用:当最后一份进程句柄被丢掉时,自动尝试终止 MCP 服务器。它相当于“忘了手动关也帮你收尾”的保险丝。
数据流:进去的是即将释放的内部句柄 → 函数先检查是否已经终止;本地进程直接调用 terminator,远端进程则尝试拿当前 Tokio 异步运行时并启动一个后台任务去 terminate → 出来没有返回值,但失败时会写警告日志。
调用关系:它由 Rust 的 Drop 机制自动触发,不需要手动调用。它和 StdioServerProcessHandle::terminate 配合:手动 close 是正常路径,drop 是兜底路径。
ExecutorStdioServerLauncher::new446–448 ↗
fn new(exec_backend: Arc<dyn ExecBackend>) -> Self
作用:创建一个通过 executor API 启动 MCP 服务器的启动器。executor 可以理解成另一个负责实际运行命令的进程或服务。
数据流:进去的是一个 ExecBackend,共享引用形式保存 → 函数放进 ExecutorStdioServerLauncher → 出来是可用于远端启动 stdio 服务器的 launcher。
调用关系:make_rmcp_client 在需要远端或 executor 模式时会调用它。后续 launch 会用这个 exec_backend 发送启动请求。
调用图:被 1 处调用(make_rmcp_client)。
ExecutorStdioServerLauncher::launch452–458 ↗
fn launch(
&self,
command: StdioServerCommand,
) -> BoxFuture<'static, io::Result<StdioServerTransport>>
作用:按统一接口启动一个由 executor 托管的 MCP stdio 服务器。它把远端启动包装成异步任务返回。
数据流:进去的是 StdioServerCommand → 函数复制 exec_backend,并把启动动作放进异步块 → 出来是 future,执行后得到 StdioServerTransport 或 io 错误。
调用关系:这是上层通过 StdioServerLauncher trait 调用远端启动器时的入口。它把具体启动过程交给 ExecutorStdioServerLauncher::launch_server。
调用图:外部调用 2 个(clone, launch_server)。
ExecutorStdioServerLauncher::launch_server466–519 ↗
async fn launch_server(
command: StdioServerCommand,
exec_backend: Arc<dyn ExecBackend>,
) -> io::Result<StdioServerTransport>
作用:真正通过 executor API 启动远端 MCP stdio 服务器,并把远端进程的输入输出适配成 rmcp 能用的 transport。
数据流:进去的是命令对象和 exec_backend → 函数要求必须有明确工作目录,准备环境变量覆盖层,检查命令、参数、环境变量都能转成 UTF-8,生成进程 ID 和远端路径 URI,然后调用 exec_backend.start 启动进程 → 出来是带 ExecutorProcessTransport 和进程句柄的 StdioServerTransport。
调用关系:它由 ExecutorStdioServerLauncher::launch 调用。它会用 process_api_argv、process_api_env 做格式转换,用 remote_env_policy 设置远端环境继承规则,用 StdioServerProcessHandle::executor 保存清理能力。
调用图:调用 6 个内部函数(new, next_process_id, executor, create_env_overlay_for_remote_mcp_server, remote_mcp_env_var_names, from_path);外部调用 6 个(clone, process_api_argv, process_api_env, remote_env_policy, other, Executor)。
ExecutorStdioServerLauncher::process_api_argv521–534 ↗
fn process_api_argv(program: &OsString, args: &[OsString]) -> Result<Vec<String>>
作用:把本地形式的程序名和参数转换成 executor API 要求的字符串数组。远端协议只能安全传 UTF-8 字符串,所以这里会提前检查。
数据流:进去的是 OsString 程序名和 OsString 参数列表 → 函数先放入程序名,再逐个转换参数;任何一个不是合法 Unicode 就报错 → 出来是 Vec<String> 形式的 argv。
调用关系:ExecutorStdioServerLauncher::launch_server 在发送远端启动请求前调用它。它内部把每个值交给 os_string_to_process_api_string 做单项检查。
调用图:外部调用 4 个(clone, len, os_string_to_process_api_string, with_capacity)。
ExecutorStdioServerLauncher::process_api_env536–545 ↗
fn process_api_env(env: HashMap<OsString, OsString>) -> Result<HashMap<String, String>>
作用:把环境变量表转换成 executor API 能发送的普通字符串表。它会同时检查变量名和值是不是合法 Unicode。
数据流:进去的是 HashMap<OsString, OsString> 环境变量 → 函数逐项把 key 和 value 转成 String,遇到非 Unicode 就返回错误 → 出来是 HashMap<String, String>。
调用关系:ExecutorStdioServerLauncher::launch_server 在构造 ExecParams 前调用它。它和 process_api_argv 一样,负责把操作系统字符串挡在远端协议边界之外。
ExecutorStdioServerLauncher::os_string_to_process_api_string547–551 ↗
fn os_string_to_process_api_string(value: OsString, label: &str) -> Result<String>
作用:把一个操作系统字符串转成普通 String,并在失败时报出是哪类字段不合法。这里的“不合法”主要指不是有效 Unicode。
数据流:进去的是一个 OsString 和一个说明标签,比如 command 或 argument → 函数尝试 into_string;成功就返回 String,失败就生成带标签的错误 → 出来是字符串或错误。
调用关系:它被 process_api_argv 和 process_api_env 用作最小的转换工具。这样所有远端 API 字符串限制都集中在一个地方处理,错误信息也一致。
调用图:外部调用 1 个(into_string)。
ExecutorStdioServerLauncher::remote_env_policy553–578 ↗
fn remote_env_policy(remote_env_vars: &[String]) -> ExecEnvPolicy
作用:生成远端进程继承环境变量的规则,既让需要的远端变量可用,又尽量不把多余变量带进去。环境变量常常包含令牌和密钥,所以这里很关键。
数据流:进去的是需要从远端环境读取的变量名列表 → 如果列表为空,就只继承核心环境;如果不为空,就允许从远端全集中取值,但再用 include_only 限制最终只留下默认必要变量和点名变量 → 出来是 ExecEnvPolicy。
调用关系:ExecutorStdioServerLauncher::launch_server 构造 ExecParams 时会用它。三个测试函数也直接调用它,验证没有远端变量、有远端变量、以及过滤未请求变量这几种情况。
调用图:被 3 处调用(remote_env_policy_effectively_filters_unrequested_vars, remote_env_policy_includes_remote_source_vars_without_full_env, remote_env_policy_uses_core_env_without_remote_source_vars);外部调用 2 个(new, new)。
tests::remote_env_policy_uses_core_env_without_remote_source_vars589–594 ↗
fn remote_env_policy_uses_core_env_without_remote_source_vars()
作用:测试没有请求任何远端来源环境变量时,策略应当只使用核心环境。这样默认情况下不会把远端完整环境暴露给 MCP 服务器。
数据流:进去的是空的远端变量列表 → 调用 remote_env_policy 生成策略 → 检查 inherit 是 Core,并且 include_only 为空。
调用关系:这是 remote_env_policy 的基础安全测试。它确保普通远端 MCP 启动不会意外扩大环境变量继承范围。
调用图:调用 1 个内部函数(remote_env_policy);外部调用 2 个(assert!, assert_eq!)。
tests::remote_env_policy_includes_remote_source_vars_without_full_env597–611 ↗
fn remote_env_policy_includes_remote_source_vars_without_full_env()
作用:测试当用户点名要一个远端变量时,这个变量会被允许,同时默认必要环境变量也还在。重点是允许需要的,不等于放开所有的。
数据流:进去的是 REMOTE_TOKEN 这个变量名 → 调用 remote_env_policy → 检查策略会从 All 中取源,但 include_only 里只包含点名变量和默认必要变量。
调用关系:它验证 remote_env_policy 在有远端变量需求时的折中设计:先让 executor 有机会读到变量,再用 include_only 把最终子进程环境收窄。
调用图:调用 1 个内部函数(remote_env_policy);外部调用 2 个(assert!, assert_eq!)。
tests::remote_env_policy_effectively_filters_unrequested_vars614–653 ↗
fn remote_env_policy_effectively_filters_unrequested_vars()
作用:测试生成的远端环境策略真的能挡住没请求的秘密变量。它用一个模拟环境证明 UNREQUESTED_SECRET 不会传给子进程。
数据流:进去的是包含 PATH、REMOTE_TOKEN、UNREQUESTED_SECRET 的模拟远端环境 → 先生成 exec 策略,再转成 shell_environment 使用的策略并创建最终环境 → 检查 PATH 和 REMOTE_TOKEN 留下,UNREQUESTED_SECRET 被过滤掉。
调用关系:它把 remote_env_policy 放进更接近真实环境生成的流程里验证。通过调用 create_env_from_vars,它确认策略不是表面正确,而是实际过滤结果正确。
调用图:调用 2 个内部函数(create_env_from_vars, remote_env_policy);外部调用 2 个(assert!, assert_eq!)。
codex-mcp/src/rmcp_client.rs源码 ↗
这个文件解决的是“怎么安全、稳定地接上一台 MCP 工具服务器”的问题。没有它,Codex 不知道该用命令行还是 HTTP 去连服务器,也不知道服务器启动好了没有、有哪些工具、工具要不要过滤、启动慢时能不能先用缓存。它的大致流程像开店前盘点货架:先检查服务器名字是否合法,再按配置创建连接;然后向服务器打招呼并交换能力;接着拉取工具列表,清理不可信的连接器信息,给 Codex Apps 这类特殊服务器做名字规范化和缓存;最后把客户端、服务器信息、工具列表、超时时间等打包成 ManagedClient。AsyncManagedClient 则像“后台开店助手”:启动还没完成时,可以先拿旧缓存给用户看;启动失败或取消时,也能给出明确状态。文件里还特别注意了安全边界:普通 MCP 服务器不能随便冒充带连接器元数据的 Codex Apps 工具。
ManagedClient::listed_tools101–124 ↗
fn listed_tools(&self) -> Vec<ToolInfo>
作用:返回这个已经启动好的客户端当前可用的工具列表。如果是 Codex Apps 服务器,它会优先尝试用缓存,避免每次都重新向服务器要一遍工具清单。
数据流:进去的是 ManagedClient 自己保存的工具、过滤规则和可选缓存信息 → 它先看缓存里有没有 Codex Apps 工具快照,有就记录“缓存命中”的耗时并按规则过滤;没有就记录“缓存未命中”并使用启动时拿到的工具列表 → 出来的是一份已经过滤好的 ToolInfo 列表,不会改动客户端本身。
调用关系:这是启动完成后的读工具入口之一。AsyncManagedClient::listed_tools 在确认真实客户端可用后会调用它;它自己会把缓存读取交给 load_cached_codex_apps_tools,把耗时上报交给 emit_duration,把工具筛选交给 filter_tools。
调用图:调用 3 个内部函数(load_cached_codex_apps_tools, emit_duration, filter_tools);外部调用 1 个(now)。
AsyncManagedClient::new141–237 ↗
fn new(
server_name: String,
server: EffectiveMcpServer,
store_mode: OAuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyringBackendKind,
cancel_token: Canc
作用:创建一个“正在后台启动”的 MCP 客户端对象。它不会要求调用者一直等到服务器完全连好,而是把启动过程放进一个可共享的异步任务里。
数据流:进去的是服务器名、服务器配置、认证保存方式、取消令牌、事件通道、缓存上下文、运行环境等启动材料 → 它先生成工具过滤规则,读取可能存在的启动缓存,再创建一个异步流程:校验服务器名、创建底层 RMCP 客户端、初始化服务器并拉工具 → 出来的是 AsyncManagedClient,里面既有未来会完成的真实客户端,也有启动期间可用的缓存快照和取消开关。
调用关系:这是更上层连接管理器创建单个服务器连接时会用到的构造器。它把具体建连接的活交给 make_rmcp_client,把初始化和工具拉取交给 start_server_task,把名字检查交给 validate_mcp_server_name;如果有缓存,还会主动在后台推动启动任务继续跑。
调用图:调用 6 个内部函数(load_startup_cached_codex_apps_server_info, load_startup_cached_codex_apps_tools_snapshot, make_rmcp_client, start_server_task, validate_mcp_server_name, configured_config);被 1 处调用(new);外部调用 6 个(clone, new, new, clone, clone, spawn)。
AsyncManagedClient::client239–241 ↗
async fn client(&self) -> Result<ManagedClient, StartupOutcomeError>
作用:等待后台启动任务结束,并拿到真正可用的 ManagedClient。调用者想执行需要真实连接的操作时会用它。
数据流:进去的是 AsyncManagedClient 自己保存的共享异步任务 → 它克隆这个共享任务句柄并等待结果 → 出来的是启动成功后的 ManagedClient,或者启动失败、被取消的错误。
调用关系:这是 AsyncManagedClient 内部和外部访问真实客户端的统一入口。shutdown 会用它判断是否已经连上;AsyncManagedClient::listed_tools 在不能用缓存时也会通过它拿真实工具列表。
调用图:被 2 处调用(listed_tools, shutdown);外部调用 1 个(clone)。
AsyncManagedClient::shutdown243–252 ↗
async fn shutdown(&self)
作用:关闭这个 MCP 客户端。它会先通知后台启动或运行流程取消,然后如果客户端已经成功建好,就让底层连接真正关掉。
数据流:进去的是 AsyncManagedClient 自己的取消令牌和共享客户端任务 → 它先发出取消信号,再等待 client() 的结果;如果客户端已启动,就调用底层 shutdown;如果只是被取消就静默结束;如果启动失败则写一条警告 → 出来没有业务结果,主要副作用是取消启动和关闭连接。
调用关系:这是程序收尾或移除服务器连接时走的清理路径。它依赖 AsyncManagedClient::client 获取真实客户端,并把实际断开连接的工作交给 RmcpClient 的 shutdown。
AsyncManagedClient::cached_tool_info_snapshot_while_initializing254–259 ↗
fn cached_tool_info_snapshot_while_initializing(&self) -> Option<Vec<ToolInfo>>
作用:在服务器还没启动完成时,取出一份旧的工具缓存快照。这样用户不用因为服务器正在连接就完全看不到工具。
数据流:进去的是启动完成标记和可选缓存工具列表 → 它检查原子布尔值,原子布尔值可以理解成多任务之间安全共享的“是否完成”小旗子;如果还没完成就克隆缓存返回,完成后就不再返回旧缓存 → 出来是可选的工具列表。
调用关系:它只服务于 AsyncManagedClient::listed_tools。后者会先问它有没有启动期间可展示的旧工具;如果没有,再去等真实客户端。
调用图:被 1 处调用(listed_tools)。
AsyncManagedClient::listed_tools261–324 ↗
async fn listed_tools(&self) -> Option<Vec<ToolInfo>>
作用:给外部返回当前可展示的工具列表。它会在“启动中”和“已启动”两种状态之间自动选择:能用新数据就用新数据,暂时不能就用缓存兜底。
数据流:进去的是 AsyncManagedClient 的缓存、真实客户端任务和插件来源信息 → 它先尝试拿启动中的缓存;拿不到就等待真实客户端并调用 ManagedClient::listed_tools;如果失败则退回旧缓存。之后它给每个工具补充插件显示名,并把插件来源写进工具描述;Codex Apps 工具还会调整输入 schema,让模型能看到合适的参数结构 → 出来的是可选的、带注释的工具列表。
调用关系:这是上层展示或选择工具时最常用的读取入口。它先调用 cached_tool_info_snapshot_while_initializing 做快速兜底,再必要时调用 client 等待真实连接;拿到工具后还会结合 ToolPluginProvenance 给工具补上“来自哪个插件”的说明。
调用图:调用 2 个内部函数(cached_tool_info_snapshot_while_initializing, client)。
StartupOutcomeError::from338–342 ↗
fn from(error: anyhow::Error) -> Self
作用:把通用错误转换成这个文件专用的启动错误。这样异步启动结果可以被克隆、共享和统一展示。
数据流:进去的是 anyhow::Error,也就是一种可包住各种失败原因的通用错误 → 它把错误内容转成字符串 → 出来的是 StartupOutcomeError::Failed,里面保存可读的错误文字。
调用关系:它被 make_rmcp_client、start_server_task 等启动流程用来统一错误类型。因为共享异步任务要求错误可复制,这里不保存原始错误对象,只保存文字。
调用图:外部调用 1 个(to_string)。
list_tools_for_client_uncached345–408 ↗
async fn list_tools_for_client_uncached(
server_name: &str,
client: &Arc<RmcpClient>,
timeout: Option<Duration>,
server_instructions: Option<&str>,
) -> Result<Vec<ToolInfo>>
作用:直接向 MCP 服务器询问工具列表,不走缓存。它适合首次启动、强制刷新缓存,或者需要确认服务器最新工具时使用。
数据流:进去的是服务器名、底层 RMCP 客户端、超时时间和服务器说明 → 它请求服务器列出工具;逐个工具清理或保留连接器元数据,生成 Codex 内部使用的调用名、命名空间和说明;如果是 Codex Apps 服务器,还会过滤掉不允许暴露的工具 → 出来是一组标准化后的 ToolInfo。
调用关系:start_server_task 在初始化服务器后会调用它来拿第一份真实工具清单;hard_refresh_codex_apps_tools_cache 也会用它刷新缓存。它内部会调用 sanitize_tool_connector_metadata 做安全清理,并在 Codex Apps 场景下调用 filter_disallowed_codex_apps_tools。
调用图:调用 1 个内部函数(filter_disallowed_codex_apps_tools);被 2 处调用(hard_refresh_codex_apps_tools_cache, start_server_task)。
sanitize_tool_connector_metadata410–423 ↗
fn sanitize_tool_connector_metadata(
server_name: &str,
tool: &mut RmcpTool,
connector_id: Option<String>,
connector_name: Option<String>,
connector_description: Option<String>,
)
作用:决定工具里的“连接器信息”能不能被信任。连接器信息比如 Gmail、Slack 这类来源标识,普通服务器不能随便伪造。
数据流:进去的是服务器名、可修改的工具定义,以及服务器返回的 connector_id、connector_name、connector_description → 如果服务器是官方 Codex Apps,就原样保留这些信息;否则把工具 meta 里不可信的连接器字段删掉,并把三个连接器返回值都改成 None → 出来的是清理后的连接器三元组,同时工具对象可能被修改。
调用关系:list_tools_for_client_uncached 在整理每个工具时会用它。测试 custom_mcp_connector_metadata_is_stripped 和 codex_apps_connector_metadata_is_preserved 专门验证它对普通服务器和 Codex Apps 服务器的不同处理。
调用图:调用 1 个内部函数(strip_untrusted_connector_meta);被 2 处调用(codex_apps_connector_metadata_is_preserved, custom_mcp_connector_metadata_is_stripped)。
strip_untrusted_connector_meta425–429 ↗
fn strip_untrusted_connector_meta(tool: &mut RmcpTool)
作用:从工具的附加信息里删除那些普通 MCP 服务器不该自称拥有的连接器字段。它是一个小的安全清扫函数。
数据流:进去的是一个可修改的 RmcpTool → 如果工具有 meta 附加字段,它会保留安全字段,删掉 connector_id、connector_name 等明确列入黑名单的键 → 出来没有单独返回值,但工具的 meta 可能已经少了一些不可信字段。
调用关系:它由 sanitize_tool_connector_metadata 调用。sanitize_tool_connector_metadata 先判断服务器是否可信,只有普通 MCP 服务器才会走到这里清理。
调用图:被 1 处调用(sanitize_tool_connector_metadata)。
is_untrusted_connector_meta_key431–433 ↗
fn is_untrusted_connector_meta_key(key: &str) -> bool
作用:判断某个 meta 字段名是不是不可信的连接器字段。它像门卫手里的黑名单检查。
数据流:进去的是一个字段名字符串 → 它检查这个名字是否在 UNTRUSTED_CONNECTOR_META_KEYS 列表里 → 出来是 true 或 false,表示该字段该不该被删除。
调用关系:它服务于 strip_untrusted_connector_meta 的 retain 过滤逻辑。虽然调用图没有单独列出闭包里的调用,但它的作用就是帮助清理工具 meta。
resolve_bearer_token435–460 ↗
fn resolve_bearer_token(
server_name: &str,
bearer_token_env_var: Option<&str>,
) -> Result<Option<String>>
作用:从环境变量里读取 HTTP Bearer token。Bearer token 可以理解成放在请求头里的“通行证”,用于访问需要认证的 MCP HTTP 服务器。
数据流:进去的是服务器名和可选的环境变量名 → 如果没有配置变量名,就返回 None;如果配置了,就读取系统环境变量,并检查它是否存在、是否为空、是否是合法 Unicode 字符串 → 出来是可选 token,或者一条说明清楚的错误。
调用关系:make_rmcp_client 在创建 Streamable HTTP 客户端时会调用它。这样认证密钥不会硬编码在配置里,而是从运行环境安全读取。
调用图:被 1 处调用(make_rmcp_client);外部调用 2 个(anyhow!, var)。
validate_mcp_server_name462–471 ↗
fn validate_mcp_server_name(server_name: &str) -> Result<()>
作用:检查 MCP 服务器名字是否只包含字母、数字、下划线和短横线。这样可以避免奇怪名字进入缓存、日志或协议字段后造成混乱。
数据流:进去的是服务器名字符串 → 它用正则表达式,也就是一种文本格式规则,检查名字是否匹配 ^[a-zA-Z0-9_-]+$ → 如果匹配就返回成功;否则返回带有规则说明的错误。
调用关系:AsyncManagedClient::new 启动后台任务时首先调用它。名字不合法时,后面的建连接和初始化都不会继续。
start_server_task473–551 ↗
async fn start_server_task(
server_name: String,
client: Arc<RmcpClient>,
params: StartServerTaskParams,
) -> Result<ManagedClient, StartupOutcomeError>
作用:完成 MCP 客户端启动的核心步骤:初始化服务器、读取工具、写缓存、套用过滤规则,最后组装成 ManagedClient。
数据流:进去的是服务器名、已创建的 RmcpClient,以及启动参数包,例如超时、工具过滤器、事件发送器、缓存上下文和客户端能力 → 它先声明 Codex 客户端能力并发送 initialize;然后检查服务器是否支持 sandbox 状态元信息;再调用 list_tools_for_client_uncached 拉工具并记录耗时;接着把服务器实现信息转成 McpServerInfo,必要时写入 Codex Apps 工具缓存,最后按过滤规则筛工具 → 出来的是可直接使用的 ManagedClient,或启动错误。
调用关系:AsyncManagedClient::new 创建底层客户端后会把后半段启动交给它。它再把工具拉取交给 list_tools_for_client_uncached,把服务器信息转换交给 mcp_server_info_from_implementation,把缓存写入交给 write_cached_codex_apps_tools_if_needed。
调用图:调用 5 个内部函数(write_cached_codex_apps_tools_if_needed, list_tools_for_client_uncached, mcp_server_info_from_implementation, emit_duration, filter_tools);被 1 处调用(new);外部调用 6 个(clone, default, new, new, now, env!)。
mcp_server_info_from_implementation553–567 ↗
fn mcp_server_info_from_implementation(server_info: Implementation) -> McpServerInfo
作用:把协议初始化结果里的服务器身份信息,转换成 Codex 自己使用的服务器信息格式。
数据流:进去的是 RMCP 协议里的 Implementation,里面有名字、标题、版本、描述、图标、网站等 → 它逐项搬到 McpServerInfo;图标会先尝试转成 JSON 值,转不了的就跳过 → 出来的是 Codex 协议层能统一使用的 McpServerInfo。
调用关系:start_server_task 在服务器 initialize 成功后调用它。这样后续缓存、展示和连接管理都不用直接依赖 RMCP 的原始类型。
调用图:被 1 处调用(start_server_task)。
make_rmcp_client579–663 ↗
async fn make_rmcp_client(
server_name: &str,
server: EffectiveMcpServer,
store_mode: OAuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyringBackendKind,
runtime_context: McpR
作用:根据服务器配置创建真正的 RMCP 客户端连接。它负责判断该用本地命令行启动服务器,还是通过 HTTP 连接远端服务器。
数据流:进去的是服务器名、有效服务器配置、OAuth 保存方式、钥匙串后端、运行环境和可选认证提供者 → 它先解析服务器运行环境,再看 transport 配置;如果是 Stdio,就准备命令、参数、环境变量和启动器,本地环境用 LocalStdioServerLauncher,非本地环境用执行器后端;如果是 StreamableHttp,就准备 HTTP 客户端、请求头、Bearer token 和认证配置 → 出来的是 RmcpClient,或者启动错误。
调用关系:AsyncManagedClient::new 的后台启动流程会先调用它拿到底层连接对象,然后再交给 start_server_task 初始化。它会调用 resolve_bearer_token 读取 HTTP token,也会依赖 runtime_context 解析本地或远端执行环境。
调用图:调用 8 个内部函数(resolve_bearer_token, local_stdio_fallback_cwd, resolve_server_environment, launch, new_stdio_client, new_streamable_http_client, new, new);被 1 处调用(new);外部调用 2 个(new, unreachable!)。
tests::tool_with_connector_meta671–693 ↗
fn tool_with_connector_meta() -> RmcpTool
作用:构造一个带很多连接器 meta 字段的假工具,专门给测试使用。它模拟服务器返回了 Gmail 这类连接器信息的情况。
数据流:进去没有外部输入 → 它创建一个名为 capture_file_upload 的 RmcpTool,并在 meta 里放入 connector_id、connector_name、connector_description、未来字段、自定义字段等 → 出来是一份测试用工具对象。
调用关系:两个测试函数都会调用它。它提供同一份“样本工具”,方便比较普通 MCP 服务器和 Codex Apps 服务器的清理结果是否不同。
tests::custom_mcp_connector_metadata_is_stripped696–729 ↗
fn custom_mcp_connector_metadata_is_stripped()
作用:测试普通 MCP 服务器返回的连接器信息会被删除。这样可以防止普通服务器伪装成受信任的 Codex Apps 连接器。
数据流:进去没有运行时输入 → 它先创建带连接器 meta 的假工具,再用普通服务器名调用 sanitize_tool_connector_metadata;随后检查返回的 connector_id、connector_name、connector_description 都是 None,并确认黑名单字段从 meta 里消失,同时普通自定义字段还保留 → 出来是测试通过或断言失败。
调用关系:这是 sanitize_tool_connector_metadata 的安全行为测试。它和 codex_apps_connector_metadata_is_preserved 成对出现,一个验证会删,一个验证该保留时不删。
调用图:调用 1 个内部函数(sanitize_tool_connector_metadata);外部调用 3 个(assert!, assert_eq!, tool_with_connector_meta)。
tests::codex_apps_connector_metadata_is_preserved732–760 ↗
fn codex_apps_connector_metadata_is_preserved()
作用:测试官方 Codex Apps 服务器的连接器信息会被保留。因为这些信息对展示工具来源和命名空间很重要。
数据流:进去没有运行时输入 → 它创建同样的假工具,但用 CODEX_APPS_MCP_SERVER_NAME 调用 sanitize_tool_connector_metadata;然后检查 connector_id、connector_name、connector_description 原样返回,并确认各种连接器 meta 字段仍在工具里 → 出来是测试通过或断言失败。
调用关系:这是 sanitize_tool_connector_metadata 的信任例外测试。它确保为 Codex Apps 设计的连接器信息不会被普通安全清理误删。
调用图:调用 1 个内部函数(sanitize_tool_connector_metadata);外部调用 3 个(assert!, assert_eq!, tool_with_connector_meta)。
mcp-server/src/lib.rs源码 ↗
这个文件像一个“总控台”。MCP 可以理解成一种让工具和客户端互相说话的协议;这里的说话方式是 JSON-RPC,也就是每条消息都是一段 JSON,里面说明“我要请求什么”或“我通知你什么”。run_main 启动时先读命令行里的配置覆盖项,加载完整配置,再设置遥测,也就是日志、追踪、指标这些用来观察程序运行情况的数据。然后它准备执行环境,创建几条通道:一条从标准输入接收外部发来的消息,一条把处理结果送到标准输出。接着它同时启动三个异步任务:读输入、处理消息、写输出。这样程序不会因为等输入或等输出而卡住。文件底部还有两个测试,确认默认会开启分析数据,并确认遥测提供者能同时支持日志、追踪和指标。
run_main59–203 ↗
async fn run_main(
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
strict_config: bool,
) -> IoResult<()>
作用:这是 MCP 服务器真正跑起来的地方。它负责把配置读好,把运行环境搭好,然后进入“读消息、处理消息、写回复”的循环。
数据流:进去的是程序启动时拿到的路径信息、命令行配置覆盖项,以及是否严格检查配置的开关。它先把这些变成正式配置,初始化遥测和状态数据库,再创建执行环境和几条消息通道。之后它从标准输入读入 JSON-RPC 消息,交给 MessageProcessor 处理,再把处理后的回复转成 JSON 写到标准输出。最后当输入结束、通道关闭时,几个后台任务依次退出,函数返回成功或输入输出错误。
调用关系:它是整个文件的核心,也是外部启动 MCP 服务器时会调用的总入口。它会调用配置构建、安装 ID 解析、遥测初始化、执行环境创建等外部组件,然后把真正理解请求的工作交给 MessageProcessor;MessageProcessor 产出的消息再交给输出任务写回客户端。
调用图:调用 9 个内部函数(new, new, build_provider, install_sqlite_telemetry, record_process_start, from_codex_home, from_optional_paths, set_default_client_residency_requirement, parse_overrides);外部调用 17 个(new, new, from_default_env, init_state_db, resolve_installation_id, default, debug!, env!, error!, info! (+7 more))。
tests::mcp_server_defaults_analytics_to_enabled215–217 ↗
fn mcp_server_defaults_analytics_to_enabled()
作用:这个测试确认服务器的默认行为是开启分析数据。这样开发者以后改代码时,如果不小心把默认值改掉,测试会立刻提醒。
数据流:它没有复杂输入,只读取文件里的 DEFAULT_ANALYTICS_ENABLED 常量。然后把这个值和 true 比较。结果是测试通过或失败,不会改动运行时数据。
调用关系:它只在测试时运行,不参与真正的服务器流程。它用断言检查 run_main 里传给遥测初始化的默认分析开关是否仍然符合预期。
调用图:外部调用 1 个(assert_eq!)。
tests::mcp_server_builds_otel_provider_with_logs_traces_and_metrics220–254 ↗
async fn mcp_server_builds_otel_provider_with_logs_traces_and_metrics() -> anyhow::Result<()>
作用:这个测试确认遥测组件能按配置同时创建日志、追踪和指标三类输出。简单说,就是确认程序的“运行观察系统”没有缺胳膊少腿。
数据流:进去的是测试临时创建的 Codex 主目录和一份临时配置。测试把日志、追踪、指标的导出方式都设成 OTLP gRPC,也就是一种把遥测数据发到外部收集器的网络格式。然后它调用 build_provider 创建遥测提供者,检查里面确实有日志导出器、追踪导出器和指标导出器,最后关闭这个提供者。
调用关系:它只在测试环境里运行,重点覆盖 run_main 启动阶段会用到的 build_provider。这样可以保证服务器真实启动时,相关遥测配置不会只初始化一部分。
调用图:调用 1 个内部函数(build_provider);外部调用 4 个(new, new, assert!, default)。
协议桥接工具
这些文件提供小型专用辅助进程,用于桥接本地协议或代理单一 API 表面。
stdio-to-uds/src/lib.rs源码 ↗
这个文件解决的是“终端怎么和本地后台服务说话”的问题。很多后台服务不直接读键盘、写屏幕,而是开一个 Unix Domain Socket,让别的程序连进来。这里的 run 函数先按给定路径连接这个 socket,然后把这条连接拆成读和写两半:一半专门读 socket 的回复并写到标准输出,也就是屏幕;另一半专门读标准输入,也就是用户或管道送来的内容,再写进 socket。两边同时工作,像电话两端同时能听也能说。它还处理了一个细节:如果对方已经先关了连接,本端关闭写入半边时可能报“没连上”,这种情况不当成真正错误,避免把正常结束误报成失败。
run12–46 ↗
async fn run(socket_path: &Path) -> anyhow::Result<()>
作用:把当前程序的标准输入和标准输出,转接到指定路径的 Unix Domain Socket。别人会用它来做一个简单的本地通信桥,让终端或脚本可以和只提供 socket 接口的服务交互。
数据流:输入是一个 socket 文件路径。它先用这个路径建立本地 socket 连接;连接成功后,把连接分成读取端和写入端。随后它同时做两件事:把标准输入里的字节原样复制到 socket,把 socket 发回来的字节原样复制到标准输出。复制结束后会刷新输出,并尽量关闭 socket 的写入端;如果过程中出错,就返回带说明的错误;全部顺利则返回成功。
调用关系:这是这个小库的核心入口。调用者在需要开始转发时调用 run;run 自己负责连接 socket,并把实际搬运字节的工作交给 Tokio 的异步工具,比如 connect、split、copy、stdin、stdout 和 try_join。try_join 让“输入到 socket”和“socket 到输出”两条搬运线一起跑,只要其中一条出大问题,整个转发就会结束并报告原因。
调用图:调用 1 个内部函数(connect);外部调用 6 个(Ok, copy, split, stdin, stdout, try_join!)。
responses-api-proxy/src/lib.rs源码 ↗
这个文件就像一个小型“传达室”:它只允许本机访问,把收到的 POST /v1/responses 请求检查一遍,然后带上真正的 Authorization 鉴权头转发到上游 API。启动时,它先从标准输入读取密钥,解析命令行参数,决定监听哪个端口;如果需要,还会把端口和进程号写进一个 JSON 文件,方便别的程序找到它。运行中,每个进来的请求都会开一个新线程处理,避免一个慢请求卡住其他请求。它会拒绝不符合路径和方法的访问,避免这个代理被当成通用转发器乱用。它还可以把请求和响应保存到目录里,方便调试。特别要注意:鉴权头不会从客户端原样转发,而是由代理替换成启动时读到的秘密值,这样能减少密钥泄露的机会。
run_main73–136 ↗
fn run_main(args: Args) -> Result<()>
作用:这是这个代理的主运行函数,负责把服务器从“还没启动”带到“正在监听并转发请求”的状态。外部程序通常只要把命令行参数交给它,它就会完成读取密钥、开端口、建 HTTP 服务、接请求这些工作。
数据流:进去的是 Args,也就是端口、上游地址、是否写 server_info、是否允许 HTTP 关闭、是否保存转储文件等设置。它先从标准输入读出鉴权头,再解析上游 URL,算出要发给上游的 Host 头;接着准备转储器、绑定本机监听端口、可选地写出启动信息文件,并创建 HTTP 客户端。之后它不断接收请求,为每个请求开一个线程;普通请求会被送去转发,GET /shutdown 在允许时会让进程退出。正常情况下它一直运行;如果服务器意外停掉,就返回错误。
调用关系:它是全文件的总指挥。启动阶段它会调用 bind_listener 找到可用端口,也会在需要时调用 write_server_info 告诉外部“我开在哪个端口”。它还调用 read_auth_header_from_stdin 取得秘密鉴权信息,并使用外部库创建 URL、HTTP 服务器和客户端。进入接请求阶段后,它把真正的单个请求处理交给 forward_request。
调用图:调用 3 个内部函数(bind_listener, read_auth_header_from_stdin, write_server_info);外部调用 9 个(new, from_str, from_listener, parse, anyhow!, builder, eprintln!, format!, spawn)。
bind_listener138–143 ↗
fn bind_listener(port: Option<u16>) -> Result<(TcpListener, SocketAddr)>
作用:这个函数负责在本机 127.0.0.1 上打开一个 TCP 监听端口。TCP 可以理解成网络连接的“电话线”,监听端口就是代理等别人拨进来的号码。
数据流:进去的是一个可选端口号;如果给了端口,它就尝试绑定这个端口,如果没给,就让操作系统自动分配一个临时空闲端口。它成功后返回两样东西:真正用于接连接的 TcpListener,以及最终绑定到的地址和端口。失败时会返回带说明的错误,比如端口已经被占用。
调用关系:它只在 run_main 启动服务器时使用。run_main 需要先拿到这个监听器,才能把它交给 tiny_http 创建 HTTP 服务器;同时 run_main 也需要它返回的实际端口,才能打印日志或写入 server_info 文件。
write_server_info145–161 ↗
fn write_server_info(path: &Path, port: u16) -> Result<()>
作用:这个函数把代理启动后的关键信息写到一个 JSON 文件里,主要是端口号和进程号。这样别的程序不用猜代理在哪个端口,也能知道是哪一个进程在跑。
数据流:进去的是目标文件路径和端口号。它会先确保父目录存在,然后组装一个包含 port 和 pid 的小对象,把它转成一行 JSON 文本,最后创建或覆盖目标文件并写进去。出来的结果是成功或失败;成功时磁盘上会多出这个信息文件。
调用关系:它由 run_main 在服务器成功绑定端口之后调用。这个顺序很重要:只有端口真的确定了,写出去的信息才可信。它依赖文件系统创建目录和文件,也依赖 JSON 序列化把结构化信息变成别的程序容易读取的文本。
调用图:被 1 处调用(run_main);外部调用 5 个(create, parent, create_dir_all, to_string, id)。
forward_request163–275 ↗
fn forward_request(
client: &Client,
auth_header: &'static str,
config: &ForwardConfig,
dump_dir: Option<&ExchangeDumper>,
mut req: Request,
) -> Result<()>
作用:这个函数处理单个进入代理的 HTTP 请求:该拒绝的拒绝,该转发的转发,并把上游返回的内容原样送回客户端。它是代理真正干活的地方。
数据流:进去的是 HTTP 客户端、启动时读到的鉴权头、上游转发配置、可选的转储器,以及一个客户端请求。它先检查请求必须正好是 POST /v1/responses;不符合就直接回 403,意思是“禁止访问”。符合时,它读完整个请求正文,必要时先保存一份请求记录;然后复制大多数请求头,但去掉客户端传来的 Authorization 和 Host,改用代理自己的秘密鉴权头和上游 Host。接着它把请求发到上游 URL,拿到上游响应后过滤掉一些由本地 HTTP 库自己管理的响应头,再把状态码、响应头和响应体转回给原客户端。如果开启转储,它还会一边转发响应体一边保存响应内容。
调用关系:它处在每个请求的处理现场,由 run_main 为每个收到的请求安排执行。它把网络外发工作交给 reqwest 客户端,把本地回包交给 tiny_http 的 Request.respond;如果启用了 ExchangeDumper,它还会把请求和响应复制给转储模块。它不会负责启动服务器,也不会决定监听端口,只专注于“这一单请求该怎么转发”。
调用图:外部调用 17 个(new, from_bytes, new, from_bytes, from_bytes, from_static, new, post, as_reader, headers (+7 more))。
网络代理运行时
这些文件编排独立的网络代理进程及其 SOCKS5 传输实现。
network-proxy/src/proxy.rs源码 ↗
这个文件把网络代理从“配置文字”变成真正能用的服务。它先按配置选端口,并尽量只绑在本机回环地址,也就是只能本机访问的地址,避免代理暴露到外网。然后它保存运行时开关,比如是否允许本地绑定、是否允许 Unix socket(一种本机进程间通信文件)、是否启用 HTTPS 拦截用的证书包。启动时,它会分别拉起 HTTP 代理和可选的 SOCKS5 代理;给子进程准备环境变量时,它会把 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 等常见变量统一改成 Codex 的代理地址。它还特别处理不同工具的怪脾气,比如 Node、Electron、npm、pip、Docker、Git SSH,以及 macOS 上的 SSH 代理命令。整体像一个交通调度员:先占好安全车道,再告诉所有车辆必须从指定入口通行。
ReservedListeners::new33–38 ↗
fn new(http: StdTcpListener, socks: Option<StdTcpListener>) -> Self
作用:把已经提前占好的 HTTP 和 SOCKS 监听口保存起来,等真正启动代理时再交出去。这样可以避免“刚选好的端口被别人抢走”。
数据流:进去的是一个 HTTP 监听器和一个可选的 SOCKS 监听器;函数把它们分别包进互斥锁(一把锁,防止两个任务同时拿同一个监听器)里;出来的是一个可共享的 ReservedListeners 对象。
调用关系:它由 ReservedListenerSet::into_reserved_listeners 调用,用在构建代理时把临时占住的端口转成运行时可取走的资源。
调用图:被 1 处调用(into_reserved_listeners);外部调用 1 个(new)。
ReservedListeners::take_http40–46 ↗
fn take_http(&self) -> Option<StdTcpListener>
作用:取出提前保留的 HTTP 监听器,而且只能取一次。取走后这里就变空,防止重复启动两个 HTTP 代理抢同一个端口。
数据流:进去的是这个保存监听器的对象;它先加锁,再把里面的 HTTP 监听器拿出来并清空原位置;出来的是可能存在的 HTTP 监听器。
调用关系:NetworkProxy::run 启动 HTTP 代理前会用它。如果拿到了监听器,就直接交给 http_proxy::run_http_proxy_with_std_listener。
ReservedListeners::take_socks48–54 ↗
fn take_socks(&self) -> Option<StdTcpListener>
作用:取出提前保留的 SOCKS5 监听器,同样只能取一次。SOCKS5 是一种更通用的代理协议,常用于非 HTTP 流量。
数据流:进去的是保存监听器的对象;它加锁后取走 SOCKS 监听器,并把内部位置清空;出来的是可选的 SOCKS 监听器。
调用关系:NetworkProxy::run 在启用 SOCKS5 时会用它。如果有预留监听器,就交给 socks5::run_socks5_with_std_listener。
ReservedListenerSet::new63–68 ↗
fn new(http_listener: StdTcpListener, socks_listener: Option<StdTcpListener>) -> Self
作用:把一组刚刚占好的 HTTP/SOCKS 监听器打包成一个小集合,方便后面统一读取地址或转交给代理。
数据流:进去的是 HTTP 监听器和可选 SOCKS 监听器;函数只是保存它们;出来的是 ReservedListenerSet。
调用关系:reserve_loopback_ephemeral_listeners 和 Windows 专用的 try_reserve_windows_managed_listeners 会用它来返回占端口的结果。
调用图:被 2 处调用(reserve_loopback_ephemeral_listeners, try_reserve_windows_managed_listeners)。
ReservedListenerSet::http_addr70–74 ↗
fn http_addr(&self) -> Result<SocketAddr>
作用:读取已经占好的 HTTP 监听器实际绑定到了哪个地址和端口。端口可能是系统自动分配的,所以必须问监听器本人。
数据流:进去的是 ReservedListenerSet;它读取 HTTP 监听器的本地地址;出来的是 SocketAddr,失败时带上清楚的错误说明。
调用关系:NetworkProxyBuilder::build 在托管模式下用它确认最终要告诉子进程的 HTTP 代理地址。
调用图:外部调用 1 个(local_addr)。
ReservedListenerSet::socks_addr76–84 ↗
fn socks_addr(&self, default_addr: SocketAddr) -> Result<SocketAddr>
作用:读取 SOCKS5 监听器实际绑定地址;如果这次没有启用 SOCKS5 监听器,就返回配置里的默认地址。
数据流:进去的是 ReservedListenerSet 和一个默认 SOCKS 地址;如果有 SOCKS 监听器就读它的地址,否则直接用默认地址;出来的是最终 SOCKS 地址。
调用关系:NetworkProxyBuilder::build 用它决定 SOCKS5 地址该写进代理对象和环境变量里。
ReservedListenerSet::into_reserved_listeners86–91 ↗
fn into_reserved_listeners(self) -> Arc<ReservedListeners>
作用:把端口预留集合转换成运行时可共享、可一次性取走的形式。
数据流:进去的是 ReservedListenerSet 本身;它把 HTTP 和 SOCKS 监听器移进 ReservedListeners,再用 Arc(引用计数共享指针,让多个地方安全共享同一对象)包起来;出来的是 Arc<ReservedListeners>。
调用关系:NetworkProxyBuilder::build 在托管模式下调用它,之后 NetworkProxy::run 会从里面取监听器启动服务。
NetworkProxyBuilder::default105–114 ↗
fn default() -> Self
作用:创建一个空白的代理建造器,带上默认选择:由 Codex 管理端口,但还没有状态、地址或策略。
数据流:没有输入;它填好默认字段,比如 managed_by_codex 为 true;出来的是 NetworkProxyBuilder。
调用关系:NetworkProxy::builder 会调用它,外部代码再链式设置 state、地址和策略。
调用图:被 1 处调用(builder)。
NetworkProxyBuilder::state118–121 ↗
fn state(mut self, state: Arc<NetworkProxyState>) -> Self
作用:给代理建造器装上共享状态。这个状态里有当前配置、允许/拒绝域名等代理运行必须知道的信息。
数据流:进去的是建造器和 NetworkProxyState;它把状态存起来;出来的是更新后的建造器,方便继续链式调用。
调用关系:调用者创建代理前必须设置它,NetworkProxyBuilder::build 会检查这个字段,没有就报错。
NetworkProxyBuilder::http_addr123–126 ↗
fn http_addr(mut self, addr: SocketAddr) -> Self
作用:指定 HTTP 代理要监听的地址,主要用于非 Codex 托管模式或测试。
数据流:进去的是建造器和一个地址;它把地址记到 http_addr 字段;出来的是更新后的建造器。
调用关系:NetworkProxyBuilder::build 在非托管模式下会优先使用这个地址,否则使用配置解析出的地址。
NetworkProxyBuilder::socks_addr128–131 ↗
fn socks_addr(mut self, addr: SocketAddr) -> Self
作用:指定 SOCKS5 代理要监听的地址,方便外部控制端口或测试固定端口。
数据流:进去的是建造器和 SOCKS 地址;它保存这个地址;出来的是更新后的建造器。
调用关系:NetworkProxyBuilder::build 在非托管模式下会用它覆盖配置里的 SOCKS 地址。
NetworkProxyBuilder::managed_by_codex133–136 ↗
fn managed_by_codex(mut self, managed_by_codex: bool) -> Self
作用:设置端口是否由 Codex 自己管理。托管时 Codex 会主动占安全的本机端口;非托管时更尊重传入或配置的地址。
数据流:进去的是建造器和布尔值;它更新 managed_by_codex;出来的是更新后的建造器。
调用关系:NetworkProxyBuilder::build 根据这个开关走两条不同路线:托管模式会预留端口,非托管模式直接采用地址。
NetworkProxyBuilder::policy_decider138–144 ↗
fn policy_decider(mut self, decider: D) -> Self
作用:设置一个网络策略判断器,用来决定某个请求该放行还是拦截。
数据流:进去的是建造器和一个实现 NetworkPolicyDecider 的对象;它用 Arc 包起来保存;出来的是更新后的建造器。
调用关系:NetworkProxy::run 启动 HTTP 和 SOCKS5 代理时,会把这个判断器传给真正处理请求的代理模块。
调用图:外部调用 1 个(new)。
NetworkProxyBuilder::policy_decider_arc146–149 ↗
fn policy_decider_arc(mut self, decider: Arc<dyn NetworkPolicyDecider>) -> Self
作用:直接接收已经用 Arc 包好的网络策略判断器,避免重复包装,方便共享同一个判断器。
数据流:进去的是建造器和 Arc<dyn NetworkPolicyDecider>;它保存这个共享对象;出来的是更新后的建造器。
调用关系:它服务于已经有共享策略对象的调用者,最终同样由 NetworkProxy::run 传给 HTTP/SOCKS5 代理。
NetworkProxyBuilder::blocked_request_observer151–157 ↗
fn blocked_request_observer(mut self, observer: O) -> Self
作用:设置一个“被拦请求观察者”,当请求被拒绝时可以记录或通知外部。
数据流:进去的是建造器和观察者对象;它用 Arc 包起来保存;出来的是更新后的建造器。
调用关系:NetworkProxyBuilder::build 会把这个观察者写进 NetworkProxyState,之后代理拒绝请求时状态层可以通知它。
调用图:外部调用 1 个(new)。
NetworkProxyBuilder::blocked_request_observer_arc159–165 ↗
fn blocked_request_observer_arc(
mut self,
observer: Arc<dyn BlockedRequestObserver>,
) -> Self
作用:设置已经共享包装好的被拦请求观察者,适合多个地方共用同一个观察者。
数据流:进去的是建造器和 Arc<dyn BlockedRequestObserver>;它保存起来;出来的是更新后的建造器。
调用关系:和 blocked_request_observer 作用一样,只是省掉内部包装,最终在 build 时放进状态对象。
NetworkProxyBuilder::build167–231 ↗
async fn build(self) -> Result<NetworkProxy>
作用:真正造出 NetworkProxy。它会检查必要状态、解析配置、占端口、计算运行时设置,并把这些零件装成一个可启动的代理对象。
数据流:进去的是配置好的建造器;它读取当前配置,必要时解析运行地址、预留本机端口、限制绑定地址、生成运行时设置;出来的是 NetworkProxy,失败时返回带上下文的错误。
调用关系:外部通常通过 NetworkProxy::builder 开始,最后调用 build。它会调用 config::resolve_runtime、config::clamp_bind_addrs、reserve_loopback_ephemeral_listeners 或 Windows 的 reserve_windows_managed_listeners,并用 NetworkProxyRuntimeSettings::from_config 准备运行时开关。
调用图:调用 5 个内部函数(clamp_bind_addrs, resolve_runtime, from_config, reserve_loopback_ephemeral_listeners, reserve_windows_managed_listeners);外部调用 2 个(new, new)。
reserve_loopback_ephemeral_listeners234–245 ↗
fn reserve_loopback_ephemeral_listeners(
reserve_socks_listener: bool,
) -> Result<ReservedListenerSet>
作用:在本机回环地址上预留临时端口。临时端口就是让操作系统自动挑一个空闲端口,减少冲突。
数据流:进去的是是否也要预留 SOCKS 端口;它总是预留 HTTP 端口,必要时再预留 SOCKS 端口;出来的是 ReservedListenerSet。
调用关系:NetworkProxyBuilder::build 在非 Windows 托管模式下调用它;Windows 托管端口被占用时也会退回调用它。
调用图:调用 2 个内部函数(new, reserve_loopback_ephemeral_listener);被 2 处调用(build, reserve_windows_managed_listeners)。
reserve_windows_managed_listeners248–265 ↗
fn reserve_windows_managed_listeners(
http_addr: SocketAddr,
socks_addr: SocketAddr,
reserve_socks_listener: bool,
) -> Result<ReservedListenerSet>
作用:在 Windows 上优先按配置的托管端口占位;如果端口被占,就退回到随机本机端口,避免启动失败。
数据流:进去的是 HTTP 地址、SOCKS 地址和是否需要 SOCKS;它先把地址夹到 127.0.0.1,再尝试绑定;如果只是端口占用,就警告并改用临时端口;出来的是预留监听器集合或错误。
调用关系:NetworkProxyBuilder::build 在 Windows 托管模式下调用它。它内部会用 windows_managed_loopback_addr、try_reserve_windows_managed_listeners 和 reserve_loopback_ephemeral_listeners。
调用图:调用 3 个内部函数(reserve_loopback_ephemeral_listeners, try_reserve_windows_managed_listeners, windows_managed_loopback_addr);被 2 处调用(build, reserve_windows_managed_listeners_falls_back_when_http_port_is_busy);外部调用 1 个(warn!)。
try_reserve_windows_managed_listeners268–280 ↗
fn try_reserve_windows_managed_listeners(
http_addr: SocketAddr,
socks_addr: SocketAddr,
reserve_socks_listener: bool,
) -> std::io::Result<ReservedListenerSet>
作用:按指定 Windows 地址直接尝试绑定 HTTP 和可选 SOCKS 端口,不做兜底。
数据流:进去的是两个地址和 SOCKS 开关;它调用系统绑定端口;成功时出来 ReservedListenerSet,失败时直接返回底层 IO 错误。
调用关系:只由 reserve_windows_managed_listeners 调用;外层负责判断端口占用时是否退回随机端口。
调用图:调用 1 个内部函数(new);被 1 处调用(reserve_windows_managed_listeners);外部调用 1 个(bind)。
windows_managed_loopback_addr283–291 ↗
fn windows_managed_loopback_addr(addr: SocketAddr) -> SocketAddr
作用:把 Windows 托管代理地址强制改成本机地址 127.0.0.1,同时保留端口,防止代理监听到外部网卡。
数据流:进去的是任意 SocketAddr;如果 IP 不是回环地址就记一条警告;出来的是 127.0.0.1:原端口。
调用关系:reserve_windows_managed_listeners 在真正绑定前调用它,作为安全护栏。
调用图:被 1 处调用(reserve_windows_managed_listeners);外部调用 4 个(from, ip, port, warn!)。
reserve_loopback_ephemeral_listener293–296 ↗
fn reserve_loopback_ephemeral_listener() -> Result<StdTcpListener>
作用:在 127.0.0.1 上绑定一个由系统分配的空闲端口。
数据流:没有业务输入;它绑定 127.0.0.1:0,其中端口 0 表示请系统挑端口;出来的是标准 TCP 监听器。
调用关系:reserve_loopback_ephemeral_listeners 用它分别创建 HTTP 和可选 SOCKS 的占位监听器。
调用图:被 1 处调用(reserve_loopback_ephemeral_listeners);外部调用 2 个(from, bind)。
NetworkProxyRuntimeSettings::from_config307–323 ↗
fn from_config(config: &config::NetworkProxyConfig) -> Result<Self>
作用:从完整配置里提取代理运行期间常用的开关,并在需要 HTTPS 中间人代理时准备证书信任包。
数据流:进去的是 NetworkProxyConfig;它读取是否允许本地绑定、允许哪些 Unix socket、是否危险地允许全部 Unix socket,以及是否启用 MITM;出来的是 NetworkProxyRuntimeSettings。
调用关系:NetworkProxyBuilder::build 创建代理时调用它;NetworkProxy::replace_config_state 更新配置时也会重新生成它。
调用图:调用 1 个内部函数(managed_ca_trust_bundle);被 2 处调用(replace_config_state, build)。
NetworkProxy::fmt338–345 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:定义 NetworkProxy 被打印调试信息时显示什么。它只显示地址,避免把内部配置、路径等敏感或吵杂信息打进日志。
数据流:进去的是代理对象和格式化器;它写入 http_addr、socks_addr,并标记还有未展开字段;出来的是格式化结果。
调用关系:Rust 调试打印 NetworkProxy 时自动使用它,通常出现在日志或测试失败输出中。
调用图:外部调用 1 个(debug_struct)。
NetworkProxy::eq349–353 ↗
fn eq(&self, other: &Self) -> bool
作用:判断两个 NetworkProxy 是否可认为相同。它比较 HTTP 地址、SOCKS 地址和运行时设置,而不是比较内部共享状态指针。
数据流:进去的是两个代理对象;它读取双方地址和 runtime_settings;出来的是 true 或 false。
调用关系:当代码或测试需要比较代理对象时自动使用它;它会调用 NetworkProxy::runtime_settings。
调用图:调用 1 个内部函数(runtime_settings);外部调用 1 个(runtime_settings)。
proxy_url_env_value452–461 ↗
fn proxy_url_env_value(
env: &'a HashMap<String, String>,
canonical_key: &str,
) -> Option<&'a str>
作用:从环境变量表里读取某个代理变量,同时兼容大写和小写名字。很多工具有的认 HTTP_PROXY,有的认 http_proxy。
数据流:进去的是环境变量 HashMap 和规范键名;它先查原键名,再查小写版本;出来的是找到的字符串引用或 None。
调用关系:has_proxy_url_env_vars 用它批量判断环境里是否已经存在代理设置;相关测试验证小写别名能被识别。
has_proxy_url_env_vars463–467 ↗
fn has_proxy_url_env_vars(env: &HashMap<String, String>) -> bool
作用:判断一组环境变量里是否已经设置了任何常见代理地址,而且值不是空白。
数据流:进去的是环境变量表;它逐个检查 HTTP、HTTPS、ALL、FTP、WebSocket、npm、pip、Docker 等代理变量;出来的是是否存在有效代理设置。
调用关系:它依赖 proxy_url_env_value 处理大小写差异,供启动或配置逻辑判断环境是否已有代理痕迹。
set_env_keys469–473 ↗
fn set_env_keys(env: &mut HashMap<String, String>, keys: &[&str], value: &str)
作用:把同一个值批量写进多个环境变量名,避免一遍遍重复插入。
数据流:进去的是可修改的环境变量表、一组键名和一个值;它逐个写入 key=value;出来没有单独结果,但环境变量表被改了。
调用关系:apply_proxy_env_overrides 大量调用它,把同一个代理地址写给不同工具认识的变量名。
调用图:被 1 处调用(apply_proxy_env_overrides)。
codex_proxy_git_ssh_command476–478 ↗
fn codex_proxy_git_ssh_command(socks_addr: SocketAddr) -> String
作用:在 macOS 上生成一条 Git 走 SOCKS5 代理的 SSH 命令。这样 Git 通过 SSH 拉代码时也能被代理管住。
数据流:进去的是 SOCKS5 代理地址;它把地址拼进固定的 ssh ProxyCommand 字符串;出来的是完整命令字符串。
调用关系:apply_proxy_env_overrides 在 macOS 且 SOCKS5 启用时会调用它;相关测试也用它生成期望值。
调用图:被 2 处调用(apply_proxy_env_overrides, apply_proxy_env_overrides_refreshes_previous_codex_proxy_git_ssh_command);外部调用 1 个(format!)。
is_codex_proxy_git_ssh_command481–484 ↗
fn is_codex_proxy_git_ssh_command(command: &str) -> bool
作用:判断一条 GIT_SSH_COMMAND 是否是 Codex 之前自动写入的命令,而不是用户自己的 SSH 包装命令。
数据流:进去的是命令字符串;它检查固定前缀和后缀;出来的是布尔值。
调用关系:apply_proxy_env_overrides 用它决定是否可以刷新旧的 Codex SSH 代理命令,避免覆盖用户自定义设置。
调用图:被 1 处调用(apply_proxy_env_overrides)。
apply_proxy_env_overrides486–593 ↗
fn apply_proxy_env_overrides(
env: &mut HashMap<String, String>,
http_addr: SocketAddr,
socks_addr: SocketAddr,
socks_enabled: bool,
allow_local_binding: bool,
mitm_ca_trust_bu
作用:把一个子进程的环境变量改造成“所有常见工具都尽量走 Codex 代理”。这是防止子进程绕过网络沙箱的关键步骤。
数据流:进去的是可修改环境变量表、HTTP 地址、SOCKS 地址、SOCKS 是否启用、是否允许本地绑定、可选 MITM 证书包;它写入各种代理变量、NO_PROXY、本地绑定标记、Node/Electron 开关、macOS Git SSH 命令和证书变量;出来没有返回值,但环境变量表被系统性改写。
调用关系:NetworkProxy::apply_to_env 调用它给子进程套代理环境;大量测试直接调用它,确认不同工具变量、SOCKS 开关、证书覆盖和 macOS Git 行为都正确。
调用图:调用 3 个内部函数(codex_proxy_git_ssh_command, is_codex_proxy_git_ssh_command, set_env_keys);被 10 处调用(apply_to_env, apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override, apply_proxy_env_overrides_preserves_existing_git_ssh_command, apply_proxy_env_overrides_preserves_unmarked_git_ssh_command_with_proxy_shape, apply_proxy_env_overrides_refreshes_previous_codex_proxy_git_ssh_command, apply_proxy_env_overrides_sets_common_tool_vars, apply_proxy_env_overrides_sets_mitm_ca_trust_bundle_vars, apply_proxy_env_overrides_sets_only_expected_env_keys, apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks, apply_proxy_env_overrides_uses_plain_http_proxy_url);外部调用 1 个(format!)。
NetworkProxy::builder596–598 ↗
fn builder() -> NetworkProxyBuilder
作用:提供创建 NetworkProxy 的入口,让调用者用链式方式一点点填配置。
数据流:没有输入;它调用 NetworkProxyBuilder::default;出来的是默认建造器。
调用关系:启动代理、测试代理以及生成沙箱参数的代码都会从这里开始,然后调用 builder 的各种设置方法和 build。
调用图:调用 1 个内部函数(default);被 6 处调用(start_proxy, test_network_proxy, managed_proxy_builder_does_not_reserve_socks_listener_when_disabled, managed_proxy_builder_uses_loopback_ports, non_codex_managed_proxy_builder_uses_configured_ports, create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths)。
NetworkProxy::http_addr600–602 ↗
fn http_addr(&self) -> SocketAddr
作用:返回当前 HTTP 代理监听地址,方便外部告诉子进程或显示状态。
数据流:进去的是代理对象;它读取 http_addr 字段;出来的是 SocketAddr。
调用关系:外部需要知道 HTTP 代理入口时调用它;环境变量写入则由 NetworkProxy::apply_to_env 使用内部字段完成。
NetworkProxy::socks_addr604–606 ↗
fn socks_addr(&self) -> SocketAddr
作用:返回当前 SOCKS5 代理监听地址,方便需要 SOCKS 入口的工具使用。
数据流:进去的是代理对象;它读取 socks_addr 字段;出来的是 SocketAddr。
调用关系:外部需要 SOCKS5 地址时调用它,尤其是 Git SSH 或 ALL_PROXY 相关场景。
NetworkProxy::current_cfg608–610 ↗
async fn current_cfg(&self) -> Result<config::NetworkProxyConfig>
作用:读取代理当前使用的完整配置。
数据流:进去的是代理对象;它向共享状态请求 current_cfg;出来的是 NetworkProxyConfig 或错误。
调用关系:这是 NetworkProxyState 的一个薄封装,供外部不直接碰 state 也能拿到当前配置。
NetworkProxy::add_allowed_domain612–614 ↗
async fn add_allowed_domain(&self, host: &str) -> Result<()>
作用:运行中追加一个允许访问的域名,让代理以后放行这个目标。
数据流:进去的是代理对象和域名字符串;它把请求交给 state.add_allowed_domain;出来是成功或错误。
调用关系:外部策略调整入口会调用它,真正修改规则的工作在 NetworkProxyState 里完成。
NetworkProxy::add_denied_domain616–618 ↗
async fn add_denied_domain(&self, host: &str) -> Result<()>
作用:运行中追加一个拒绝访问的域名,让代理以后拦住这个目标。
数据流:进去的是代理对象和域名字符串;它交给 state.add_denied_domain;出来是成功或错误。
调用关系:它是代理对外暴露的拒绝名单更新口,底层规则变更由 NetworkProxyState 执行。
NetworkProxy::allow_local_binding620–622 ↗
fn allow_local_binding(&self) -> bool
作用:告诉外部当前是否允许子进程绑定本地端口。本地绑定就是程序在本机开一个监听端口。
数据流:进去的是代理对象;它读取运行时设置里的 allow_local_binding;出来的是布尔值。
调用关系:它调用 NetworkProxy::runtime_settings,通常供沙箱或环境设置逻辑判断要不要允许本地服务。
调用图:调用 1 个内部函数(runtime_settings)。
NetworkProxy::allow_unix_sockets624–626 ↗
fn allow_unix_sockets(&self) -> Arc<[String]>
作用:返回当前允许访问的 Unix socket 路径列表。Unix socket 可以理解成本机上的“通信插座文件”。
数据流:进去的是代理对象;它读取运行时设置里的 allow_unix_sockets;出来的是共享字符串列表。
调用关系:它通过 NetworkProxy::runtime_settings 获取快照,供沙箱参数生成等代码使用。
调用图:调用 1 个内部函数(runtime_settings)。
NetworkProxy::dangerously_allow_all_unix_sockets628–630 ↗
fn dangerously_allow_all_unix_sockets(&self) -> bool
作用:返回是否开启了危险选项:允许访问所有 Unix socket。这个选项权限很大,所以名字里带 dangerously。
数据流:进去的是代理对象;它读取运行时设置;出来的是布尔值。
调用关系:它调用 NetworkProxy::runtime_settings,供沙箱或安全检查逻辑决定是否放宽限制。
调用图:调用 1 个内部函数(runtime_settings)。
NetworkProxy::managed_mitm_ca_trust_bundle_path633–641 ↗
fn managed_mitm_ca_trust_bundle_path(&self) -> Option<AbsolutePathBuf>
作用:返回 Codex 生成的 MITM 证书包路径。MITM 是“中间人代理”,这里指代理为了检查 HTTPS 流量而让客户端信任它的证书。
数据流:进去的是代理对象;它读取运行时设置里的证书包,尝试把路径确认成绝对路径;出来的是可选 AbsolutePathBuf,路径无效时记录警告并返回 None。
调用关系:需要把证书包暴露给子沙箱或 TLS 客户端时会调用它;它依赖 NetworkProxy::runtime_settings。
调用图:调用 1 个内部函数(runtime_settings)。
NetworkProxy::apply_to_env643–655 ↗
fn apply_to_env(&self, env: &mut HashMap<String, String>)
作用:把当前代理的地址和运行时设置写进某个子进程的环境变量。
数据流:进去的是代理对象和可修改的环境变量表;它读取 runtime_settings,然后调用 apply_proxy_env_overrides;出来没有返回值,但环境变量表被更新。
调用关系:启动受控子进程前会调用它;具体写哪些变量的细节交给 apply_proxy_env_overrides。
调用图:调用 2 个内部函数(runtime_settings, apply_proxy_env_overrides)。
NetworkProxy::replace_config_state657–688 ↗
async fn replace_config_state(&self, new_state: ConfigState) -> Result<()>
作用:在代理运行中替换配置状态,但只允许改那些不会影响监听端口和协议开关的设置。
数据流:进去的是代理对象和新 ConfigState;它先读取旧配置,确认 network.enabled、proxy_url、socks_url、SOCKS 开关等关键项没变,再生成新运行时设置并替换状态;出来是成功或错误。
调用关系:热更新配置时调用它。它用 NetworkProxyRuntimeSettings::from_config 重算运行时开关,并通过 ensure 检查禁止改动的关键配置。
调用图:调用 1 个内部函数(from_config);外部调用 1 个(ensure!)。
NetworkProxy::runtime_settings690–695 ↗
fn runtime_settings(&self) -> NetworkProxyRuntimeSettings
作用:安全地读取当前运行时设置的一份副本。
数据流:进去的是代理对象;它加读锁读取 RwLock(读写锁,允许多个读者或一个写者)里的设置并克隆;出来的是 NetworkProxyRuntimeSettings。
调用关系:allow_local_binding、allow_unix_sockets、dangerously_allow_all_unix_sockets、managed_mitm_ca_trust_bundle_path、apply_to_env 和 eq 都靠它拿一致的设置快照。
调用图:被 6 处调用(allow_local_binding, allow_unix_sockets, apply_to_env, dangerously_allow_all_unix_sockets, eq, managed_mitm_ca_trust_bundle_path)。
NetworkProxy::run697–763 ↗
async fn run(&self) -> Result<NetworkProxyHandle>
作用:真正启动代理服务。它会启动 HTTP 代理,并在配置允许时启动 SOCKS5 代理。
数据流:进去的是代理对象;它读取当前配置,检查网络代理是否启用,取出预留监听器,克隆状态和策略判断器,然后用异步任务启动 HTTP/SOCKS5 服务;出来的是 NetworkProxyHandle,用来等待或关闭这些任务。
调用关系:这是代理进入主运行阶段的入口。它把实际请求处理交给 http_proxy::run_http_proxy、http_proxy::run_http_proxy_with_std_listener、socks5::run_socks5 或 socks5::run_socks5_with_std_listener。
调用图:调用 6 个内部函数(run_http_proxy, run_http_proxy_with_std_listener, noop, unix_socket_permissions_supported, run_socks5, run_socks5_with_std_listener);外部调用 2 个(spawn, warn!)。
NetworkProxyHandle::noop773–779 ↗
NetworkProxyHandle::wait781–795 ↗
async fn wait(mut self) -> Result<()>
作用:等待 HTTP 和可选 SOCKS5 代理任务结束,并把任务里的错误传出来。
数据流:进去的是句柄本身;它取出任务句柄,等待 HTTP 任务,再等待 SOCKS 任务,标记 completed;出来是成功或任一任务的错误。
调用关系:上层想让代理一直跑到自然结束时调用它。和 shutdown 不同,它不会主动中止任务。
NetworkProxyHandle::shutdown797–801 ↗
async fn shutdown(mut self) -> Result<()>
作用:主动关闭代理任务,用于程序退出或不再需要代理时。
数据流:进去的是句柄本身;它取出 HTTP/SOCKS 任务并交给 abort_tasks 中止,随后标记 completed;出来是成功。
调用关系:上层优雅收尾时调用它;真正中止任务的动作由 abort_tasks 和 abort_task 完成。
调用图:调用 1 个内部函数(abort_tasks)。
abort_task804–809 ↗
async fn abort_task(task: Option<JoinHandle<Result<()>>>)
作用:中止一个异步任务,并等待它真正结束,避免后台任务悬着。
数据流:进去的是可选 JoinHandle;如果有任务,它调用 abort 发送中止信号,再 await 等它收尾;没有返回业务结果。
调用关系:abort_tasks 用它分别处理 HTTP 和 SOCKS5 任务。
调用图:被 1 处调用(abort_tasks)。
abort_tasks811–817 ↗
async fn abort_tasks(
http_task: Option<JoinHandle<Result<()>>>,
socks_task: Option<JoinHandle<Result<()>>>,
)
作用:一次性中止 HTTP 和 SOCKS5 两个代理任务。
数据流:进去的是两个可选任务句柄;它依次调用 abort_task;出来没有业务结果,但任务会被要求停止。
调用关系:NetworkProxyHandle::shutdown 明确关闭时调用它;NetworkProxyHandle::drop 在句柄被丢弃但还没完成时也会异步调用它兜底。
调用图:调用 1 个内部函数(abort_task);被 2 处调用(drop, shutdown)。
NetworkProxyHandle::drop820–829 ↗
fn drop(&mut self)
作用:作为安全兜底:如果句柄被丢掉时代理任务还没正常结束,就自动中止它们。
数据流:进去的是即将销毁的句柄;如果 completed 为 false,它取出任务并启动一个异步清理任务调用 abort_tasks;出来没有返回值。
调用关系:这是 Rust 的析构逻辑,调用者不需要手动调用。它防止忘记 shutdown 导致代理任务泄漏。
调用图:调用 1 个内部函数(abort_tasks);外部调用 1 个(spawn)。
tests::managed_proxy_builder_uses_loopback_ports843–881 ↗
async fn managed_proxy_builder_uses_loopback_ports()
作用:测试 Codex 托管模式下,代理最终使用本机回环端口,而不是暴露到外部地址。
数据流:进去的是测试临时创建的配置和状态;它构建代理并检查 HTTP/SOCKS 地址;测试结果是断言通过或失败。
调用关系:它通过 NetworkProxy::builder 和 build 验证托管端口选择逻辑,也覆盖部分平台差异。
调用图:调用 2 个内部函数(default, builder);外部调用 9 个(new, from, bind, assert!, assert_eq!, assert_ne!, network_proxy_state_for_policy, format!, panic!)。
tests::non_codex_managed_proxy_builder_uses_configured_ports884–906 ↗
async fn non_codex_managed_proxy_builder_uses_configured_ports()
作用:测试非 Codex 托管模式会按配置里的固定端口来,而不是自动挑随机端口。
数据流:进去的是写死 HTTP/SOCKS 地址的测试配置;它构建非托管代理;出来通过断言确认地址等于配置值。
调用关系:它走 NetworkProxy::builder、managed_by_codex(false) 和 build,验证 build 的非托管分支。
调用图:调用 2 个内部函数(default, builder);外部调用 3 个(new, assert_eq!, network_proxy_state_for_policy)。
tests::managed_proxy_builder_does_not_reserve_socks_listener_when_disabled909–944 ↗
async fn managed_proxy_builder_does_not_reserve_socks_listener_when_disabled()
作用:测试关闭 SOCKS5 时,构建器不会多占一个 SOCKS 监听端口。
数据流:进去的是 enable_socks5 为 false 的配置;它构建代理,检查 HTTP 是本机临时端口、SOCKS 地址仍来自配置,并确认没有预留 SOCKS 监听器。
调用关系:它验证 reserve_loopback_ephemeral_listeners 和 build 在 SOCKS 关闭时的配合。
调用图:调用 2 个内部函数(default, builder);外部调用 6 个(new, assert!, assert_eq!, assert_ne!, network_proxy_state_for_policy, panic!)。
tests::windows_managed_loopback_addr_clamps_non_loopback_inputs948–957 ↗
fn windows_managed_loopback_addr_clamps_non_loopback_inputs()
作用:测试 Windows 上非本机地址会被强制改成 127.0.0.1,同时保留端口。
数据流:进去的是 0.0.0.0 和 :: 这类非回环地址;它调用 windows_managed_loopback_addr;出来通过断言确认结果是 127.0.0.1:原端口。
调用关系:它只在 Windows 编译,专门保护 Windows 托管代理的安全夹紧逻辑。
调用图:外部调用 1 个(assert_eq!)。
tests::reserve_windows_managed_listeners_falls_back_when_http_port_is_busy961–985 ↗
fn reserve_windows_managed_listeners_falls_back_when_http_port_is_busy()
作用:测试 Windows 托管端口被占用时,会退回到随机本机端口,而不是直接失败。
数据流:进去的是一个故意被占用的端口;它调用 reserve_windows_managed_listeners;出来通过断言确认新 HTTP 端口仍是回环地址但不是忙端口。
调用关系:它验证 reserve_windows_managed_listeners 对 AddrInUse 错误的兜底分支。
调用图:调用 1 个内部函数(reserve_windows_managed_listeners);外部调用 4 个(from, bind, assert!, assert_ne!)。
tests::proxy_url_env_value_resolves_lowercase_aliases988–999 ↗
fn proxy_url_env_value_resolves_lowercase_aliases()
作用:测试读取代理环境变量时能识别小写别名,比如 http_proxy。
数据流:进去的是只含 http_proxy 的环境表;它查 HTTP_PROXY;出来通过断言确认能读到小写变量的值。
调用关系:它直接覆盖 proxy_url_env_value 的大小写兼容逻辑。
调用图:外部调用 2 个(new, assert_eq!)。
tests::has_proxy_url_env_vars_detects_lowercase_aliases1002–1010 ↗
fn has_proxy_url_env_vars_detects_lowercase_aliases()
作用:测试判断环境里是否有代理变量时,也能识别小写 all_proxy。
数据流:进去的是含 all_proxy 的环境表;它调用 has_proxy_url_env_vars;出来断言为 true。
调用关系:它验证 has_proxy_url_env_vars 通过 proxy_url_env_value 获得大小写兼容。
调用图:外部调用 2 个(new, assert_eq!)。
tests::has_proxy_url_env_vars_detects_websocket_proxy_keys1013–1018 ↗
fn has_proxy_url_env_vars_detects_websocket_proxy_keys()
作用:测试 WebSocket 专用代理变量也会被当成有效代理设置。
数据流:进去的是含 wss_proxy 的环境表;它调用 has_proxy_url_env_vars;出来断言为 true。
调用关系:它保护 PROXY_URL_ENV_KEYS 中包含 WS/WSS 代理变量这一行为。
调用图:外部调用 2 个(new, assert_eq!)。
tests::apply_proxy_env_overrides_sets_common_tool_vars1021–1082 ↗
fn apply_proxy_env_overrides_sets_common_tool_vars()
作用:测试代理环境写入函数会设置常见工具需要的变量,比如 HTTP_PROXY、ALL_PROXY、NO_PROXY、Node 和 Electron 开关。
数据流:进去的是空环境表和测试用 HTTP/SOCKS 地址;它调用 apply_proxy_env_overrides;出来通过多条断言检查环境表被正确填充。
调用关系:它直接验证 apply_proxy_env_overrides 的核心输出,是该函数最主要的回归测试之一。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 5 个(new, V4, new, assert!, assert_eq!)。
tests::apply_proxy_env_overrides_sets_only_expected_env_keys1085–1104 ↗
fn apply_proxy_env_overrides_sets_only_expected_env_keys()
作用:测试代理环境写入函数不会偷偷写入名单外的环境变量。
数据流:进去的是空环境表和测试地址;它调用 apply_proxy_env_overrides,再遍历所有写入的键;出来通过断言确认每个键都在允许列表里。
调用关系:它保护 PROXY_ENV_KEYS 这份白名单,避免未来改动无意污染子进程环境。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 5 个(new, V4, new, assert!, cfg!)。
tests::apply_proxy_env_overrides_sets_mitm_ca_trust_bundle_vars1107–1129 ↗
fn apply_proxy_env_overrides_sets_mitm_ca_trust_bundle_vars()
作用:测试启用 MITM 证书包时,会把证书路径写进各种 TLS 客户端认识的环境变量。
数据流:进去的是空环境表、测试地址和一个假的托管证书包路径;它调用 apply_proxy_env_overrides;出来断言每个 CUSTOM_CA_ENV_KEYS 都指向该路径。
调用关系:它验证 apply_proxy_env_overrides 与 certs::CUSTOM_CA_ENV_KEYS 的配合。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 5 个(new, V4, new, new, assert_eq!)。
tests::apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override1132–1158 ↗
fn apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override()
作用:测试如果某个命令已经专门设置了自己的 CA 证书变量,代理不会粗暴覆盖它。
数据流:进去的是带 REQUESTS_CA_BUNDLE 自定义值的环境表和托管证书包;它调用 apply_proxy_env_overrides;出来确认自定义值保留,同时其他证书变量仍写入托管路径。
调用关系:它保护 apply_proxy_env_overrides 中“保留命令级证书覆盖”的细节,避免破坏用户明确指定的证书。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 6 个(from, new, V4, new, new, assert_eq!)。
tests::apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks1161–1177 ↗
fn apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks()
作用:测试 SOCKS5 关闭时,ALL_PROXY 会退回使用 HTTP 代理地址。
数据流:进去的是空环境表、测试地址和 socks_enabled=false;它调用 apply_proxy_env_overrides;出来断言 ALL_PROXY 是 http:// 地址,并确认本地绑定标记正确。
调用关系:它验证 apply_proxy_env_overrides 根据 SOCKS 开关选择 ALL_PROXY 协议的分支。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 4 个(new, V4, new, assert_eq!)。
tests::apply_proxy_env_overrides_uses_plain_http_proxy_url1180–1221 ↗
fn apply_proxy_env_overrides_uses_plain_http_proxy_url()
作用:测试 HTTP_PROXY、HTTPS_PROXY、WS_PROXY、WSS_PROXY 始终使用普通 HTTP 代理 URL,而不是 SOCKS URL。
数据流:进去的是空环境表和 SOCKS 启用的测试地址;它调用 apply_proxy_env_overrides;出来断言 HTTP 类变量是 http://,ALL_PROXY 才是 socks5h://。
调用关系:它保护一个兼容性规则:很多客户端不接受 HTTP_PROXY 里放 SOCKS 地址。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 4 个(new, V4, new, assert_eq!)。
tests::apply_proxy_env_overrides_preserves_existing_git_ssh_command1225–1244 ↗
fn apply_proxy_env_overrides_preserves_existing_git_ssh_command()
作用:在 macOS 上测试已有的用户 Git SSH 命令不会被 Codex 覆盖。
数据流:进去的是带自定义 GIT_SSH_COMMAND 的环境表;它调用 apply_proxy_env_overrides;出来断言该命令保持原样。
调用关系:它验证 apply_proxy_env_overrides 对用户 SSH 包装器的尊重,只在 macOS 编译运行。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 4 个(new, V4, new, assert_eq!)。
tests::apply_proxy_env_overrides_preserves_unmarked_git_ssh_command_with_proxy_shape1248–1267 ↗
fn apply_proxy_env_overrides_preserves_unmarked_git_ssh_command_with_proxy_shape()
作用:在 macOS 上测试即使命令看起来像代理命令,只要不是 Codex 标记过的,也不会被刷新。
数据流:进去的是一个未带 Codex 标记但形状像 ProxyCommand 的 GIT_SSH_COMMAND;它调用 apply_proxy_env_overrides;出来断言原值不变。
调用关系:它配合 is_codex_proxy_git_ssh_command,防止误把用户命令当成 Codex 自动生成命令。
调用图:调用 1 个内部函数(apply_proxy_env_overrides);外部调用 4 个(new, V4, new, assert_eq!)。
tests::apply_proxy_env_overrides_refreshes_previous_codex_proxy_git_ssh_command1271–1294 ↗
fn apply_proxy_env_overrides_refreshes_previous_codex_proxy_git_ssh_command()
作用:在 macOS 上测试旧的 Codex Git SSH 代理命令会被刷新到新的 SOCKS 端口。
数据流:进去的是带旧 Codex GIT_SSH_COMMAND 的环境表和新 SOCKS 地址;它调用 apply_proxy_env_overrides;出来断言命令里的端口更新了。
调用关系:它验证 codex_proxy_git_ssh_command、is_codex_proxy_git_ssh_command 和 apply_proxy_env_overrides 的协作,避免代理重启后 Git 还指向旧端口。
调用图:调用 2 个内部函数(apply_proxy_env_overrides, codex_proxy_git_ssh_command);外部调用 4 个(new, V4, new, assert_eq!)。
network-proxy/src/socks5.rs源码 ↗
SOCKS5 可以理解成一个“通用转发门卫”:应用告诉它想连哪个地址和端口,它帮忙转过去。这个文件做的事就是把门卫装起来,并规定门卫怎么放行。启动时,它监听一个本地地址,创建 SOCKS5 接收器;收到 TCP 请求后,先看代理是否启用,再看当前是 Full 模式还是 Limited 模式。Limited 模式下,SOCKS5 只允许能被 HTTPS MITM 检查的 443 端口流量;MITM 是“中间人检查”,意思是代理先接住加密连接,检查里面的 HTTPS 请求后再决定是否转发。UDP 请求也会被检查,而且 Limited 模式直接禁止。所有被拦的请求都会记录原因,并发出审计事件,方便之后追查“谁被拦了、为什么被拦”。如果请求允许,TCP 要么直接连上游服务器,要么交给 MITM 流程继续检查。
run_socks563–78 ↗
async fn run_socks5(
state: Arc<NetworkProxyState>,
addr: SocketAddr,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
enable_socks5_udp: bool,
) -> Result<()>
作用:启动 SOCKS5 代理,并让它监听指定的网络地址。外部主程序用它把 SOCKS5 服务真正开起来。
数据流:输入是共享状态、监听地址、可选的策略判断器,以及是否启用 UDP。它先创建一个 TCP 监听器,绑定到这个地址;如果绑定失败,就把错误包装成更好懂的上下文。成功后,它把监听器交给内部的 run_socks5_with_listener,最后返回启动和运行过程中的结果。
调用关系:它由更上层的 run 调用,是 SOCKS5 正常启动路径的门口。它自己只负责“开端口”,真正组装 SOCKS5 服务和处理请求的工作交给 run_socks5_with_listener。
调用图:调用 1 个内部函数(run_socks5_with_listener);被 1 处调用(run);外部调用 1 个(build)。
run_socks5_with_std_listener80–89 ↗
async fn run_socks5_with_std_listener(
state: Arc<NetworkProxyState>,
listener: StdTcpListener,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
enable_socks5_udp: bool,
) -> Res
作用:用一个已经存在的标准 TCP 监听器来启动 SOCKS5 代理。它适合测试或外部代码已经提前打开端口的场景。
数据流:输入是共享状态、标准库的监听器、策略判断器和 UDP 开关。它把标准监听器转换成 rama 框架使用的异步监听器;转换成功后,继续交给 run_socks5_with_listener;转换失败则返回错误。
调用关系:它同样由上层 run 调用,是另一条启动入口。和 run_socks5 的区别是:run_socks5 自己绑定地址,这个函数接收别人已经绑定好的监听器。
调用图:调用 1 个内部函数(run_socks5_with_listener);被 1 处调用(run);外部调用 1 个(try_from)。
run_socks5_with_listener91–151 ↗
async fn run_socks5_with_listener(
state: Arc<NetworkProxyState>,
listener: TcpListener,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
enable_socks5_udp: bool,
) -> Result<()>
作用:把 SOCKS5 服务的各个零件装配起来,然后开始接收客户端连接。它是这个文件里真正搭建代理运行管线的地方。
数据流:输入是应用状态、已经可用的监听器、可选策略判断器和 UDP 开关。它读取监听地址并写日志;查看网络模式,提示 Limited 模式下的限制;然后创建 TCP 连接器、策略检查包装、SOCKS5 接收器。若启用 UDP,还会加上 UDP 检查器。最后它把状态作为扩展信息塞进每个请求里,并开始 serve,持续处理连接。
调用关系:run_socks5 和 run_socks5_with_std_listener 都会把监听器交给它。它把 TCP 请求交给 handle_socks5_tcp,把 TCP 转发结果交给 proxy_socks5_tcp;如果启用 UDP,则把 UDP 包交给 inspect_socks5_udp 做放行检查。
调用图:调用 1 个内部函数(new);被 2 处调用(run_socks5, run_socks5_with_std_listener);外部调用 9 个(new, default, default, new, local_addr, serve, info!, service_fn, warn!)。
handle_socks5_tcp153–408 ↗
async fn handle_socks5_tcp(
req: TcpRequest,
tcp_connector: TargetCheckedTcpConnector,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
) -> Result<EstablishedClientConnection<Socks5
作用:检查一个 SOCKS5 TCP 连接到底能不能连出去,并决定是直接转发还是走 HTTPS MITM 检查。它是 TCP 请求的主要“门禁规则”。
数据流:输入是一个目标主机端口请求、实际拨号用的连接器、可选策略判断器。它从请求扩展里取出全局状态,整理主机名和客户端地址;先检查代理是否启用,再检查 Limited/Full 网络模式,然后构造网络策略请求并调用 evaluate_host_policy 判断域名是否允许。若被拒绝,它记录 blocked 记录、发审计事件、返回带策略说明的权限错误。若允许,它再看这个主机是否需要 MITM、MITM 状态是否存在。最后输出一个 EstablishedClientConnection:要么包着真实 TCP 连接,要么包着 MITM 所需的目标信息。
调用关系:它被 run_socks5_with_listener 装进 TCP 连接器流程里,每次 SOCKS5 TCP 客户端请求上游连接时都会经过这里。它会调用 emit_socks_block_decision_audit_event 记录拒绝事件,调用 policy_denied_error 生成拒绝错误,调用 evaluate_host_policy 做域名策略判断;如果决定直连,就把拨号交给 TargetCheckedTcpConnector。测试函数也直接调用它来验证各种拦截和 MITM 场景。
调用图:调用 7 个内部函数(serve, new, evaluate_host_policy, normalize_host, new, emit_socks_block_decision_audit_event, policy_denied_error);被 4 处调用(handle_socks5_tcp_blocks_hooked_non_https_host_in_full_mode, handle_socks5_tcp_blocks_limited_mode_without_mitm_state, handle_socks5_tcp_uses_mitm_for_hooked_host_in_full_mode, handle_socks5_tcp_uses_mitm_in_limited_mode);外部调用 8 个(new, now, extensions, new, other, error!, info!, warn!)。
Socks5TcpConnection::poll_read424–433 ↗
fn poll_read(
self: Pin<&mut Self>,
cx: &mut TaskContext<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>>
作用:让 Socks5TcpConnection 看起来像一个可读的异步连接。直连时它从真实 TCP 流读取;MITM 占位时它不真的读网络数据。
数据流:输入是连接本身、异步任务上下文和读缓冲区。若连接是 Direct,它把读取动作转交给内部 TcpStream;若是 Mitm,它直接表示读取完成但没有填入数据。输出是一次异步读取的状态。
调用关系:它是 AsyncRead 接口的一部分,供代理框架在需要像普通网络流一样读取 Socks5TcpConnection 时使用。Direct 分支服务于普通转发;Mitm 分支只是为了让 MITM 路径能通过同一套类型接口。
调用图:外部调用 2 个(new, Ready)。
Socks5TcpConnection::poll_write437–446 ↗
fn poll_write(
self: Pin<&mut Self>,
cx: &mut TaskContext<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>>
作用:让 Socks5TcpConnection 看起来像一个可写的异步连接。直连时写入真实服务器;MITM 占位时假装写入成功。
数据流:输入是连接、异步任务上下文和要写出的字节。Direct 情况下,它把字节写到内部 TcpStream;Mitm 情况下,它不发到网络,只返回“已经写了这些字节”。输出是写入结果。
调用关系:它是 AsyncWrite 接口的一部分,主要给 SOCKS5 框架统一处理连接用。真正直连转发会依赖它;MITM 分支只是保持类型兼容,因为实际流量会交给 mitm::mitm_stream。
调用图:外部调用 2 个(new, Ready)。
Socks5TcpConnection::poll_flush448–453 ↗
fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>>
作用:把待写出的数据尽量刷出去。直连时刷新真实 TCP 流;MITM 占位时直接算成功。
数据流:输入是连接和异步任务上下文。Direct 分支调用内部 TcpStream 的刷新;Mitm 分支没有真实上游连接,所以直接返回成功。输出是刷新是否完成。
调用关系:它属于 AsyncWrite 的配套方法,供转发服务或框架在写完数据后调用。它和 poll_write、poll_shutdown 一起让 Socks5TcpConnection 能被当成普通异步连接使用。
调用图:外部调用 2 个(new, Ready)。
Socks5TcpConnection::poll_shutdown455–460 ↗
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>>
作用:关闭写入方向的连接。直连时关闭真实 TCP 流;MITM 占位时直接认为已经关闭。
数据流:输入是连接和异步任务上下文。Direct 分支把关闭动作交给内部 TcpStream;Mitm 分支没有真实上游 socket,所以立即返回成功。输出是关闭操作的异步结果。
调用关系:它也是 AsyncWrite 接口的一部分,通常在转发结束或连接收尾时由框架调用。它保证 Direct 和 Mitm 两种连接形态都能走同一套收尾流程。
调用图:外部调用 2 个(new, Ready)。
Socks5TcpConnection::local_addr464–469 ↗
fn local_addr(&self) -> io::Result<SocketAddr>
作用:返回这个连接本地端的地址。直连时是真实 socket 地址;MITM 占位时返回一个空的 0.0.0.0:0 地址。
数据流:输入是连接本身。Direct 分支读取内部 TcpStream 的本地地址;Mitm 分支构造一个占位地址。输出是本地 SocketAddr 或错误。
调用关系:它实现 Socket 接口,供框架或日志代码查询连接信息。MITM 路径没有提前拨上游,所以只能给一个不会代表真实网络连接的占位地址。
调用图:外部调用 1 个(from)。
Socks5TcpConnection::peer_addr471–476 ↗
fn peer_addr(&self) -> io::Result<SocketAddr>
作用:返回这个连接对端的地址。直连时是真实服务器地址;MITM 占位时返回 0.0.0.0:0。
数据流:输入是连接本身。Direct 分支从真实 TcpStream 读取远端地址;Mitm 分支生成占位地址。输出是对端 SocketAddr 或错误。
调用关系:它和 local_addr 一起满足 Socket 接口。普通转发可以得到真实对端;MITM 路径还没连接上游,所以只能提供占位值,后续真正处理由 MITM 流程完成。
调用图:外部调用 1 个(from)。
Socks5TcpConnection::extensions480–485 ↗
fn extensions(&self) -> &Extensions
作用:读取连接上附带的扩展信息。扩展信息可以理解成“贴在请求或连接旁边的小纸条”,给后续流程传额外数据。
数据流:输入是连接本身。Direct 分支返回内部 TcpStream 的扩展集合;Mitm 分支返回自己保存的扩展集合。输出是只读的 Extensions 引用。
调用关系:它实现 ExtensionsRef 接口,供框架和后续服务读取连接携带的上下文。proxy_socks5_tcp 在 MITM 路径会往源连接写扩展,而这个方法保证目标连接形态也符合框架预期。
Socks5TcpConnection::extensions_mut489–494 ↗
fn extensions_mut(&mut self) -> &mut Extensions
作用:修改连接上附带的扩展信息。它让后续流程能把额外上下文塞进连接里。
数据流:输入是可变的连接本身。Direct 分支返回真实 TcpStream 的可变扩展集合;Mitm 分支返回自身保存的可变扩展集合。输出是可修改的 Extensions 引用,并允许调用者改动其中内容。
调用关系:它实现 ExtensionsMut 接口,和 extensions 配套使用。框架需要统一读写连接扩展时,不必关心底层是 Direct 还是 Mitm。
proxy_socks5_tcp497–515 ↗
async fn proxy_socks5_tcp(
request: ProxyRequest<TcpStream, Socks5TcpConnection>,
) -> Result<(), BoxError>
作用:执行已经通过检查的 SOCKS5 TCP 请求:要么把两端直接打通,要么转入 MITM 检查流程。
数据流:输入是一个 ProxyRequest,里面有客户端连接 source 和目标连接 target。若 target 是 Direct,它用 StreamForwardService 把客户端和服务器双向转发。若 target 是 Mitm,它把目标地址、网络模式、MITM 状态塞进 source 的扩展信息,然后调用 mitm::mitm_stream,让 HTTPS 检查流程接手。输出是转发或 MITM 处理是否成功。
调用关系:它被 run_socks5_with_listener 装进 SOCKS5 服务中,在 handle_socks5_tcp 已经决定连接形态之后运行。它是“放行之后真正搬运数据”的环节;直连交给 StreamForwardService,需检查的 HTTPS 交给 mitm_stream。
调用图:调用 1 个内部函数(mitm_stream);外部调用 2 个(default, ProxyTarget)。
inspect_socks5_udp517–673 ↗
async fn inspect_socks5_udp(
request: RelayRequest,
state: Arc<NetworkProxyState>,
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
) -> io::Result<RelayResponse>
作用:检查一个 SOCKS5 UDP 包能不能被转发。UDP 没有像 TCP 那样稳定的一条连接,所以每个中继请求都要在这里过一次门禁。
数据流:输入是 UDP 中继请求、共享状态和可选策略判断器。它取出目标 IP/端口、数据包内容和扩展信息,整理主机名和客户端地址;先看代理是否启用,再看网络模式。Limited 模式下 UDP 直接拒绝。Full 模式下,它构造 NetworkPolicyRequest 并调用 evaluate_host_policy 判断目标是否允许。允许时输出 RelayResponse,把原 payload 和 extensions 交回继续转发;拒绝时记录 blocked、发审计事件并返回权限错误。
调用关系:当 run_socks5_with_listener 启用了 SOCKS5 UDP 时,它会被挂到 DefaultUdpRelay 的检查器里。它和 handle_socks5_tcp 使用同一套策略判断、审计事件和拒绝错误工具,但专门处理 UDP。
调用图:调用 6 个内部函数(new, evaluate_host_policy, normalize_host, new, emit_socks_block_decision_audit_event, policy_denied_error);外部调用 4 个(new, other, error!, warn!)。
emit_socks_block_decision_audit_event675–696 ↗
fn emit_socks_block_decision_audit_event(
state: &NetworkProxyState,
source: NetworkDecisionSource,
reason: &str,
protocol: NetworkProtocol,
host: &str,
port: u16,
client_a
作用:发出一条“SOCKS 请求被拦截”的审计事件。审计事件就是给日志和监控看的正式记录,说明拦截原因和目标信息。
数据流:输入是状态、决策来源、原因、协议、主机、端口和可选客户端地址。它把这些值整理成 BlockDecisionAuditEventArgs,并交给 emit_block_decision_audit_event。输出没有返回值,但会产生一条审计事件。
调用关系:handle_socks5_tcp 和 inspect_socks5_udp 在拒绝请求时都会调用它。它把 SOCKS5 TCP/UDP 的拦截信息统一翻译成通用网络策略审计格式,避免每个地方重复拼字段。
调用图:调用 1 个内部函数(emit_block_decision_audit_event);被 2 处调用(handle_socks5_tcp, inspect_socks5_udp)。
policy_denied_error698–703 ↗
fn policy_denied_error(reason: &str, details: &PolicyDecisionDetails<'_>) -> io::Error
作用:把策略拒绝变成一个标准的权限错误,并把拒绝原因写进错误消息里。
数据流:输入是拒绝原因和策略细节。它调用 blocked_message_with_policy 生成给调用方看的说明文字,再创建 io::ErrorKind::PermissionDenied 类型的错误。输出是一个 io::Error。
调用关系:handle_socks5_tcp 和 inspect_socks5_udp 在决定拒绝后都会用它生成最终错误。这样 SOCKS5 TCP 和 UDP 返回的拒绝信息格式一致,也能带上策略来源、协议、主机和端口等细节。
调用图:调用 1 个内部函数(blocked_message_with_policy);被 2 处调用(handle_socks5_tcp, inspect_socks5_udp);外部调用 1 个(new)。
tests::StaticReloader::maybe_reload742–744 ↗
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>>
作用:测试用的配置重载器方法,表示“现在没有新配置要自动加载”。它让测试环境可以满足生产代码需要的接口。
数据流:输入是测试重载器本身。它创建一个立即完成的异步结果,内容是 Ok(None),意思是不更新配置。输出是一个 boxed future,也就是装在盒子里的异步任务。
调用关系:它实现 ConfigReloader 接口,供 tests::state_for_settings 创建 NetworkProxyState 时使用。测试不关心自动重载,所以这里固定返回没有变化。
调用图:外部调用 1 个(pin)。
tests::StaticReloader::reload_now746–748 ↗
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState>
作用:测试用的“立刻重载配置”方法,但它不会读文件,只返回预先准备好的配置状态。
数据流:输入是测试重载器本身。它复制内部保存的 ConfigState,并把它包装成一个立即成功的异步结果。输出是包含配置状态的 future。
调用关系:它也是 ConfigReloader 接口的一部分,被 NetworkProxyState 在需要强制加载配置时调用。tests::state_for_settings 用它让状态对象像真实运行时一样可重载,但数据完全来自测试代码。
调用图:外部调用 2 个(pin, clone)。
tests::StaticReloader::source_label750–752 ↗
fn source_label(&self) -> String
作用:返回测试配置来源的名字,方便出错或记录日志时知道这是测试用配置。
数据流:输入是测试重载器本身。它不读取外部数据,直接返回字符串“static test reloader”。输出是这个标签字符串。
调用关系:它完成 ConfigReloader 接口要求的来源说明。测试构造 NetworkProxyState 时会带上这个重载器,生产逻辑如果询问配置来源,就能得到一个固定的测试标签。
tests::state_for_settings755–766 ↗
fn state_for_settings(network: NetworkProxySettings) -> Arc<NetworkProxyState>
作用:根据一组测试用网络设置,快速造出可运行的 NetworkProxyState。它让每个测试不用手写一大堆状态初始化代码。
数据流:输入是 NetworkProxySettings。它包装成 NetworkProxyConfig;如果启用了 MITM,就先拿测试锁,避免多个测试同时改共享的 MITM CA 文件;然后调用 build_config_state 生成配置状态,创建 StaticReloader,最后用 NetworkProxyState::with_reloader 输出共享状态 Arc。
调用关系:多个测试函数都会先调用它准备代理状态,然后再调用 handle_socks5_tcp 或 inspect_socks5_udp。它把真实配置构建流程保留下来,所以测试更接近生产行为。
调用图:调用 2 个内部函数(with_reloader, build_config_state);外部调用 2 个(new, default)。
tests::handle_socks5_tcp_emits_block_decision_for_proxy_disabled769–807 ↗
async fn handle_socks5_tcp_emits_block_decision_for_proxy_disabled()
作用:验证代理被关闭时,SOCKS5 TCP 请求会被拒绝,并且会发出正确的策略审计事件。
数据流:输入来自测试内构造的设置:enabled=false、Full 模式、目标 example.com:443。它创建请求并塞入状态,调用 handle_socks5_tcp,同时用 capture_events 捕获事件。输出是断言结果:请求应失败,事件字段应显示 deny、proxy_state、proxy_disabled、socks5_tcp 等信息。
调用关系:它直接测试 handle_socks5_tcp 的“代理总开关”分支,也间接验证 emit_socks_block_decision_audit_event 和 policy_denied_error 配合正确。
调用图:调用 1 个内部函数(default);外部调用 7 个(try_from, new, assert!, assert_eq!, capture_events, find_event_by_name, state_for_settings)。
tests::handle_socks5_tcp_uses_mitm_in_limited_mode810–832 ↗
async fn handle_socks5_tcp_uses_mitm_in_limited_mode()
作用:验证 Limited 模式下,允许的 443 端口 SOCKS5 TCP 请求会走 MITM,而不是直接连出去。
数据流:它构造启用代理、Limited 模式、启用 MITM,并允许 example.com 的设置;然后创建 example.com:443 请求,调用 handle_socks5_tcp。输出是断言:返回的连接类型必须是 Socks5TcpConnection::Mitm。
调用关系:它覆盖 handle_socks5_tcp 中 Limited 模式加 HTTPS 目标的正常路径。这个测试确保受限模式下的 HTTPS 会进入后续 mitm::mitm_stream 所需的连接形态。
调用图:调用 3 个内部函数(default, new, handle_socks5_tcp);外部调用 5 个(try_from, new, assert!, state_for_settings, vec!)。
tests::handle_socks5_tcp_blocks_non_https_in_limited_mode835–878 ↗
async fn handle_socks5_tcp_blocks_non_https_in_limited_mode()
作用:验证 Limited 模式下,非 443 端口的 SOCKS5 TCP 请求会被拒绝。因为 SOCKS5 只告诉代理目标主机和端口,代理无法确认非 443 流量是不是可安全检查的 HTTPS。
数据流:它构造启用代理、Limited 模式、允许 example.com 的设置;创建 example.com:80 请求并调用 handle_socks5_tcp,同时捕获审计事件。输出是断言:请求失败,事件显示来源是 mode_guard,原因是 method_not_allowed,协议是 socks5_tcp,端口是 80。
调用关系:它测试 handle_socks5_tcp 的 Limited 模式保护规则,也验证拒绝时的审计事件字段没有丢失。
调用图:调用 1 个内部函数(default);外部调用 8 个(try_from, new, assert!, assert_eq!, capture_events, find_event_by_name, state_for_settings, vec!)。
tests::handle_socks5_tcp_blocks_limited_mode_without_mitm_state881–905 ↗
async fn handle_socks5_tcp_blocks_limited_mode_without_mitm_state()
作用:验证 Limited 模式下,即使目标是 443,如果没有可用的 MITM 状态,也必须拒绝。否则代理无法检查 HTTPS 内容。
数据流:它构造启用代理、Limited 模式、但不启用 MITM 的设置;允许 example.com,创建 example.com:443 请求并调用 handle_socks5_tcp。输出是断言:调用应返回错误,并且错误文字里包含“MITM required”。
调用关系:它覆盖 handle_socks5_tcp 中“需要 MITM 但 MITM 没准备好”的安全失败路径,确保系统不会在无法检查时偷偷放行。
调用图:调用 3 个内部函数(default, new, handle_socks5_tcp);外部调用 5 个(try_from, new, assert!, state_for_settings, vec!)。
tests::handle_socks5_tcp_uses_mitm_for_hooked_host_in_full_mode908–939 ↗
async fn handle_socks5_tcp_uses_mitm_for_hooked_host_in_full_mode()
作用:验证 Full 模式下,如果某个主机配置了 MITM hook,也会走 MITM。hook 可以理解成“对特定网站和请求规则加一段检查/处理钩子”。
数据流:它构造 Full 模式、启用 MITM,并给 api.github.com 配置 MITM hook 的设置;允许该域名后,创建 api.github.com:443 请求并调用 handle_socks5_tcp。输出是断言:连接类型是 Mitm。
调用关系:它测试 handle_socks5_tcp 中 Full 模式并非总是直连:只要目标主机有 MITM hook 且是 HTTPS 端口,就要交给 MITM 路径。
调用图:调用 3 个内部函数(default, new, handle_socks5_tcp);外部调用 5 个(try_from, new, assert!, state_for_settings, vec!)。
tests::handle_socks5_tcp_blocks_hooked_non_https_host_in_full_mode942–976 ↗
async fn handle_socks5_tcp_blocks_hooked_non_https_host_in_full_mode()
作用:验证 Full 模式下,如果主机有 MITM hook,但请求不是 443 端口,也要拒绝。因为 hook 需要 HTTPS MITM 才能可靠执行。
数据流:它构造 Full 模式、启用 MITM,并给 api.github.com 配置 hook;然后请求 api.github.com:80。输出是断言:handle_socks5_tcp 返回错误,错误信息包含“MITM required”。
调用关系:它测试 handle_socks5_tcp 的安全边界:有 hook 的主机不能通过非 HTTPS SOCKS5 TCP 绕过检查。
调用图:调用 3 个内部函数(default, new, handle_socks5_tcp);外部调用 5 个(try_from, new, assert!, state_for_settings, vec!)。
tests::inspect_socks5_udp_emits_block_decision_for_mode_guard_deny979–1015 ↗
async fn inspect_socks5_udp_emits_block_decision_for_mode_guard_deny()
作用:验证 Limited 模式下 SOCKS5 UDP 会被模式规则拒绝,并且会发出正确审计事件。
数据流:它构造启用代理、Limited 模式的状态,再创建一个发往 93.184.216.34:53 的 UDP 中继请求。它调用 inspect_socks5_udp 并捕获事件。输出是断言:请求失败,事件字段显示 deny、mode_guard、method_not_allowed、socks5_udp,以及正确的服务器地址和端口。
调用关系:它直接测试 inspect_socks5_udp 的 Limited 模式拒绝分支,也间接验证 UDP 拒绝时使用的 emit_socks_block_decision_audit_event 和 policy_denied_error 行为。
调用图:调用 1 个内部函数(default);外部调用 10 个(default, new, V4, new, new, assert!, assert_eq!, capture_events, find_event_by_name, state_for_settings)。