Codex 系统手册

Unified-exec 会话和 PTY/进程后端

stage-14.2.217 个文件

这一层是统一执行命令的“发动机舱”,属于幕后支撑。上面只说“启动命令、继续输入、拿输出、停掉它”,这里负责真把进程管住。mod 和错误文件定规矩、统一说法;process 和 manager 把本机进程、远端 exec-server 进程包成同一种东西,并登记、读写、清理。spawn 负责安全启动外部程序。exec-server 里的本地和远端进程负责实际跑命令、缓存事件。pty、pipe 和 Windows 后端则像不同插头,让普通命令和交互式终端在各系统上都能接通。

本阶段的文件17

Unified-exec 编排

这些文件定义 unified-exec 模块接口、错误模型、单进程封装器、管理器,以及驱动核心层交互式会话生命周期的 stdin 写入工具。

core/src/unified_exec/mod.rs源码 ↗
orchestrationcross-cutting / request handling

这个文件可以理解成一个“终端进程管理站”的入口说明牌。项目里有些功能需要启动命令,比如跑 shell、写入标准输入、继续读取输出,还要考虑用户批准、沙箱限制、网络权限、输出太长怎么办。真正启动和照看进程的细节分在旁边几个子文件里;这个文件负责把重要类型、默认数值和小工具集中暴露出来。它定义了执行时上下文 UnifiedExecContext,里面带着当前会话、当前回合和调用编号;也定义了请求结构,说明一次执行命令或写入输入需要哪些信息。ProcessStore 像一本登记簿,记着还活着的进程和预留编号。UnifiedExecProcessManager 则像前台柜员,持有这本登记簿,并规定后台终端最多等多久。文件里还有几个保护性规则:等待时间不能太短或太长,输出 token 数有默认值,返回给前端的数据块会有随机短编号。没有这些统一规则,不同执行路径很容易表现不一致,或者出现输出爆量、进程编号冲突、等待时间失控这类问题。

函数细节8
set_deterministic_process_ids_for_tests53–55 ↗
fn set_deterministic_process_ids_for_tests(enabled: bool)

作用:这个函数是给测试用的开关,用来让进程编号变得可预测。平时进程编号可能带随机性或运行时差异,测试如果依赖它就会不稳定,所以测试会打开这个模式。

数据流:进去的是一个布尔值:true 表示启用固定编号,false 表示恢复正常。它不自己生成编号,而是把这个开关转交给 process_manager 里的同名测试辅助函数。出来没有返回值,但会改变后续测试里分配进程编号的方式。

调用关系:它被测试辅助代码 set_deterministic_process_ids 调用,位置很靠外,像一个测试专用按钮。真正的活儿交给 process_manager::set_deterministic_process_ids_for_tests,这样外部测试不用知道内部文件怎么拆分。

调用图:调用 1 个内部函数(set_deterministic_process_ids_for_tests);被 1 处调用(set_deterministic_process_ids)。

UnifiedExecContext::new82–88 ↗
fn new(session: Arc<Session>, turn: Arc<TurnContext>, call_id: String) -> Self

作用:这个函数用来打包一次统一执行命令所需的基本背景信息。它把“是哪次会话、当前是哪一轮、这次工具调用叫什么编号”放进一个小盒子里,后面的执行流程都能带着它走。

数据流:进去的是 Session、TurnContext 和 call_id。Session 可以理解成一次对话的大环境,TurnContext 是当前这一轮请求的环境,call_id 是这次调用的标签。函数只是把三样东西原样装进 UnifiedExecContext,出来一个新的上下文对象,不额外修改别的状态。

调用关系:它会在 handle_call、exec_command_with_tty 以及相关测试场景里被调用。调用方先准备好会话和当前回合,再用它生成上下文,之后执行命令、申请权限、记录结果时就能统一知道自己属于哪次调用。

调用图:被 3 处调用(handle_call, exec_command_with_tty, failed_initial_end_for_unstored_process_uses_fallback_output)。

ProcessStore::remove128–131 ↗
fn remove(&mut self, process_id: i32) -> Option<ProcessEntry>

作用:这个函数从进程登记簿里移走一个进程。它不只是删掉进程记录,也会顺手取消这个进程编号的“已预留”状态,避免编号被误认为还在占用。

数据流:进去的是 process_id,也就是要删除的进程编号。函数先从 reserved_process_ids 这张预留名单里删除这个编号,再从 processes 这张进程表里取出并删除对应记录。出来的是可能存在的 ProcessEntry:如果原来有这个进程,就返回它;如果没有,就返回空。

调用关系:它被 prune_processes_if_needed 调用,也就是当系统需要清理过多或过旧进程时使用。清理流程负责决定删谁,ProcessStore::remove 负责把登记簿和预留名单同时整理干净。

调用图:被 1 处调用(prune_processes_if_needed)。

UnifiedExecProcessManager::new140–146 ↗
fn new(max_write_stdin_yield_time_ms: u64) -> Self

作用:这个函数创建一个新的统一执行进程管理器。调用者可以告诉它“写入标准输入后最多等多久”,它会保证这个时间至少不低于安全下限。

数据流:进去的是 max_write_stdin_yield_time_ms,表示写入输入后允许等待输出的最长毫秒数。函数新建一个带互斥锁的 ProcessStore;互斥锁可以理解成一把锁,防止多个异步任务同时改同一本进程登记簿。然后它把传入等待时间和 MIN_EMPTY_YIELD_TIME_MS 取较大值,避免空输入时等得太短。出来是一个 UnifiedExecProcessManager。

调用关系:它会被各种会话构造和测试辅助函数调用,也会被默认构造函数间接调用。它是管理器开始工作的第一步:先准备好进程登记簿和等待时间规则,后面的执行、复用、清理进程才有地方记账。

调用图:被 3 处调用(new, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx);外部调用 2 个(new, default)。

UnifiedExecProcessManager::default150–152 ↗
fn default() -> Self

作用:这个函数提供一个不用费心配置的默认进程管理器。多数地方只想要“按系统推荐值来”,就会用它。

数据流:进去没有参数。它使用 DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS 这个默认后台终端超时时间,调用 UnifiedExecProcessManager::new 创建管理器。出来是一个带默认设置的 UnifiedExecProcessManager。

调用关系:很多测试和执行场景会直接用它,比如验证沙箱目录、远程执行服务器、管道命令退出码等行为。它把默认值集中到 new 的创建流程里,避免各处重复写初始化逻辑。

调用图:被 7 处调用(unified_exec_uses_the_trusted_sandbox_cwd, zsh_fork_execpolicy_allow_preserves_parent_sandbox_override, zsh_fork_first_attempt_preserves_additional_permissions_request, zsh_fork_first_attempt_preserves_parent_sandbox_override, completed_pipe_commands_preserve_exit_code, remote_exec_server_rejects_inherited_fd_launches, unified_exec_uses_remote_exec_server_when_configured);外部调用 1 个(new)。

clamp_yield_time168–175 ↗
fn clamp_yield_time(yield_time_ms: u64) -> u64

作用:这个函数把“等命令输出多久”这个时间压到合理范围内。它防止等待时间太短导致刚启动就没输出,也防止太长导致用户一直干等。

数据流:进去的是 yield_time_ms,也就是请求希望等待的毫秒数。如果运行在 Windows 上,它会先确保初次等待不低于 Windows 专用下限,因为 Windows 启动交互式命令通常更慢。然后它把时间限制在 MIN_YIELD_TIME_MS 和 MAX_YIELD_TIME_MS 之间。出来的是修正后的等待时间。

调用关系:它被 exec_command 调用,处在真正执行命令前的准备阶段。exec_command 收到用户或模型给的等待时间后,会先让它校准,再继续启动进程和读取输出。

调用图:被 1 处调用(exec_command);外部调用 1 个(cfg!)。

resolve_max_tokens177–179 ↗
fn resolve_max_tokens(max_tokens: Option<usize>) -> usize

作用:这个函数决定最多返回多少输出 token。token 可以粗略理解成模型处理文字时的一小块单位;限制 token 数是为了避免命令输出太长,把响应撑爆。

数据流:进去的是一个可选数字 max_tokens。如果调用方给了数字,就使用调用方的限制;如果没给,就使用 DEFAULT_MAX_OUTPUT_TOKENS 这个默认值。出来是一个确定的 usize 数字,后面截断输出时可以直接使用。

调用关系:它被 truncate_code_mode_result 和 model_output_max_tokens 调用,属于输出整理阶段的小工具。前面的执行流程产生输出后,相关代码会用它确定该保留多少内容,再进行截断或返回。

调用图:被 2 处调用(truncate_code_mode_result, model_output_max_tokens)。

generate_chunk_id181–186 ↗
fn generate_chunk_id() -> String

作用:这个函数生成一个短的随机块编号。它用于给一次输出片段或执行结果片段贴标签,方便后续引用和区分。

数据流:进去没有参数。函数创建一个随机数生成器,然后连续生成 6 个十六进制字符,每个字符来自 0 到 15 的随机值。出来是一个 6 位字符串,比如类似“a3f09c”的短编号;它不改动进程状态。

调用关系:它会在 handle_call、exec_command 和 write_stdin 中被调用。也就是说,无论是首次执行命令,还是后来向进程写入输入,只要需要标记一段新输出,就可能通过它生成一个块编号。

调用图:被 3 处调用(handle_call, exec_command, write_stdin);外部调用 1 个(rng)。

core/src/unified_exec/errors.rs源码 ↗
data_modelcross-cutting

统一执行命令时,出错的地方很多:进程可能创建不了,运行中可能失败,可能找不到对应的进程编号,也可能写不进标准输入(stdin,就是给命令喂文字的入口),还可能被沙箱(一种安全隔离机制,防止命令乱碰系统)拒绝。这个文件把这些情况都收进 UnifiedExecError 这个错误类型里。这样一来,调用方不用猜“失败到底是哪种失败”,而是能拿到明确分类和可读的错误信息。里面还特别区分了模型看到的 session_id 和内部使用的 process_id,避免对外说法和内部追踪混在一起。最后的几个小函数是便捷入口,用来快速造出常见错误,像给错误贴上统一格式的标签。

函数细节3
UnifiedExecError::create_process29–31 ↗
fn create_process(message: String) -> Self

作用:这个函数用来生成“创建统一执行进程失败”的错误。有人启动命令环境失败时,会用它把失败原因包装成统一格式。

数据流:进去的是一段错误说明文字 message → 函数把这段文字放进 CreateProcess 这一类错误里 → 出来的是一个 UnifiedExecError,表示“进程一开始就没创建成功”,不会额外修改别的东西。

调用关系:它会在 open_session_with_exec_env 打开执行会话、准备启动进程时被用到。也就是说,流程刚开始建执行环境,如果建不起来,就通过这个函数把原因交给上层处理。

调用图:被 1 处调用(open_session_with_exec_env)。

UnifiedExecError::process_failed33–35 ↗
fn process_failed(message: String) -> Self

作用:这个函数用来生成“统一执行进程运行失败”的错误。它适合表示进程已经进入执行流程,但中途出了问题。

数据流:进去的是失败说明 message → 函数把它装进 ProcessFailed 这一类错误 → 出来的是一个标准化的 UnifiedExecError,告诉外面“进程执行阶段失败了”。

调用关系:它会被 writeexec_commandwrite_stdinfail_process_with_message 使用。也就是说,无论是在执行命令、给命令写输入,还是主动把进程标记为失败,只要需要表达“这个进程失败了”,都会走这个统一出口。

调用图:被 4 处调用(write, exec_command, write_stdin, fail_process_with_message)。

UnifiedExecError::sandbox_denied37–39 ↗
fn sandbox_denied(message: String, output: ExecToolCallOutput) -> Self

作用:这个函数用来生成“命令被沙箱拒绝”的错误。除了文字原因,它还保留命令输出,方便上层把被拒绝时发生了什么讲清楚。

数据流:进去的是拒绝原因 message 和执行输出 output → 函数把两者一起放进 SandboxDenied 错误里 → 出来的是一个既说明被拦截原因、又带有现场输出的 UnifiedExecError

调用关系:它会被 check_for_sandbox_denial_with_text 调用。那个流程负责检查输出文字里是否显示命令被沙箱拦住;一旦确认被拒绝,就用这个函数把拒绝信息和输出打包给后续错误处理流程。

调用图:被 1 处调用(check_for_sandbox_denial_with_text)。

core/src/unified_exec/process.rs源码 ↗
orchestrationrequest handling / process lifetime

这个文件像一个“统一遥控器”。底层命令可能是本机的 PTY 会话(PTY 可以理解成给程序伪造的终端窗口),也可能是 exec-server 提供的进程句柄,但上层只看到 UnifiedExecProcess。它保存进程状态、输出缓存、输出通知、取消信号和沙箱类型。命令有新输出时,后台任务会把字节放进 HeadTailBuffer,并广播给正在流式读取的人;命令退出、失败或输出关闭时,它会更新 ProcessState,并通知等待方。它还会在进程很快退出时检查是不是被沙箱拦了,避免把“权限被挡住”误当成普通失败。Drop 时自动 terminate,意思是对象没人用了就尽量把子进程收掉,防止留下孤儿进程。

函数细节27
SpawnLifecycle::inherited_fds42–44 ↗
fn inherited_fds(&self) -> Vec<i32>

作用:告诉启动子进程时,有哪些文件描述符要一起传给子进程。文件描述符可以理解成操作系统给打开文件、管道等资源发的号码。

数据流:进去的是具体的启动生命周期对象 → 默认什么都不检查,直接给出一个空列表 → 出来表示没有额外资源需要继承,也不会改动任何状态。

调用关系:这是 SpawnLifecycle 的默认行为。真正需要特殊启动资源的实现可以覆盖它;没有特殊需求时,UnifiedExecProcess 创建本机进程前后就可以用这个空实现安全地跳过额外处理。

调用图:外部调用 1 个(new)。

SpawnLifecycle::after_spawn46–46 ↗
fn after_spawn(&mut self)

作用:在子进程已经启动后,给调用方一个收尾机会。比如父进程可以在这里释放只为启动阶段临时保留的资源。

数据流:进去的是启动生命周期对象本身 → 默认什么都不做 → 出来没有返回值,也不改变状态。

调用关系:它和 inherited_fds 是一组钩子。启动前 inherited_fds 告诉哪些资源要保留,启动后 after_spawn 允许清理;NoopSpawnLifecycle 使用默认实现,表示不需要这些额外步骤。

UnifiedExecProcess::fmt92–98 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:给 UnifiedExecProcess 做调试打印时,显示几个最关键的信息:有没有退出、退出码、沙箱类型。

数据流:进去的是一个进程对象和格式化器 → 它读取 has_exited、exit_code、sandbox_type → 输出一段给开发者看的调试文本,不暴露内部所有字段。

调用关系:当日志或调试工具需要打印 UnifiedExecProcess 时会走这里。它把具体查询交给 has_exited 和 exit_code,避免重复写判断本机进程和 exec-server 进程的逻辑。

调用图:调用 2 个内部函数(exit_code, has_exited);外部调用 1 个(debug_struct)。

UnifiedExecProcess::new102–131 ↗
fn new(
        process_handle: ProcessHandle,
        sandbox_type: SandboxType,
        spawn_lifecycle: Option<SpawnLifecycleHandle>,
    ) -> Self

作用:创建一个统一进程对象的空壳,把输出缓存、通知器、状态频道、取消信号这些基础零件都装好。

数据流:进去的是底层进程句柄、沙箱类型、可选的启动生命周期对象 → 它新建输出缓存、广播频道、状态 watch 频道、取消令牌等共享工具 → 出来一个还没挂上输出读取任务的 UnifiedExecProcess。

调用关系:from_spawned 和 from_exec_server_started 都先调用它搭好通用骨架,然后再根据本机或 exec-server 的不同来源,分别启动对应的输出收集后台任务。

调用图:调用 1 个内部函数(default);外部调用 8 个(new, new, new, new, new, channel, default, channel)。

UnifiedExecProcess::write133–156 ↗
async fn write(&self, data: &[u8]) -> Result<(), UnifiedExecError>

作用:把用户输入写进正在运行的命令,就像往终端里敲字或把数据送进标准输入。

数据流:进去的是一段字节数据 → 如果是本机进程,就发给本机 PTY 的写入通道;如果是 exec-server 进程,就调用远端写入接口 → 成功返回空结果;如果进程不存在、标准输入关闭或写入失败,就更新退出/取消状态并返回错误。

调用关系:上层需要给命令喂输入时调用它。它根据 ProcessHandle 自动分流:本机走 writer_sender,exec-server 走 process_handle.write;当远端说进程已不可写时,它会顺手通知整个统一执行流程停止等待。

调用图:调用 1 个内部函数(process_failed);外部调用 3 个(cancel, send_replace, borrow)。

UnifiedExecProcess::output_handles158–166 ↗
fn output_handles(&self) -> OutputHandles

作用:把读取输出所需的共享工具打包交出去,包括输出缓存、通知器、关闭标记和取消信号。

数据流:进去的是进程对象 → 它克隆这些共享句柄,克隆的是引用而不是复制整份数据 → 出来一个 OutputHandles,别人可以用它等待新输出或知道输出已经结束。

调用关系:from_exec_server_started 用它把输出相关零件交给 spawn_exec_server_output_task。这样后台任务和外部轮询/流式读取的人看的是同一份输出状态。

调用图:外部调用 2 个(clone, clone)。

UnifiedExecProcess::output_receiver168–170 ↗
fn output_receiver(&self) -> tokio::sync::broadcast::Receiver<Vec<u8>>

作用:给想实时看输出的人开一个新的订阅口。以后进程每产生一块输出,这个订阅者就能收到一份。

数据流:进去的是进程对象 → 它从广播频道上新建一个接收器 → 出来一个可以接收 Vec<u8> 输出块的 receiver,不改变已有订阅者。

调用关系:start_streaming_output 会调用它来开始流式输出。后台输出任务负责往 output_tx 里发数据,这个函数只是给消费者接上一根新“收音机天线”。

调用图:被 1 处调用(start_streaming_output);外部调用 1 个(subscribe)。

UnifiedExecProcess::cancellation_token172–174 ↗
fn cancellation_token(&self) -> CancellationToken

作用:把这个进程的取消信号交给外部。取消信号可以理解成一面旗子,竖起来后大家都知道该停了。

数据流:进去的是进程对象 → 它克隆 CancellationToken 句柄 → 出来一个指向同一取消状态的令牌,不会新建独立取消状态。

调用关系:start_streaming_output 会用它监听进程是否被取消。finish_termination、signal_exit 或错误路径会触发 cancel,流式读取方看到后就能及时收尾。

调用图:被 1 处调用(start_streaming_output);外部调用 1 个(clone)。

UnifiedExecProcess::output_drained_notify176–178 ↗
fn output_drained_notify(&self) -> Arc<Notify>

作用:提供一个通知器,用来告诉别人“输出已经被读干净了”这类事件。

数据流:进去的是进程对象 → 它克隆 output_drained 这个共享 Notify → 出来一个可等待的通知句柄,不直接改变状态。

调用关系:start_streaming_output 会拿到这个通知器,在流式输出流程里配合关闭和取消信号使用,避免上层过早认为输出处理完了。

调用图:被 1 处调用(start_streaming_output);外部调用 1 个(clone)。

UnifiedExecProcess::has_exited180–186 ↗
fn has_exited(&self) -> bool

作用:判断命令是不是已经结束。

数据流:进去的是进程对象 → 它先看统一保存的 ProcessState;如果是本机进程,还会问本机 PTY 句柄自己的退出状态 → 出来一个布尔值,true 表示已经退出。

调用关系:fmt 用它展示调试状态,check_for_sandbox_denial_with_text 用它避免在进程还没结束时误判沙箱错误。它把本机和 exec-server 的差异藏在内部。

调用图:被 2 处调用(check_for_sandbox_denial_with_text, fmt);外部调用 1 个(borrow)。

UnifiedExecProcess::exit_code188–196 ↗
fn exit_code(&self) -> Option<i32>

作用:拿到命令退出时的退出码。退出码通常是程序结束后给系统的数字,0 多半代表成功,非 0 多半代表失败。

数据流:进去的是进程对象 → 它读取统一状态里的退出码;如果是本机进程且统一状态还没有,就再问本机进程句柄 → 出来 Option<i32>,有值表示知道退出码,没值表示还不知道。

调用关系:fmt、terminate_confirmed、check_for_sandbox_denial_with_text 都会用它。尤其沙箱判断需要退出码和输出一起看,才能判断是不是权限拦截。

调用图:被 3 处调用(check_for_sandbox_denial_with_text, fmt, terminate_confirmed);外部调用 1 个(borrow)。

UnifiedExecProcess::finish_termination198–205 ↗
fn finish_termination(&self)

作用:做停止进程后的统一收尾:标记输出关闭、通知等待者、取消相关任务、停止输出后台任务。

数据流:进去的是进程对象 → 它把 output_closed 设为 true,唤醒等待输出关闭的人,触发取消令牌,并中止 output_task → 出来没有返回值,但共享状态已变成“该结束了”。

调用关系:terminate 和 terminate_confirmed 都在真正发出终止请求后调用它。它不负责杀进程本身,只负责把统一执行框架里的等待和输出读取都收住。

调用图:被 2 处调用(terminate, terminate_confirmed);外部调用 1 个(cancel)。

UnifiedExecProcess::terminate207–218 ↗
fn terminate(&self)

作用:请求停止这个命令。它是“尽快关掉”的版本,不等待远端一定确认完成。

数据流:进去的是进程对象 → 本机进程直接调用 terminate;exec-server 进程则另起一个异步任务去调用远端 terminate → 随后立刻执行 finish_termination,标记本地这边结束等待。

调用关系:Drop、fail_and_terminate、fail_process_with_message 都会走到它。它适合清理资源或遇到错误时快速收场;如果需要确认远端终止结果,则用 terminate_confirmed。

调用图:调用 1 个内部函数(finish_termination);被 3 处调用(drop, fail_and_terminate, fail_process_with_message);外部调用 2 个(clone, spawn)。

UnifiedExecProcess::terminate_confirmed220–233 ↗
async fn terminate_confirmed(&self) -> Result<(), UnifiedExecError>

作用:停止这个命令,并且对 exec-server 进程等待终止请求完成,适合需要明确知道停止动作是否成功的场景。

数据流:进去的是进程对象 → 本机进程发终止;exec-server 进程 await 远端 terminate,失败就转成 UnifiedExecError → 然后记录退出状态、执行统一收尾 → 成功返回 Ok。

调用关系:它内部会调用 exit_code、signal_exit 和 finish_termination。和 terminate 相比,它更谨慎,适合上层希望“确认已经通知到远端”的流程。

调用图:调用 3 个内部函数(exit_code, finish_termination, signal_exit)。

UnifiedExecProcess::interrupt235–245 ↗
async fn interrupt(&self) -> Result<(), UnifiedExecError>

作用:给进程发送中断信号,类似用户在终端按 Ctrl+C。它通常是请求程序自己停下来,而不是强行杀掉。

数据流:进去的是进程对象 → 本机进程发送 PTY 的 Interrupt 信号;exec-server 进程发送远端定义的 Interrupt 信号 → 成功返回 Ok,失败返回带错误信息的 UnifiedExecError。

调用关系:当用户想打断正在跑的命令但还希望程序有机会自己清理时,会用它。它不调用 finish_termination,因为中断后程序不一定马上退出。

UnifiedExecProcess::fail_and_terminate247–253 ↗
fn fail_and_terminate(&self, message: String)

作用:把进程标记为失败,并立刻停止它。适合框架发现某个不可恢复问题时使用。

数据流:进去的是一条失败消息 → 它读取当前 ProcessState,如果还没有失败消息,就写入这条消息 → 然后调用 terminate 停掉进程并关闭输出等待。

调用关系:fail_process_with_message 会调用它。它把“记录为什么失败”和“收掉进程”绑在一起,避免只报错不清理,或只清理却丢失失败原因。

调用图:调用 1 个内部函数(terminate);被 1 处调用(fail_process_with_message);外部调用 2 个(send_replace, borrow)。

UnifiedExecProcess::snapshot_output255–258 ↗
async fn snapshot_output(&self) -> Vec<Vec<u8>>

作用:拍一张当前输出缓存的快照,给后续检查使用。

数据流:进去的是进程对象 → 它锁住输出缓存,取出缓存里的输出块列表 → 出来 Vec<Vec<u8>>,不会清空原缓存。

调用关系:check_for_sandbox_denial 会调用它,把已经收集到的输出拼成文本,再交给沙箱拒绝判断逻辑。锁的作用是防止后台输出任务同时改缓存。

调用图:被 1 处调用(check_for_sandbox_denial);外部调用 1 个(lock)。

UnifiedExecProcess::sandbox_type260–262 ↗
fn sandbox_type(&self) -> SandboxType

作用:返回这个进程运行时使用的沙箱类型。沙箱可以理解成限制程序权限的安全围栏。

数据流:进去的是进程对象 → 它读取保存的 sandbox_type 字段 → 出来一个 SandboxType,不改变任何状态。

调用关系:check_for_sandbox_denial_with_text 会用它决定是否需要做沙箱拒绝判断。如果没有沙箱,就没必要把失败往权限拦截方向解释。

调用图:被 1 处调用(check_for_sandbox_denial_with_text)。

UnifiedExecProcess::failure_message264–266 ↗
fn failure_message(&self) -> Option<String>

作用:取出当前记录的失败原因,如果有的话。

数据流:进去的是进程对象 → 它读取 ProcessState 里的 failure_message 并克隆一份 → 出来 Option<String>,有值就是失败说明,没值就是还没有记录失败。

调用关系:fail_process_with_message 会用它判断是否已经有失败信息,避免重复覆盖或重复处理。后台输出任务遇到 exec-server 读失败时也会把失败消息写进同一份状态。

调用图:被 1 处调用(fail_process_with_message);外部调用 1 个(borrow)。

UnifiedExecProcess::check_for_sandbox_denial268–282 ↗
async fn check_for_sandbox_denial(&self) -> Result<(), UnifiedExecError>

作用:检查刚启动或刚退出的命令,是不是因为沙箱权限限制而失败。

数据流:进去的是进程对象 → 它最多等 20 毫秒让早期输出到达,然后读取输出快照,把字节转成文本 → 再交给 check_for_sandbox_denial_with_text 判断;如果像沙箱拒绝,就返回专门的错误。

调用关系:from_spawned 和 from_exec_server_started 在进程很快退出时会调用它。这样可以把“普通命令失败”和“被安全围栏拦住”区分开,给用户更准确的提示。

调用图:调用 2 个内部函数(check_for_sandbox_denial_with_text, snapshot_output);外部调用 4 个(from_millis, from_utf8_lossy, new, timeout)。

UnifiedExecProcess::check_for_sandbox_denial_with_text284–313 ↗
async fn check_for_sandbox_denial_with_text(
        &self,
        text: &str,
    ) -> Result<(), UnifiedExecError>

作用:用给定的输出文字、退出码和沙箱类型,判断失败是不是很像沙箱拒绝。

数据流:进去的是一段输出文本 → 它先确认确实有沙箱且进程已经退出;再组装 ExecToolCallOutput,调用 is_likely_sandbox_denied 判断 → 如果命中,就把输出截短成适合展示的提示并返回 sandbox_denied 错误;否则返回 Ok。

调用关系:check_for_sandbox_denial 负责收集文本后调用它。它还会调用 sandbox_type、has_exited、exit_code 获取判断条件,并用 formatted_truncate_text 避免错误消息过长。

调用图:调用 6 个内部函数(is_likely_sandbox_denied, sandbox_denied, exit_code, has_exited, sandbox_type, new);被 1 处调用(check_for_sandbox_denial);外部调用 4 个(default, formatted_truncate_text, format!, Tokens)。

UnifiedExecProcess::from_spawned315–373 ↗
async fn from_spawned(
        spawned: SpawnedPty,
        sandbox_type: SandboxType,
        spawn_lifecycle: SpawnLifecycleHandle,
    ) -> Result<Self, UnifiedExecError>

作用:把一个已经在本机启动好的 PTY 进程,接进统一执行框架。

数据流:进去的是 SpawnedPty、沙箱类型和启动生命周期对象 → 它合并 stdout/stderr 输出,创建 UnifiedExecProcess,启动本机输出收集任务 → 接着检查进程是否已经很快退出;如果已退出就记录退出码并做沙箱检查;否则启动后台任务等待未来退出 → 出来可供上层统一操作的进程对象。

调用关系:open_session_with_exec_env 会在本机启动命令后调用它。它使用 new 搭骨架,用 spawn_local_output_task 接输出,并在早退场景里调用 check_for_sandbox_denial。

调用图:被 1 处调用(open_session_with_exec_env);外部调用 8 个(clone, new, new, spawn_local_output_task, combine_output_receivers, Local, spawn, timeout)。

UnifiedExecProcess::from_exec_server_started375–408 ↗
async fn from_exec_server_started(
        started: StartedExecProcess,
        sandbox_type: SandboxType,
    ) -> Result<Self, UnifiedExecError>

作用:把 exec-server 返回的已启动进程,接进统一执行框架。

数据流:进去的是 StartedExecProcess 和沙箱类型 → 它创建 exec-server 类型的 ProcessHandle,创建 UnifiedExecProcess,启动远端输出读取任务 → 然后短暂等待早期退出或失败事件;如果很快结束,就检查沙箱拒绝 → 出来统一进程对象。

调用关系:open_session_with_exec_env、blocking_terminate_unified_process、remote_process 等流程会调用它。它用 spawn_exec_server_output_task 把远端 read 接口转换成本文件统一的输出缓存、广播和状态更新。

调用图:被 4 处调用(blocking_terminate_unified_process, open_session_with_exec_env, remote_process, remote_process_waits_for_early_exit_event);外部调用 5 个(clone, new, spawn_exec_server_output_task, ExecServer, timeout)。

UnifiedExecProcess::spawn_exec_server_output_task410–497 ↗
fn spawn_exec_server_output_task(
        started: StartedExecProcess,
        output_handles: OutputHandles,
        output_tx: broadcast::Sender<Vec<u8>>,
        state_tx: watch::Sender<ProcessStat

作用:启动一个后台任务,持续从 exec-server 读取输出和状态变化。

数据流:进去的是远端 started 进程、输出共享句柄、广播发送器、状态发送器 → 后台循环调用 process.read,拿到输出块就写入缓存并广播;看到 failure 就记录失败并关闭;看到 exited 就写退出码;看到 closed 或读出错就关闭输出并取消 → 出来一个 JoinHandle,用来代表这个后台任务。

调用关系:from_exec_server_started 创建远端统一进程时会调用它。它是 exec-server 到 UnifiedExecProcess 的“翻译员”:把远端协议里的 chunks、exited、closed、failure 翻译成本地共享状态和通知。

调用图:外部调用 4 个(borrow, send, send_replace, spawn)。

UnifiedExecProcess::spawn_local_output_task499–526 ↗
fn spawn_local_output_task(
        mut receiver: tokio::sync::broadcast::Receiver<Vec<u8>>,
        buffer: OutputBuffer,
        output_notify: Arc<Notify>,
        output_closed: Arc<AtomicBool>,

作用:启动一个后台任务,持续接收本机 PTY 的输出,并放进统一输出系统。

数据流:进去的是本机输出接收器、缓存、通知器、关闭标记和广播发送器 → 后台循环 recv 输出块,收到就写缓存、广播、通知等待者;如果接收落后就跳过丢失提示继续;如果通道关闭,就标记输出关闭并通知 → 出来一个 JoinHandle。

调用关系:from_spawned 会调用它。它把本机 stdout/stderr 合并后的广播流,转成 UnifiedExecProcess 对外统一提供的缓存快照和实时输出订阅。

调用图:调用 1 个内部函数(recv);外部调用 3 个(lock, send, spawn)。

UnifiedExecProcess::signal_exit528–532 ↗
fn signal_exit(&self, exit_code: Option<i32>)

作用:在统一状态里记录“进程已经退出”,并通知相关等待流程可以停止。

数据流:进去的是可选退出码 → 它读取当前 ProcessState,把它改成 exited 状态并写回 watch 频道 → 然后触发取消令牌;出来没有返回值,但等待状态变化的人会被唤醒。

调用关系:terminate_confirmed 会调用它来补齐退出状态。本机和 exec-server 的后台等待逻辑也会用类似方式更新 state_tx,让 has_exited 和 exit_code 能看到结果。

调用图:被 1 处调用(terminate_confirmed);外部调用 3 个(cancel, send_replace, borrow)。

UnifiedExecProcess::drop536–538 ↗
fn drop(&mut self)

作用:当 UnifiedExecProcess 对象被销毁时,自动尝试停止底层进程,防止程序没人管还继续跑。

数据流:进去的是即将被释放的进程对象 → 它调用 terminate → 结果是底层进程收到终止请求,输出等待和取消信号也被收尾。

调用关系:这是 Rust 的 Drop 自动清理钩子,调用方通常不用手动调用。它把资源安全兜底放在对象生命周期末尾,避免忘记清理造成孤儿进程。

调用图:调用 1 个内部函数(terminate)。

core/src/unified_exec/process_manager.rs源码 ↗
orchestrationrequest handling / background process lifecycle / teardown

可以把这个文件理解成一个“后台终端值班室”。用户发来一条命令后,它先准备环境变量,走沙箱和审批流程,再真正启动进程。进程如果很快结束,就立刻返回结果;如果还在跑,就把它登记到 ProcessStore 里,后续可以继续轮询输出、往里面写输入,或者手动终止。它还会处理网络权限这种特殊情况:如果网络后来被拒,就把进程停掉并返回清楚的错误。输出不是一次性全吞,而是在限定时间内收集一段,避免命令卡住整个系统。文件里也有“清理机制”:进程太多时会淘汰旧的,退出后会注销网络审批,关闭时能一口气终止所有后台进程。

函数细节37
set_deterministic_process_ids_for_tests85–87 ↗
fn set_deterministic_process_ids_for_tests(enabled: bool)

作用:这是给测试用的开关,用来强制进程 ID 按固定规律生成。这样测试结果稳定,不会因为随机 ID 变化而难以验证。

数据流:进去一个布尔值 enabled → 它把这个值写进全局原子标记(一种可安全跨线程读取的小开关)→ 之后分配进程 ID 时会看到这个设置。

调用关系:它只服务测试场景;后面的 should_use_deterministic_process_ids 会读取这个开关,allocate_process_id 再根据结果决定用固定 ID 还是随机 ID。

调用图:被 1 处调用(set_deterministic_process_ids_for_tests)。

deterministic_process_ids_forced_for_tests89–91 ↗
fn deterministic_process_ids_forced_for_tests() -> bool

作用:读取测试开关,判断现在是否被强制使用固定进程 ID。它本身不改状态,只负责回答“开了吗”。

数据流:进去没有参数 → 从全局原子标记里读取当前值 → 返回 true 或 false。

调用关系:它被 should_use_deterministic_process_ids 调用,是测试覆盖生产行为时的一层小入口。

调用图:被 1 处调用(should_use_deterministic_process_ids)。

should_use_deterministic_process_ids93–95 ↗
fn should_use_deterministic_process_ids() -> bool

作用:判断进程 ID 要不要走可预测模式。测试时需要可预测,正式运行时通常要随机,减少碰撞和猜测。

数据流:进去没有参数 → 检查当前是不是测试编译环境,并读取测试强制开关 → 返回是否使用固定规律生成 ID。

调用关系:allocate_process_id 每次要分配新 ID 前都会问它;它再去问 deterministic_process_ids_forced_for_tests。

调用图:调用 1 个内部函数(deterministic_process_ids_forced_for_tests);被 1 处调用(allocate_process_id);外部调用 1 个(cfg!)。

apply_unified_exec_env97–102 ↗
fn apply_unified_exec_env(mut env: HashMap<String, String>) -> HashMap<String, String>

作用:给命令执行环境补上一组固定环境变量,让命令输出更稳定、更适合机器读取。比如关颜色、禁分页器、指定语言环境。

数据流:进去一份环境变量表 → 把 NO_COLOR、TERM、PAGER 等统一设置写进去,覆盖同名项 → 返回补好的环境变量表。

调用关系:open_session_with_sandbox 在真正启动命令前会调用它,保证所有统一执行的命令都有一致的基础环境。

调用图:被 1 处调用(open_session_with_sandbox)。

exec_env_policy_from_shell_policy104–122 ↗
fn exec_env_policy_from_shell_policy(
    policy: &ShellEnvironmentPolicy,
) -> codex_exec_server::ExecEnvPolicy

作用:把项目自己的 shell 环境策略转换成执行服务器能理解的环境策略。简单说,就是把“哪些变量继承、排除、指定”翻译成另一套格式。

数据流:进去 ShellEnvironmentPolicy → 拷贝继承规则、排除列表、强制设置、只包含列表 → 出来 codex_exec_server 使用的 ExecEnvPolicy。

调用关系:open_session_with_sandbox 会用它准备远程或执行服务器启动命令时的环境规则。

调用图:被 1 处调用(open_session_with_sandbox)。

env_overlay_for_exec_server124–133 ↗
fn env_overlay_for_exec_server(
    request_env: &HashMap<String, String>,
    local_policy_env: &HashMap<String, String>,
) -> HashMap<String, String>

作用:找出请求环境里相对本地策略环境真正变动过的变量。这样发给执行服务器时只传“额外差异”,不重复传一整份相同环境。

数据流:进去请求环境和本地策略生成的环境 → 逐项比较,保留值不同或本地没有的变量 → 返回一张差异环境表。

调用关系:exec_server_env_for_request 调用它,用来为执行服务器准备更干净的环境覆盖层。

调用图:被 1 处调用(exec_server_env_for_request)。

exec_server_env_for_request135–149 ↗
fn exec_server_env_for_request(
    request: &ExecRequest,
) -> (
    Option<codex_exec_server::ExecEnvPolicy>,
    HashMap<String, String>,
)

作用:为执行服务器整理环境配置。它决定是带着环境策略加差异变量启动,还是直接使用请求里的完整环境。

数据流:进去 ExecRequest → 如果请求里有执行服务器环境配置,就生成策略和差异环境;否则直接克隆请求环境 → 返回“可选策略 + 环境变量表”。

调用关系:exec_server_params_for_request 会调用它,把结果塞进发送给执行服务器的启动参数。

调用图:调用 1 个内部函数(env_overlay_for_exec_server);被 1 处调用(exec_server_params_for_request)。

exec_server_params_for_request151–167 ↗
fn exec_server_params_for_request(
    process_id: i32,
    request: &ExecRequest,
    tty: bool,
) -> codex_exec_server::ExecParams

作用:把一次命令请求打包成执行服务器需要的启动参数。执行服务器可以理解这包东西后,才知道跑什么、在哪跑、用什么环境。

数据流:进去进程 ID、执行请求和是否使用 tty(伪终端,模拟真实终端)→ 转换进程 ID、命令、目录 URI、环境策略等字段 → 返回 ExecParams。

调用关系:open_session_with_exec_env 在远程环境启动命令时调用它,然后把参数交给执行服务器后端。

调用图:调用 3 个内部函数(exec_server_env_for_request, exec_server_process_id, from_abs_path);被 1 处调用(open_session_with_exec_env)。

InitialExecCommandGuard::drop191–193 ↗
fn drop(&mut self)

作用:这是一个自动收尾的小守卫。初始 exec_command 调用结束时,它会把“初始调用仍在活跃”的标记关掉。

数据流:对象被释放时触发 → 把 active 原子布尔值设为 false → 后续终止进程时就知道初始调用已经结束,可以安全移除记录。

调用关系:exec_command 存储后台进程时创建它;Rust 自动析构时调用 drop,terminate_process 会读取这个标记避免过早删除。

exec_server_process_id196–198 ↗
fn exec_server_process_id(process_id: i32) -> String

作用:把内部使用的数字进程 ID 转成执行服务器需要的字符串形式。它是一个很小的格式转换函数。

数据流:进去 i32 数字 ID → 调用转字符串 → 出来 String。

调用关系:exec_server_params_for_request 打包远程执行参数时会调用它。

调用图:被 1 处调用(exec_server_params_for_request)。

unregister_network_approval_for_entry200–210 ↗
async fn unregister_network_approval_for_entry(entry: &ProcessEntry)

作用:注销某个进程登记过的网络审批。这样进程结束或被移除后,权限系统里不会留下无用的挂账。

数据流:进去一个进程记录 ProcessEntry → 如果里面有网络审批且还能找到会话,就向网络审批服务注销调用 ID → 没有返回业务数据,只完成清理。

调用关系:release_process_id、store_process 淘汰旧进程、terminate_all_processes 和 terminate_process 都会在移除进程时调用它。

调用图:被 4 处调用(release_process_id, store_process, terminate_all_processes, terminate_process)。

finish_network_approval_after_process_exit_for_entry212–221 ↗
async fn finish_network_approval_after_process_exit_for_entry(
    entry: &ProcessEntry,
) -> Result<(), String>

作用:在进程退出后完成它的网络审批收尾。它把 ProcessEntry 里的会话和延期审批取出来,交给统一收尾函数。

数据流:进去进程记录 → 尝试恢复会话引用,并取出网络审批信息 → 返回成功,或返回可展示的错误字符串。

调用关系:write_stdin 发现后台进程已经退出时会调用它,内部继续交给 finish_deferred_network_approval_after_process_exit_for_session。

调用图:调用 1 个内部函数(finish_deferred_network_approval_after_process_exit_for_session);被 1 处调用(write_stdin)。

finish_deferred_network_approval_for_session223–233 ↗
async fn finish_deferred_network_approval_for_session(
    session: Option<&Arc<crate::session::session::Session>>,
    deferred: Option<DeferredNetworkApproval>,
) -> Result<(), String>

作用:完成一次延期网络审批。延期审批就是命令先跑着,但网络权限结果可能稍后才出来。

数据流:进去可选会话和可选延期审批 → 没有会话就直接成功;有会话就调用网络审批收尾函数 → 返回成功或错误文字。

调用关系:exec_command、write_stdin 和进程退出后的收尾函数都会用它,确保网络权限流程最终有结果。

调用图:调用 1 个内部函数(finish_deferred_network_approval);被 3 处调用(exec_command, write_stdin, finish_deferred_network_approval_after_process_exit_for_session)。

network_approval_error_message235–240 ↗
fn network_approval_error_message(err: ToolError) -> String

作用:把网络审批失败的错误变成普通人能看的字符串。不同错误类型在这里统一变成一段消息。

数据流:进去 ToolError → 如果是用户拒绝,就取拒绝说明;如果是系统错误,就转成字符串 → 返回错误消息。

调用关系:network_denial_message_for_session 在网络被拒时调用它,用来生成最终展示给用户的原因。

调用图:被 1 处调用(network_denial_message_for_session);外部调用 1 个(to_string)。

network_denial_message_for_session242–253 ↗
async fn network_denial_message_for_session(
    session: Option<&Arc<crate::session::session::Session>>,
    deferred: Option<DeferredNetworkApproval>,
) -> String

作用:生成“网络访问被拒”的最终提示。它会尽量从审批系统拿到更具体的拒绝原因,拿不到就用默认文案。

数据流:进去可选会话和可选延期审批 → 如果没有会话,返回默认拒绝消息;否则尝试完成审批 → 成功时返回默认消息,失败时返回具体错误。

调用关系:exec_command、write_stdin 和 terminate_process_on_network_denial 都会在网络取消后调用它,让错误信息一致。

调用图:调用 2 个内部函数(finish_deferred_network_approval, network_approval_error_message);被 3 处调用(exec_command, write_stdin, terminate_process_on_network_denial)。

wait_for_late_network_denial255–267 ↗
async fn wait_for_late_network_denial(network_cancelled: Option<CancellationToken>) -> bool

作用:给“进程刚退出但网络拒绝信号稍晚到达”的情况留一点缓冲时间。这样不会把网络拒绝误判成普通退出。

数据流:进去可选取消令牌 CancellationToken(一种异步取消信号)→ 如果已经取消就返回 true;否则最多等 100 毫秒 → 返回这段时间内是否收到拒绝。

调用关系:finish_deferred_network_approval_after_process_exit_for_session 会先调用它,再真正完成网络审批收尾。

调用图:被 1 处调用(finish_deferred_network_approval_after_process_exit_for_session);外部调用 1 个(select!)。

finish_deferred_network_approval_after_process_exit_for_session269–280 ↗
async fn finish_deferred_network_approval_after_process_exit_for_session(
    session: Option<&Arc<crate::session::session::Session>>,
    deferred: Option<DeferredNetworkApproval>,
) -> Result<(), St

作用:进程退出后,专门处理延期网络审批的收尾。它会先等一小会儿,防止网络拒绝信号晚到。

数据流:进去可选会话和延期审批 → 从审批里取取消信号并短暂等待 → 再调用 finish_deferred_network_approval_for_session → 返回成功或错误消息。

调用关系:exec_command 和 finish_network_approval_after_process_exit_for_entry 会调用它,覆盖短命令和后台命令退出两种场景。

调用图:调用 2 个内部函数(finish_deferred_network_approval_for_session, wait_for_late_network_denial);被 2 处调用(exec_command, finish_network_approval_after_process_exit_for_entry)。

fail_process_with_message282–290 ↗
fn fail_process_with_message(process: &UnifiedExecProcess, message: String) -> UnifiedExecError

作用:把一个进程标记为失败,并尽量终止它。它保证外层拿到的是统一的“进程失败”错误。

数据流:进去进程对象和错误消息 → 如果进程已有失败消息,就终止并使用旧消息;否则写入失败消息并终止 → 返回 UnifiedExecError。

调用关系:exec_command 和 write_stdin 在网络拒绝、审批失败等场景调用它,把进程状态和返回错误对齐。

调用图:调用 4 个内部函数(process_failed, fail_and_terminate, failure_message, terminate);被 2 处调用(exec_command, write_stdin)。

emit_failed_initial_exec_end_if_unstored293–320 ↗
async fn emit_failed_initial_exec_end_if_unstored(
    process_started_alive: bool,
    context: &UnifiedExecContext,
    request: &ExecCommandRequest,
    cwd: AbsolutePathBuf,
    transcript: Arc<to

作用:如果命令还没来得及存成后台进程就失败了,它负责补发一条“执行失败结束”事件。这样前端或日志不会只看到开始,看不到结束。

数据流:进去进程是否曾活着、上下文、请求、目录、输出记录、错误消息和耗时 → 如果进程已存活登记就不做事;否则发失败结束事件 → 没有返回值。

调用关系:exec_command 在初始启动失败、网络拒绝或审批失败时调用它;真正发事件的工作交给 emit_failed_exec_end_for_unified_exec。

调用图:调用 1 个内部函数(emit_failed_exec_end_for_unified_exec);被 1 处调用(exec_command);外部调用 1 个(clone)。

terminate_process_on_network_denial322–343 ↗
fn terminate_process_on_network_denial(
    process: Arc<UnifiedExecProcess>,
    session: std::sync::Weak<crate::session::session::Session>,
    deferred: DeferredNetworkApproval,
)

作用:启动一个后台监听任务:只要网络审批后来被拒,就终止对应进程。这样命令不能在失去网络许可后继续偷偷运行。

数据流:进去进程、弱会话引用和延期网络审批 → 后台等待网络取消或进程退出;如果确认被拒,生成拒绝消息 → 调用进程失败并终止。

调用关系:exec_command 在启动进程且存在延期网络审批时调用它;它内部用 network_denial_message_for_session 生成错误文案。

调用图:调用 2 个内部函数(cancellation_token, network_denial_message_for_session);被 1 处调用(exec_command);外部调用 4 个(as_ref, upgrade, select!, spawn)。

UnifiedExecProcessManager::allocate_process_id346–371 ↗
async fn allocate_process_id(&self) -> i32

作用:分配一个当前没有被占用的进程 ID。这个 ID 是后续轮询、写输入、终止进程时的“号码牌”。

数据流:进去 manager 自身 → 加锁查看已保留 ID;测试模式按顺序生成,正式模式随机生成;如果冲突就重试 → 记录并返回新 ID。

调用关系:命令启动前会用它拿 ID;它依赖 should_use_deterministic_process_ids 判断生成方式。

调用图:调用 1 个内部函数(should_use_deterministic_process_ids);外部调用 1 个(rng)。

UnifiedExecProcessManager::release_process_id373–381 ↗
async fn release_process_id(&self, process_id: i32)

作用:释放一个进程 ID,并移除对应进程记录。它也会顺手清理网络审批登记,避免资源泄漏。

数据流:进去 process_id → 从进程仓库里删除对应记录 → 如果确实删到了,就注销网络审批 → 不返回数据。

调用关系:exec_command 和 write_stdin 在命令失败、短命令结束或进程不可继续时调用它;清理细节交给 unregister_network_approval_for_entry。

调用图:调用 1 个内部函数(unregister_network_approval_for_entry);被 2 处调用(exec_command, write_stdin)。

UnifiedExecProcessManager::exec_command383–614 ↗
async fn exec_command(
        &self,
        request: ExecCommandRequest,
        context: &UnifiedExecContext,
    ) -> Result<ExecCommandToolOutput, UnifiedExecError>

作用:执行一条新命令,是这个文件最核心的入口之一。它负责启动命令、发开始事件、收集第一段输出,并决定命令是结束还是转入后台。

数据流:进去 ExecCommandRequest 和执行上下文 → 打开沙箱会话、启动进程、开始流式输出、按 yield 时间收集输出、处理网络拒绝和失败 → 返回一份工具输出,里面有输出片段、耗时、进程 ID 或退出码。

调用关系:上层工具调用会进入这里;它会调用 open_session_with_sandbox、store_process、collect_output_until_deadline、refresh_process_state 等,把启动、登记、输出和收尾串成一条完整流程。

调用图:调用 18 个内部函数(unified_exec, new, emit_exec_end_for_unified_exec, start_streaming_output, clamp_yield_time, process_failed, generate_chunk_id, default, open_session_with_sandbox, refresh_process_state (+8 more));外部调用 10 个(clone, downgrade, new, new, from_millis, now, collect_output_until_deadline, from_utf8_lossy, approx_token_count, new)。

UnifiedExecProcessManager::write_stdin616–769 ↗
async fn write_stdin(
        &self,
        request: WriteStdinRequest<'_>,
    ) -> Result<ExecCommandToolOutput, UnifiedExecError>

作用:给已经在后台运行的进程写输入,或者只是不写输入、单纯轮询它的新输出。交互式终端靠它继续对话。

数据流:进去 WriteStdinRequest → 找到进程句柄;如果有输入就写入或发送中断;然后等待一段时间收集新输出;检查网络和失败状态;刷新进程是否退出 → 返回本次输出片段、退出码和可能仍可继续使用的进程 ID。

调用关系:用户后续操作后台终端时会调用它;它先用 prepare_process_handles 取句柄,再用 collect_output_until_deadline 收输出,最后用 refresh_process_state 判断是否还活着。

调用图:调用 9 个内部函数(process_failed, generate_chunk_id, prepare_process_handles, refresh_process_state, release_process_id, fail_process_with_message, finish_deferred_network_approval_for_session, finish_network_approval_after_process_exit_for_entry, network_denial_message_for_session);外部调用 7 个(from_millis, now, collect_output_until_deadline, from_utf8_lossy, approx_token_count, matches!, sleep)。

UnifiedExecProcessManager::refresh_process_state771–795 ↗
async fn refresh_process_state(&self, process_id: i32) -> ProcessStatus

作用:刷新某个后台进程的状态,看它还活着、已经退出,还是根本找不到。退出的进程会从仓库里移除。

数据流:进去 process_id → 加锁查进程记录;读取退出码和是否已退出;若退出就删除记录并返回 Exited,否则返回 Alive → 找不到则返回 Unknown。

调用关系:exec_command 和 write_stdin 在收集输出后调用它,用来决定返回进程 ID 继续跟踪,还是返回退出码并收尾。

调用图:被 2 处调用(exec_command, write_stdin);外部调用 1 个(new)。

UnifiedExecProcessManager::prepare_process_handles797–835 ↗
async fn prepare_process_handles(
        &self,
        process_id: i32,
    ) -> Result<PreparedProcessHandles, UnifiedExecError>

作用:为写输入或轮询输出提前取好所需的进程句柄和元信息。这样后面不用一直拿着仓库锁。

数据流:进去 process_id → 加锁找到进程记录,更新 last_used;复制输出缓冲、通知器、取消信号、会话、网络审批、命令信息等 → 返回 PreparedProcessHandles。

调用关系:write_stdin 一开始就调用它;它把后续写入、等待输出、网络检查需要的零件一次性备齐。

调用图:被 1 处调用(write_stdin);外部调用 2 个(clone, now)。

UnifiedExecProcessManager::store_process838–888 ↗
async fn store_process(
        &self,
        process: Arc<UnifiedExecProcess>,
        context: &UnifiedExecContext,
        command: &[String],
        hook_command: String,
        cwd: AbsolutePa

作用:把一个仍在运行的进程登记成后台进程。登记后,用户才能继续轮询、写输入或终止它。

数据流:进去进程、上下文、命令、目录、进程 ID、网络审批、输出记录等 → 构造 ProcessEntry 放进仓库;如果进程太多就淘汰一个旧的;再启动退出监听器 → 没有返回值。

调用关系:exec_command 发现初始命令还活着时调用它;它会用 prune_processes_if_needed 控制数量,并用 spawn_exit_watcher 负责后台结束事件。

调用图:调用 2 个内部函数(spawn_exit_watcher, unregister_network_approval_for_entry);被 1 处调用(exec_command);外部调用 4 个(clone, downgrade, prune_processes_if_needed, clone)。

UnifiedExecProcessManager::open_session_with_exec_env890–1024 ↗
async fn open_session_with_exec_env(
        &self,
        process_id: i32,
        request: &ExecRequest,
        tty: bool,
        mut spawn_lifecycle: SpawnLifecycleHandle,
        environment: &

作用:按给定执行环境真正启动进程。它兼容本地、远程执行服务器、Windows 沙箱、伪终端和普通管道几种启动方式。

数据流:进去进程 ID、执行请求、是否 tty、启动生命周期句柄和环境对象 → 根据平台和环境选择启动后端;远程时发 ExecParams,本地时 spawn 进程,Windows 特殊沙箱走专用接口 → 返回 UnifiedExecProcess 或创建失败错误。

调用关系:统一执行运行时最终会调用它来落地启动;远程路径会用 exec_server_params_for_request,本地路径会调用 pty 或 pipe 的启动工具。

调用图:调用 10 个内部函数(find_codex_home, create_process, from_exec_server_started, from_spawned, exec_server_params_for_request, get_exec_backend, is_remote, spawn_process_no_stdin_with_inherited_fds, default, spawn_process_with_inherited_fds);外部调用 4 个(after_spawn, inherited_fds, spawn_windows_sandbox_session_elevated_for_permission_profile, spawn_windows_sandbox_session_legacy)。

UnifiedExecProcessManager::open_session_with_sandbox1026–1114 ↗
async fn open_session_with_sandbox(
        &self,
        request: &ExecCommandRequest,
        cwd: AbsolutePathBuf,
        context: &UnifiedExecContext,
    ) -> Result<(UnifiedExecProcess, Option

作用:在启动命令前完成沙箱、审批、环境变量和运行时编排。它是从“用户请求”到“可启动执行请求”的转换站。

数据流:进去 ExecCommandRequest、工作目录和上下文 → 生成环境变量,构造执行服务器环境配置,询问执行审批策略,组装工具请求和工具上下文 → 通过 ToolOrchestrator 运行,返回进程和可能的延期网络审批。

调用关系:exec_command 调用它来启动新命令;它内部串起 create_env、apply_unified_exec_env、exec_env_policy_from_shell_policy、UnifiedExecRuntime 和 ToolOrchestrator。

调用图:调用 6 个内部函数(create_env, new, new, apply_unified_exec_env, exec_env_policy_from_shell_policy, plain);被 1 处调用(exec_command)。

UnifiedExecProcessManager::collect_output_until_deadline1116–1203 ↗
async fn collect_output_until_deadline(
        output_buffer: &OutputBuffer,
        output_notify: &Arc<Notify>,
        output_closed: &Arc<AtomicBool>,
        output_closed_notify: &Arc<Notify>,

作用:在指定截止时间前尽量收集进程输出。它不会无限等,也会在进程退出后稍等一小会儿,避免漏掉最后几字节。

数据流:进去输出缓冲、通知器、关闭标记、取消令牌、暂停状态和截止时间 → 循环取走缓冲里的输出;没输出就等新输出、退出信号、暂停变化或超时 → 返回收集到的字节数组。

调用关系:exec_command 用它收集初始输出,write_stdin 用它收集后续输出;它会调用 extend_deadlines_while_paused 和 wait_for_pause_change 处理暂停期间不计时的问题。

调用图:外部调用 10 个(cancelled, is_cancelled, from_millis, now, saturating_duration_since, lock, extend_deadlines_while_paused, with_capacity, pin!, select!)。

UnifiedExecProcessManager::extend_deadlines_while_paused1205–1229 ↗
async fn extend_deadlines_while_paused(
        pause_state: &mut Option<watch::Receiver<bool>>,
        deadline: &mut Instant,
        post_exit_deadline: &mut Option<Instant>,
    )

作用:当系统处于暂停状态时,把输出等待的截止时间往后顺延。这样用户被打断去处理别的提示时,不会白白消耗命令等待时间。

数据流:进去暂停状态接收器、主截止时间和退出后等待截止时间 → 如果当前没暂停就直接返回;如果暂停,就等到恢复,并计算暂停多久 → 把相关截止时间加上这段暂停时长。

调用关系:collect_output_until_deadline 每轮等待前调用它,保证暂停机制和输出收集计时能配合。

调用图:外部调用 1 个(now)。

UnifiedExecProcessManager::wait_for_pause_change1231–1239 ↗
async fn wait_for_pause_change(pause_state: Option<&watch::Receiver<bool>>)

作用:等待暂停状态发生变化。没有暂停状态时,它会永远等待,用来在 select 分支里表示“这个事件不存在”。

数据流:进去可选的 watch 接收器(一种能订阅状态变化的通道)→ 有接收器就克隆并等待 changed;没有就挂起不返回 → 没有业务返回值。

调用关系:collect_output_until_deadline 在同时等待输出、退出、超时的时候把它作为一个分支,用来及时响应暂停状态变化。

UnifiedExecProcessManager::prune_processes_if_needed1241–1257 ↗
fn prune_processes_if_needed(store: &mut ProcessStore) -> Option<ProcessEntry>

作用:当后台进程数量达到上限时,挑一个可以移除的旧进程。它防止系统里堆太多后台终端。

数据流:进去可变的 ProcessStore → 如果数量还没到上限,返回 None;否则收集每个进程的 ID、最近使用时间、是否退出 → 按策略选择并移除一个,返回被移除的记录。

调用关系:store_process 新增后台进程前调用它;具体挑谁的策略交给 process_id_to_prune_from_meta。

调用图:调用 1 个内部函数(remove);外部调用 1 个(process_id_to_prune_from_meta)。

UnifiedExecProcessManager::process_id_to_prune_from_meta1260–1286 ↗
fn process_id_to_prune_from_meta(meta: &[(i32, Instant, bool)]) -> Option<i32>

作用:根据一组进程元数据决定该淘汰哪个进程。策略是保护最近使用的几个,优先清掉已退出且较旧的。

数据流:进去若干进程的 ID、最近使用时间、是否退出 → 先找出最近 8 个受保护进程;再按最久未使用排序;优先选未受保护且已退出的,否则选未受保护的最旧进程 → 返回要淘汰的进程 ID。

调用关系:prune_processes_if_needed 调用它;它把淘汰规则集中在一处,方便以后换策略。

调用图:外部调用 2 个(is_empty, to_vec)。

UnifiedExecProcessManager::terminate_all_processes1288–1304 ↗
async fn terminate_all_processes(&self)

作用:终止并清空所有登记的后台进程。通常用于会话关闭或系统收尾,避免留下孤儿进程。

数据流:进去 manager 自身 → 加锁把所有进程记录取出并清空已保留 ID → 逐个注销网络审批并终止进程 → 没有返回值。

调用关系:运行结束或需要整体清理时会调用它;每个进程的审批清理交给 unregister_network_approval_for_entry。

调用图:调用 1 个内部函数(unregister_network_approval_for_entry)。

UnifiedExecProcessManager::list_processes1306–1323 ↗
async fn list_processes(&self) -> Vec<BackgroundTerminalInfo>

作用:列出当前仍在运行的后台终端。它给外部展示“有哪些命令还活着”。

数据流:进去 manager 自身 → 加锁读取进程仓库,过滤掉已退出的记录,按进程 ID 排序 → 转成 BackgroundTerminalInfo 列表,包含调用 ID、进程 ID、命令和目录。

调用关系:查看后台终端列表的功能会调用它;它只读状态,不启动或终止任何进程。

UnifiedExecProcessManager::terminate_process1325–1357 ↗
async fn terminate_process(&self, process_id: i32) -> bool

作用:终止指定的后台进程。它会小心避免删错进程,也不会在初始 exec_command 还没结束时抢先移除记录。

数据流:进去 process_id → 先找到进程并确认是否已退出;未退出则请求确认终止;然后重新加锁确认还是同一个进程,且初始调用不活跃 → 移除记录、注销网络审批,返回是否成功。

调用关系:用户手动关闭某个后台终端时会调用它;它使用 unregister_network_approval_for_entry 做权限清理,并用 InitialExecCommandGuard 留下的标记避免竞态。

调用图:调用 1 个内部函数(unregister_network_approval_for_entry);外部调用 2 个(clone, ptr_eq)。

core/src/tools/handlers/unified_exec/write_stdin.rs源码 ↗
io_transportrequest handling

有些终端程序不是一启动就结束,比如交互式脚本、等待密码或确认的问题。这个文件就是给这类场景用的:它接收模型发来的 write_stdin 调用,读出要写入哪个运行中的进程、要输入哪些字符、等多久、最多返回多少输出,然后交给统一执行管理器去真正写入标准输入。标准输入可以理解成“程序从键盘收到的内容”。如果输入不为空,它还会发一个终端交互事件给界面,让用户能看到模型往终端里输入了什么。一个小细节是:空输入被当成“后台查看进程状态”,只有进程还活着时才通知界面,避免制造没意义的交互记录。它还刻意不触发前置钩子,因为真正的命令在启动时已经走过那套流程;但结束时可能会补发对应的后置钩子。

函数细节7
WriteStdinHandler::tool_name35–37 ↗
fn tool_name(&self) -> ToolName

作用:告诉工具系统,这个处理器对应的工具名字叫 write_stdin。这样外部请求写标准输入时,系统才能把请求分派到这里。

数据流:进去的是这个处理器本身,不读取复杂状态;它把固定字符串 write_stdin 包装成工具名对象;出来的是一个标准化的工具名,供注册表和调度逻辑识别。

调用关系:工具注册或匹配工具调用时会问它“你叫什么”。它内部调用 plain 来生成普通工具名,后续系统就靠这个名字把 write_stdin 请求送到这个处理器。

调用图:调用 1 个内部函数(plain)。

WriteStdinHandler::spec39–41 ↗
fn spec(&self) -> ToolSpec

作用:给出 write_stdin 这个工具的说明书。说明书会告诉模型或调用方:这个工具需要哪些参数、参数大概是什么意思。

数据流:进去的是处理器本身;它调用 create_write_stdin_tool 生成工具规格;出来的是 ToolSpec,也就是工具的结构化描述。

调用关系:工具系统展示或注册 write_stdin 能力时会用到它。它把具体说明书的创建工作交给 create_write_stdin_tool,自己只负责把结果交回去。

调用图:调用 1 个内部函数(create_write_stdin_tool)。

WriteStdinHandler::handle43–45 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这是工具真正被调用时的入口。它把一次 write_stdin 请求包装成异步任务,方便系统一边等待终端响应,一边不堵住其他工作。

数据流:进去的是一次 ToolInvocation,也就是一次工具调用的上下文;它把 handle_call 变成可等待的异步执行对象;出来的是一个 future,可以理解成“稍后会完成的结果”。

调用关系:工具调度器选中 WriteStdinHandler 后会调用它。它不自己做细活,而是把实际处理交给 WriteStdinHandler::handle_call,并用 pin 固定这个异步任务的位置,方便运行时安全地执行。

调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。

WriteStdinHandler::handle_call49–102 ↗
async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>

作用:这是本文件的核心函数:解析模型给的参数,把字符写进指定的运行中进程,并把结果返回给模型。它还负责把可见的终端输入事件通知给界面。

数据流:进去的是一次工具调用,里面有会话、当前轮次信息和调用参数;它先确认这是函数式参数,不是别的 payload,否则返回给模型一个错误;再用 parse_arguments 把原始参数解析成 session_id、chars、等待时间和输出上限;随后调用 unified_exec_manager.write_stdin,把字符送进对应进程;拿到响应后,如果这次确实输入了字符,或者进程还活着,就发送 TerminalInteraction 事件给界面;最后用 boxed_tool_output 把执行结果包装成统一的工具输出返回。过程中如果解析或写入失败,会把错误整理成模型能看懂的失败信息。

调用关系:WriteStdinHandler::handle 会把实际工作交给它。它连接了三边:从模型来的参数、后台统一执行管理器、以及前端界面的终端交互事件。它调用 parse_arguments 做参数翻译,调用 TerminalInteraction 构造界面事件,失败时用 RespondToModel 生成给模型看的错误,成功时用 boxed_tool_output 交回标准输出格式。

调用图:调用 2 个内部函数(boxed_tool_output, parse_arguments);被 1 处调用(handle);外部调用 2 个(TerminalInteraction, RespondToModel)。

WriteStdinHandler::matches_kind106–108 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool

作用:判断某个工具请求是不是这个处理器能处理的类型。这里它只接受函数调用形式的请求。

数据流:进去的是一个 ToolPayload,也就是工具请求的载荷;它检查载荷是不是 Function 这一类;出来的是 true 或 false,表示能不能交给这个处理器。

调用关系:工具运行时在分派请求前会用它做一道简单筛选。它使用 matches! 这个模式匹配检查,只让函数式 write_stdin 调用继续往下走。

调用图:外部调用 1 个(matches!)。

WriteStdinHandler::pre_tool_use_payload110–115 ↗
fn pre_tool_use_payload(&self, _invocation: &ToolInvocation) -> Option<PreToolUsePayload>

作用:决定 write_stdin 调用前要不要发“工具即将使用”的提示。这里故意什么都不发,因为 write_stdin 只是延续一个已经启动的命令,不应该重复宣布一次新命令。

数据流:进去的是一次工具调用上下文,但这个函数不使用里面的信息;它直接返回 None;结果是不会产生前置工具使用事件,也不会触发第二次类似 Bash 命令开始的提示。

调用关系:统一工具运行流程在执行工具前会询问它是否需要前置事件。这里的答案始终是否定的,因为原始 exec_command 启动时已经做过前置流程,write_stdin 只是给它继续递纸条。

WriteStdinHandler::post_tool_use_payload117–125 ↗
fn post_tool_use_payload(
        &self,
        invocation: &ToolInvocation,
        result: &dyn crate::tools::context::ToolOutput,
    ) -> Option<PostToolUsePayload>

作用:决定 write_stdin 调用后要不要发“工具使用结束”的信息。它的特殊作用是:一次空输入轮询可能刚好发现原来的命令结束了,这时要补上那个命令对应的结束事件。

数据流:进去的是工具调用上下文和这次工具输出结果;它把这两样交给 post_unified_exec_tool_use_payload 判断;出来的是可能存在的 PostToolUsePayload,如果没有需要通知的结束信息,就返回 None。

调用关系:统一工具运行流程在 write_stdin 完成后会调用它。它不自己判断复杂规则,而是委托 post_unified_exec_tool_use_payload,这样 write_stdin 观察到进程结束时,也能和最初的 exec_command 收尾流程对齐。

调用图:外部调用 1 个(post_unified_exec_tool_use_payload)。

Exec-server 进程后端

这些文件确立 exec-server 进程契约,并提供符合该契约的本地和远程实现。

exec-server/src/process.rs源码 ↗
domain_logiccross-cutting

这个文件像是执行服务器里的“进程合同”和“消息公告栏”。一方面,它用 ExecProcess 这个 trait(可以理解成一份接口约定)规定:任何真实进程都必须能告诉别人自己的编号、输出内容、实时事件、写入结果、信号处理和终止方式。另一方面,ExecProcessEventLog 保存进程事件,比如输出了一段 stdout、进程退出、输出流关闭,或者会话失败。它不只把新事件实时广播出去,还会留一小段历史,像公告栏上保留最近几张通知。这样监听者晚一点加入时,也能先补看最近发生的事,再接着听直播。缓存有两个上限:事件条数和输出字节数,防止大量输出把内存撑爆。文件最后还有一个测试,确认超大输出被挤掉后,退出和关闭事件仍能被回放。

函数细节8
ExecProcessEvent::seq67–73 ↗
fn seq(&self) -> Option<u64>

作用:取出一个进程事件的顺序号,用来判断这些事件先后发生的顺序。不是所有事件都有顺序号,比如连接失败是客户端临时补出来的,不是进程自己发出的。

数据流:进去的是一个事件本身 → 它查看事件类型:输出、退出、关闭就拿出里面的 seq;失败事件就返回“没有顺序号” → 出来的是一个可选的数字,用来给事件排序。

调用关系:publish_ordered_event 会用它来确认事件该按什么顺序发布。它不负责发布,只负责回答“这个事件有没有进程给的编号”。

调用图:被 1 处调用(publish_ordered_event)。

ExecProcessEvent::retained_len75–81 ↗
fn retained_len(&self) -> usize

作用:计算一个事件放进历史缓存时大概占多少字节,主要用来防止输出内容太多占满内存。

数据流:进去的是一个事件 → 如果是输出事件,就看输出数据有多长;如果是失败消息,就看文字有多长;退出和关闭事件不带大块内容,所以算 0 → 出来的是这个事件需要计入缓存容量的字节数。

调用关系:ExecProcessEventLog::publish 在保存事件前后会调用它,用它决定历史记录是否已经太大、要不要丢掉最旧的事件。

调用图:被 1 处调用(publish)。

ExecProcessEventLog::new85–95 ↗
fn new(event_capacity: usize, byte_capacity: usize) -> Self

作用:新建一个进程事件日志,也就是给某个进程准备一块“历史缓存”和一个“实时广播频道”。调用者可以指定最多留多少条事件、最多留多少字节。

数据流:进去的是事件条数上限和字节数上限 → 它创建实时广播通道,准备一个空的历史队列,并把这些东西包进可共享的结构里 → 出来的是一个可被复制引用的 ExecProcessEventLog。

调用关系:启动真实进程、启动测试进程,或者测试事件缓存时会创建它。之后发布方会调用 ExecProcessEventLog::publish 往里面放事件,订阅方会调用 ExecProcessEventLog::subscribe 来收事件。

调用图:被 4 处调用(new, start_process, spawn_test_process, event_history_replay_is_bounded_by_retained_bytes);外部调用 4 个(new, new, channel, default)。

ExecProcessEventLog::publish97–117 ↗
fn publish(&self, event: ExecProcessEvent)

作用:把一个新的进程事件发布出去,同时放进有限大小的历史记录里。这样正在监听的人马上能收到,晚来的监听者也可能补看到最近的事件。

数据流:进去的是一个事件 → 它先给历史记录加锁(互斥锁,一把锁,防止两个任务同时改同一份历史),计算事件占用大小,放到队尾;如果条数或字节数超限,就从队头丢掉旧事件;最后把事件发到实时广播通道 → 出来没有返回值,但历史缓存和实时订阅者都会被更新。

调用关系:publish_ordered_event 会在确认事件顺序后调用它。它内部会用 ExecProcessEvent::retained_len 计算容量,并把事件交给 Tokio 的 broadcast 通道,供 ExecProcessEventReceiver::recv 之后收到。

调用图:调用 1 个内部函数(retained_len);被 1 处调用(publish_ordered_event);外部调用 1 个(clone)。

ExecProcessEventLog::subscribe119–129 ↗
fn subscribe(&self) -> ExecProcessEventReceiver

作用:给一个新监听者开一个接收器。这个接收器会先收到当前保留下来的历史事件,再继续收到之后的新事件。

数据流:进去的是事件日志本身 → 它锁住历史记录,复制一份当前还保留的事件,同时从实时广播通道开一个新的接收端 → 出来的是 ExecProcessEventReceiver,里面装着“待回放历史”和“直播接收器”。

调用关系:具体的进程实现会在 subscribe_events 里调用它,把事件流交给外部请求者。后续真正取下一条事件时,由 ExecProcessEventReceiver::recv 完成。

调用图:被 2 处调用(subscribe_events, subscribe_events)。

ExecProcessEventReceiver::empty138–144 ↗
fn empty() -> Self

作用:创建一个空的事件接收器,适合在没有真实事件源、或者事件功能不可用时返回一个安全的占位对象。

数据流:进去没有参数 → 它创建一个小的广播通道接收端,但不放任何历史事件 → 出来的是一个不会先回放任何内容的 ExecProcessEventReceiver。

调用关系:一些 subscribe_events 实现会在没有可用事件日志时调用它。它让调用方仍然拿到同一种接收器,避免因为“没有事件流”而需要特殊处理。

调用图:被 2 处调用(subscribe_events, subscribe_events);外部调用 2 个(new, channel)。

ExecProcessEventReceiver::recv151–157 ↗
async fn recv(&mut self) -> Result<ExecProcessEvent, broadcast::error::RecvError>

作用:等待并取出下一条进程事件。它会先把历史补发完,再去等新的实时事件。

数据流:进去的是这个接收器的可变引用 → 它先检查 replay 队列里有没有历史事件,有就弹出第一条返回;如果历史已经空了,就异步等待实时广播通道的下一条消息 → 出来的是一个事件,或者一个接收错误,比如监听者落后太多导致实时通道里的旧消息已经被覆盖。

调用关系:receive_message 会调用它,把进程输出和状态变化发给外部客户端。它承接 ExecProcessEventLog::subscribe 准备好的历史和直播两部分,是事件真正被消费的地方。

调用图:调用 1 个内部函数(recv);被 1 处调用(receive_message);外部调用 1 个(pop_front)。

tests::event_history_replay_is_bounded_by_retained_bytes209–245 ↗
async fn event_history_replay_is_bounded_by_retained_bytes()

作用:这是一个测试,确认事件历史不仅受“条数”限制,也受“字节数”限制。特别是大块输出太大时,会被清掉,但后面的退出和关闭事件仍应保留。

数据流:进去没有外部输入 → 测试创建一个只允许保留 3 字节内容的事件日志,先发布一个很大的输出事件,再发布退出和关闭事件;然后订阅并读取回放 → 结果应该只读到退出和关闭两个事件,最后用断言检查是否符合预期。

调用关系:它直接调用 ExecProcessEventLog::new 创建日志,再通过发布和订阅验证 ExecProcessEventLog::publish 与 ExecProcessEventLog::subscribe 的配合是否正确。它只在测试时运行,不参与正式服务器处理请求。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, Output, vec!)。

exec-server/src/local_process.rs源码 ↗
domain_logicrequest handling / process lifecycle

可以把这个文件想成一个“本机进程管家”。外部发来请求,说要运行某个命令,它先检查进程编号是否重复、工作目录能不能在本机使用,再按要求选择普通管道或伪终端(pty,像一个假的终端窗口)来启动程序。程序跑起来后,它会同时做三件事:收集 stdout/stderr 或 pty 输出,给每段输出编号并保留最近一部分;监听进程退出码;通过通知通道把“有新输出、已退出、已关闭”告诉外部。它还提供读、写、发中断信号、强制结束等操作。内部用互斥锁(一把锁,防止多个异步任务同时改同一份进程表)保护进程列表,用通知器叫醒正在等输出的人。一个重要细节是:进程退出不等于马上删除,只有输出流也关完后才算 closed,并会短暂保留,方便调用方最后读取结果。

函数细节42
LocalProcess::default107–112 ↗
fn default() -> Self

作用:创建一个默认的本机进程后端,主要给测试或简单场景使用。它会准备一个“没人真正消费也不会卡住”的通知通道,避免没有通知接收者时程序出问题。

数据流:进去没有参数 → 建一个异步消息通道,并启动一个后台任务把通道里的消息读掉 → 出来一个可用的 LocalProcess,里面已经带好通知发送器。

调用关系:它会调用 LocalProcess::new 来完成真正初始化;测试和默认构造场景会用它快速拿到一个能启动进程的后端。

调用图:调用 1 个内部函数(new);被 5 处调用(default_for_tests, local, closed_process_is_evicted_after_retention, exited_process_retains_late_output_past_retention, start_process_rejects_non_native_cwd_before_launch);外部调用 2 个(new, spawn)。

LocalProcess::new116–123 ↗
fn new(notifications: RpcNotificationSender) -> Self

作用:用指定的通知发送器创建本机进程后端。调用方如果想把输出、退出等事件发到自己的 RPC 通道,就会用这个函数。

数据流:进去一个 RpcNotificationSender(用来发通知的东西)→ 包进共享的内部状态里,同时创建空的进程表 → 出来一个 LocalProcess。

调用关系:LocalProcess::default 会转过来调用它;之后 start_process、exec_read 等函数都会共用这里创建的内部状态。

调用图:被 1 处调用(new);外部调用 4 个(new, new, new, new)。

LocalProcess::shutdown125–139 ↗
async fn shutdown(&self)

作用:关闭这个后端时,把还在跑的本机进程都停掉。没有这一步,服务器结束时可能留下孤儿进程继续运行。

数据流:进去没有额外参数 → 锁住进程表,把里面所有 Running 状态的进程取出来 → 对每个进程调用 terminate,让它们停止。

调用关系:这是收尾阶段用的清理函数;测试里也会调用它,保证模拟或真实进程不会泄漏到下一轮测试。

调用图:被 1 处调用(shutdown)。

LocalProcess::set_notification_sender141–148 ↗
fn set_notification_sender(&self, notifications: Option<RpcNotificationSender>)

作用:替换或关闭事件通知发送器。它让服务器可以在连接变化时,把进程事件发给新的接收方,或者暂时不发。

数据流:进去一个可选的通知发送器 → 写入内部的读写锁保护区域 → 之后新输出、退出、关闭事件都会按这个新设置发送或不发送。

调用关系:stream_output、watch_exit、maybe_emit_closed 最终会通过 notification_sender 读取这里设置的发送器。

调用图:被 1 处调用(set_notification_sender)。

LocalProcess::start_process150–274 ↗
async fn start_process(
        &self,
        params: ExecParams,
    ) -> Result<(ExecResponse, watch::Sender<u64>, ExecProcessEventLog), JSONRPCErrorError>

作用:真正启动一个本机命令,并把它登记到进程表里。它是这个文件的核心入口:把“运行这个命令”的请求变成一个真实运行的子进程。

数据流:进去 ExecParams,里面有进程编号、命令参数、工作目录、环境变量、是否用 tty、是否接收 stdin → 先检查 argv 不为空、cwd 是本机路径、进程编号不重复;再算出子进程环境变量;然后启动 pty 或管道进程;启动成功后登记 RunningProcess,并开三个后台任务分别收输出、等退出、处理关闭 → 出来 ExecResponse、唤醒通道和事件日志;如果失败,会返回 JSON-RPC 错误。

调用关系:LocalProcess::exec 和 LocalProcess::start 都会调用它。它把后续工作交给 stream_output 监听输出、watch_exit 监听退出,并用 child_env 准备环境变量。

调用图:调用 7 个内部函数(child_env, stream_output, watch_exit, new, internal_error, invalid_request, default);被 2 处调用(exec, start);外部调用 13 个(clone, new, new, new, new, spawn_pipe_process, spawn_pipe_process_no_stdin, spawn_pty_process, Running, format! (+3 more))。

LocalProcess::exec276–280 ↗
async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError>

作用:给 JSON-RPC 风格的调用提供“启动进程”接口。它只返回外部需要知道的启动结果,不暴露内部事件对象。

数据流:进去 ExecParams → 调用 start_process 启动进程 → 丢掉内部用的唤醒通道和事件日志,只把 ExecResponse 返回。

调用关系:它是 start_process 的薄包装,适合协议处理层直接调用。

调用图:调用 1 个内部函数(start_process);被 1 处调用(exec)。

LocalProcess::exec_read282–358 ↗
async fn exec_read(
        &self,
        params: ReadParams,
    ) -> Result<ReadResponse, JSONRPCErrorError>

作用:读取某个进程从指定序号之后的新输出,也能等待一小段时间直到有变化。外部轮询进程输出时主要靠它。

数据流:进去进程编号、上次读到的序号、最多读多少字节、最多等多久 → 找到进程,挑出序号更大的已保留输出块,按字节上限截住;如果暂时没新内容且还没超时,就等输出通知 → 出来 ReadResponse,里面有输出块、下一个序号、是否退出、退出码、是否彻底关闭。

调用关系:LocalProcess::read 和测试辅助函数会调用它;stream_output、watch_exit、maybe_emit_closed 会更新它读取的输出、退出和关闭状态。

调用图:调用 1 个内部函数(invalid_request);被 3 处调用(read, read_process_until_change, exec_read);外部调用 6 个(clone, from_millis, new, format!, now, timeout)。

LocalProcess::exec_write360–393 ↗
async fn exec_write(
        &self,
        params: WriteParams,
    ) -> Result<WriteResponse, JSONRPCErrorError>

作用:把一段输入写进某个进程的标准输入。比如运行一个交互式程序时,用户敲的字就通过这里送进去。

数据流:进去进程编号和字节内容 → 找到对应进程,检查它是否已经运行、是否支持 stdin;如果可以,就拿到写入通道并发送字节 → 出来 WriteResponse,说明已接受、进程未知、还在启动、或 stdin 已关闭。

调用关系:LocalProcess::write 会包装调用它;它直接使用 RunningProcess 里的 session 写入通道。

调用图:被 2 处调用(write, exec_write)。

LocalProcess::signal_process395–416 ↗
async fn signal_process(
        &self,
        params: SignalParams,
    ) -> Result<SignalResponse, JSONRPCErrorError>

作用:给进程发送信号,目前主要是中断信号。通俗说,就是模拟按下 Ctrl-C 这类操作。

数据流:进去进程编号和协议里的信号类型 → 找到正在运行且尚未退出的进程,把协议信号转换成 pty 库能懂的信号 → 发给底层进程;无论进程不存在或还在启动,都会安静返回成功响应。

调用关系:LocalProcess::signal 会调用它;它把信号转换工作交给 pty_process_signal。

调用图:调用 1 个内部函数(pty_process_signal);被 2 处调用(signal, signal)。

LocalProcess::terminate_process418–437 ↗
async fn terminate_process(
        &self,
        params: TerminateParams,
    ) -> Result<TerminateResponse, JSONRPCErrorError>

作用:请求强制结束一个进程。它用于“不想等了,直接停掉这个命令”的场景。

数据流:进去进程编号 → 查进程表;如果进程正在运行且没退出,就调用底层 terminate;如果已经退出、还在启动或不存在,就不做事 → 出来 TerminateResponse,告诉调用方刚才目标是否还在运行。

调用关系:LocalProcess::terminate 会调用它;真正的停止动作由 RunningProcess 里的 session 完成。

调用图:被 2 处调用(terminate, terminate)。

child_env440–449 ↗
fn child_env(params: &ExecParams) -> HashMap<String, String>

作用:算出子进程启动时应该看到哪些环境变量。环境变量就是传给程序的一组名字和值,比如 PATH。

数据流:进去 ExecParams → 如果没有环境策略,就直接使用 params.env;如果有策略,就先按策略从当前 shell 环境生成一份,再用 params.env 覆盖同名变量 → 出来一个 HashMap 形式的环境变量表。

调用关系:start_process 在启动真实命令前调用它;它会把协议里的环境策略交给 shell_environment_policy 转成通用格式。

调用图:调用 2 个内部函数(shell_environment_policy, create_env);被 1 处调用(start_process)。

shell_environment_policy451–468 ↗
fn shell_environment_policy(env_policy: &ExecEnvPolicy) -> ShellEnvironmentPolicy

作用:把 exec 协议里的环境变量策略,翻译成 shell_environment 模块能理解的策略。它像翻译员,把同一件事换成另一个库认识的说法。

数据流:进去 ExecEnvPolicy → 复制继承方式、排除规则、指定设置、只包含规则,并把匹配规则做成大小写不敏感的模式 → 出来 ShellEnvironmentPolicy。

调用关系:只由 child_env 调用,用来配合 shell_environment::create_env 生成最终环境变量。

调用图:被 1 处调用(child_env)。

LocalProcess::start488–490 ↗
fn start(&self, params: ExecParams) -> ExecBackendFuture<'_>

作用:实现通用 ExecBackend 接口里的启动动作。它把本地启动出来的进程包装成统一的 ExecProcess 对象,方便上层不关心后端细节。

数据流:进去 ExecParams → 调用 start_process;如果 JSON-RPC 错误出现,就转换成 ExecServerError;成功后创建 LocalExecProcess 并放进 StartedExecProcess → 出来统一格式的启动结果。

调用关系:ExecBackend::start 会调用这个内部 async 函数;它是本地后端接入通用执行框架的桥。

调用图:调用 1 个内部函数(start_process);外部调用 2 个(new, pin)。

LocalExecProcess::process_id519–521 ↗
fn process_id(&self) -> &ProcessId

作用:返回这个进程的编号。调用方用它确认自己正在操作的是哪一个进程。

数据流:进去的是 LocalExecProcess 自身 → 读取里面保存的 process_id → 返回这个编号的引用,不改任何状态。

调用关系:它是 ExecProcess 接口的一部分,上层在记录、显示或继续操作进程时会用到。

LocalExecProcess::subscribe_wake523–525 ↗
fn subscribe_wake(&self) -> watch::Receiver<u64>

作用:订阅这个进程的“有变化了”提醒。读者可以把它理解成门铃:有新输出、退出或关闭时会响。

数据流:进去的是 LocalExecProcess 自身 → 从 wake_tx 创建一个新的接收端 → 出来 watch::Receiver,调用方可用它等待变化。

调用关系:stream_output、watch_exit、maybe_emit_closed 会往 wake_tx 里发送新序号;这个函数让外部拿到接收端。

调用图:外部调用 1 个(subscribe)。

LocalExecProcess::subscribe_events527–529 ↗
fn subscribe_events(&self) -> ExecProcessEventReceiver

作用:订阅结构化的进程事件,比如输出、退出、关闭。相比只知道“有变化”,这里能知道具体发生了什么。

数据流:进去的是 LocalExecProcess 自身 → 从事件日志创建订阅者 → 出来 ExecProcessEventReceiver。

调用关系:stream_output、watch_exit、maybe_emit_closed 会发布事件;这个函数把事件流交给上层消费者。

调用图:调用 1 个内部函数(subscribe)。

LocalExecProcess::read531–538 ↗
fn read(
        &self,
        after_seq: Option<u64>,
        max_bytes: Option<usize>,
        wait_ms: Option<u64>,
    ) -> ExecProcessFuture<'_, ReadResponse>

作用:实现统一进程对象上的读取接口。调用方不用知道它背后是 LocalProcess,只管向这个进程读输出。

数据流:进去上次读到的序号、字节上限、等待时间 → 转交给 backend.read,并带上自己的 process_id → 出来 ReadResponse 的异步结果。

调用关系:这是 ExecProcess 接口方法,内部把活交给 LocalProcess::read。

调用图:调用 1 个内部函数(read);外部调用 1 个(pin)。

LocalExecProcess::write540–542 ↗
fn write(&self, chunk: Vec<u8>) -> ExecProcessFuture<'_, WriteResponse>

作用:实现统一进程对象上的写入接口。外部要给这个进程输入内容时调用它。

数据流:进去一段字节 → 带上自己的 process_id 转交给 backend.write → 出来 WriteResponse 的异步结果。

调用关系:这是 ExecProcess 接口方法,实际写 stdin 的工作由 LocalProcess::write 和 exec_write 完成。

调用图:调用 1 个内部函数(write);外部调用 1 个(pin)。

LocalExecProcess::signal544–546 ↗
fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()>

作用:实现统一进程对象上的发信号接口。比如让这个进程收到中断。

数据流:进去一个 ProcessSignal → 带上自己的 process_id 转交给 backend.signal → 成功时出来空结果,失败时出来 ExecServerError。

调用关系:这是 ExecProcess 接口方法,实际发信号由 LocalProcess::signal 和 signal_process 完成。

调用图:调用 1 个内部函数(signal);外部调用 1 个(pin)。

LocalExecProcess::terminate548–550 ↗
fn terminate(&self) -> ExecProcessFuture<'_, ()>

作用:实现统一进程对象上的终止接口。调用方想结束该进程时用它。

数据流:进去的是 LocalExecProcess 自身 → 带上自己的 process_id 调用 backend.terminate → 成功时没有额外数据,失败时返回错误。

调用关系:这是 ExecProcess 接口方法,实际终止动作由 LocalProcess::terminate 和 terminate_process 完成。

调用图:调用 1 个内部函数(terminate);外部调用 1 个(pin)。

LocalProcess::read554–569 ↗
async fn read(
        &self,
        process_id: &ProcessId,
        after_seq: Option<u64>,
        max_bytes: Option<usize>,
        wait_ms: Option<u64>,
    ) -> Result<ReadResponse, ExecServerEr

作用:把通用后端读接口转换成协议里的 exec_read 调用。它让 LocalExecProcess 可以用统一方式读本地进程。

数据流:进去进程编号、序号、字节上限、等待时间 → 组装成 ReadParams 调用 exec_read → 把 JSON-RPC 错误转换成 ExecServerError 后返回。

调用关系:LocalExecProcess::read 会调用它;它是对象接口和协议处理函数之间的适配层。

调用图:调用 1 个内部函数(exec_read);被 1 处调用(read);外部调用 1 个(clone)。

LocalProcess::write571–582 ↗
async fn write(
        &self,
        process_id: &ProcessId,
        chunk: Vec<u8>,
    ) -> Result<WriteResponse, ExecServerError>

作用:把通用后端写接口转换成协议里的 exec_write 调用。它负责把裸字节包装成协议格式。

数据流:进去进程编号和字节内容 → 组装 WriteParams,调用 exec_write → 返回写入状态,或把错误转成 ExecServerError。

调用关系:LocalExecProcess::write 会调用它;真正判断 stdin 是否可写的是 exec_write。

调用图:调用 1 个内部函数(exec_write);被 1 处调用(write);外部调用 1 个(clone)。

LocalProcess::signal584–596 ↗
async fn signal(
        &self,
        process_id: &ProcessId,
        signal: ProcessSignal,
    ) -> Result<(), ExecServerError>

作用:把通用后端发信号接口转换成协议里的 signal_process 调用。它隐藏了协议参数组装细节。

数据流:进去进程编号和信号 → 组装 SignalParams,调用 signal_process → 成功时返回空结果,失败时转换错误。

调用关系:LocalExecProcess::signal 会调用它;signal_process 再把信号交给底层 session。

调用图:调用 1 个内部函数(signal_process);被 1 处调用(signal);外部调用 1 个(clone)。

LocalProcess::terminate598–605 ↗
async fn terminate(&self, process_id: &ProcessId) -> Result<(), ExecServerError>

作用:把通用后端终止接口转换成协议里的 terminate_process 调用。上层只需要说“结束这个进程”。

数据流:进去进程编号 → 组装 TerminateParams,调用 terminate_process → 成功时忽略 running 字段,只表示请求已处理;错误会转换格式。

调用关系:LocalExecProcess::terminate 会调用它;实际是否还在运行由 terminate_process 判断。

调用图:调用 1 个内部函数(terminate_process);被 1 处调用(terminate);外部调用 1 个(clone)。

pty_process_signal608–612 ↗
fn pty_process_signal(signal: ProcessSignal) -> PtyProcessSignal

作用:把协议里的进程信号转换成 pty 库里的信号类型。两个模块名字不同,但表达的是同一种动作。

数据流:进去 ProcessSignal → 按匹配规则转换,目前 Interrupt 对应 Interrupt → 出来 PtyProcessSignal。

调用关系:signal_process 在真正调用底层 session.signal 之前会用它。

调用图:被 1 处调用(signal_process)。

map_handler_error614–619 ↗
fn map_handler_error(error: JSONRPCErrorError) -> ExecServerError

作用:把 JSON-RPC 的错误格式转换成 exec-server 内部统一错误格式。这样上层看到的错误类型保持一致。

数据流:进去 JSONRPCErrorError,里面有 code 和 message → 复制这两个字段放进 ExecServerError::Server → 出来 ExecServerError。

调用关系:LocalProcess::start、read、write、signal、terminate 这些适配函数会用它把协议层错误翻译给通用接口。

stream_output621–677 ↗
async fn stream_output(
    process_id: ProcessId,
    stream: ExecOutputStream,
    mut receiver: tokio::sync::mpsc::Receiver<Vec<u8>>,
    inner: Arc<Inner>,
    output_notify: Arc<Notify>,
)

作用:持续接收某个进程的一路输出,并把它保存、编号、通知出去。它像一个专门盯着水管的工人,有水流出来就立刻记录。

数据流:进去进程编号、输出流类型、字节接收器、共享内部状态、通知器 → 不断从接收器拿输出块;每块分配 seq,放进保留队列,超出 1MB 就丢最旧内容;唤醒等待者,发布事件,发送 RPC 输出通知 → 接收器关闭后调用 finish_output_stream 标记这一路输出结束。

调用关系:start_process 会为 stdout/stderr 或 pty 两路输出各启动一个;测试的 spawn_test_process 也会启动它。它结束后把关闭判断交给 finish_output_stream。

调用图:调用 3 个内部函数(finish_output_stream, notification_sender, recv);被 2 处调用(start_process, spawn_test_process);外部调用 2 个(Output, clone)。

watch_exit679–715 ↗
async fn watch_exit(
    process_id: ProcessId,
    exit_rx: tokio::sync::oneshot::Receiver<i32>,
    inner: Arc<Inner>,
    output_notify: Arc<Notify>,
)

作用:等待进程退出,并记录退出码。它专门负责回答“这个命令最后是成功还是失败退出”。

数据流:进去进程编号、退出码接收器、共享状态、通知器 → 等待退出码;拿到后写入 RunningProcess,生成一个新 seq,唤醒读者,发布退出事件并发送 RPC 退出通知 → 最后调用 maybe_emit_closed 看进程是否已经完全关闭。

调用关系:start_process 和测试的 spawn_test_process 会启动它;它和 stream_output 配合,一个管退出,一个管输出。

调用图:调用 2 个内部函数(maybe_emit_closed, notification_sender);被 2 处调用(start_process, spawn_test_process);外部调用 2 个(clone, clone)。

finish_output_stream717–730 ↗
async fn finish_output_stream(process_id: ProcessId, inner: Arc<Inner>)

作用:标记某一路输出已经结束。一个进程通常有两路输出,只有都结束并且进程也退出后,才算彻底关闭。

数据流:进去进程编号和共享状态 → 找到进程后把 open_streams 减一 → 然后调用 maybe_emit_closed 检查是否满足 closed 条件。

调用关系:stream_output 在输出通道读完时调用它;它不直接发关闭通知,而是交给 maybe_emit_closed 统一判断。

调用图:调用 1 个内部函数(maybe_emit_closed);被 1 处调用(stream_output)。

maybe_emit_closed732–778 ↗
async fn maybe_emit_closed(process_id: ProcessId, inner: Arc<Inner>)

作用:判断进程是否已经“完全收摊”,如果是,就发出 closed 通知并安排稍后删除记录。这里的 closed 表示进程退出了、输出也都读完了。

数据流:进去进程编号和共享状态 → 检查进程是否未关闭、所有输出流都结束、退出码已存在;满足时设置 closed,分配 seq,唤醒等待者,发布关闭事件,发送 RPC 关闭通知;同时启动一个延时任务,过保留时间后从进程表删除它 → 如果条件不满足就什么也不做。

调用关系:watch_exit 和 finish_output_stream 都会调用它,因为退出和输出流关闭可能谁先发生。它会用 notification_sender 获取当前通知通道。

调用图:调用 1 个内部函数(notification_sender);被 2 处调用(finish_output_stream, watch_exit);外部调用 5 个(clone, clone, matches!, spawn, sleep)。

notification_sender780–786 ↗
fn notification_sender(inner: &Inner) -> Option<RpcNotificationSender>

作用:安全地取出当前的通知发送器。它处理了读写锁可能中毒的情况,尽量让服务继续工作。

数据流:进去内部共享状态 Inner → 读取 notifications 锁里的 Option<RpcNotificationSender> 并克隆一份 → 出来可选的发送器。

调用关系:stream_output、watch_exit、maybe_emit_closed 在发 RPC 通知前都会调用它;set_notification_sender 会改变它之后读到的值。

调用图:被 3 处调用(maybe_emit_closed, stream_output, watch_exit)。

tests::test_exec_params798–809 ↗
fn test_exec_params(env: HashMap<String, String>) -> ExecParams

作用:给测试快速造一个标准的 ExecParams。这样每个测试只改自己关心的字段,不必重复写一大堆参数。

数据流:进去一份环境变量表 → 填入固定进程编号、命令 true、当前目录、默认不开 tty、不接 stdin → 出来 ExecParams。

调用关系:多个环境变量和路径校验测试会调用它,作为测试数据的基础模板。

调用图:调用 2 个内部函数(from, from_path);外部调用 2 个(current_dir, vec!)。

tests::start_process_rejects_non_native_cwd_before_launch812–833 ↗
async fn start_process_rejects_non_native_cwd_before_launch()

作用:测试非本机可用的工作目录会被拒绝,而且是在真正启动进程前拒绝。这样可以避免把不合法路径交给操作系统后产生更混乱的错误。

数据流:进去没有外部输入 → 构造一个对当前系统不合法的 file URI,放进 ExecParams → 调用 start_process → 期望得到 invalid_params 错误,并检查错误内容一致。

调用关系:它调用 LocalProcess::default 和 tests::test_exec_params;验证 start_process 里的 cwd 检查逻辑。

调用图:调用 3 个内部函数(default, invalid_params, parse);外部调用 5 个(new, assert_eq!, test_exec_params, format!, panic!)。

tests::child_env_defaults_to_exact_env836–843 ↗
fn child_env_defaults_to_exact_env()

作用:测试没有环境策略时,child_env 会原样使用传入的环境变量。也就是说,不偷偷继承当前系统环境。

数据流:进去没有外部输入 → 构造只含 ONLY_THIS 的 ExecParams → 调用 child_env → 检查结果正好等于这一项。

调用关系:它调用 tests::test_exec_params;验证 child_env 的默认分支。

调用图:外部调用 3 个(from, assert_eq!, test_exec_params)。

tests::child_env_applies_policy_then_overlay846–868 ↗
fn child_env_applies_policy_then_overlay()

作用:测试有环境策略时,先按策略生成环境,再让显式传入的 env 覆盖策略设置。重点是确认“用户指定的值优先”。

数据流:进去没有外部输入 → 构造一个不继承环境、但策略里设置 POLICY_SET 的参数,同时 env 里也设置同名变量 → 调用 child_env → 检查最终值使用 env 里的 overlay-wins;Windows 上还允许 PATHEXT 默认项。

调用关系:它调用 tests::test_exec_params;验证 child_env 和 shell_environment_policy 的配合。

调用图:外部调用 5 个(from, new, assert_eq!, cfg!, test_exec_params)。

tests::exited_process_retains_late_output_past_retention871–919 ↗
async fn exited_process_retains_late_output_past_retention()

作用:测试进程退出后,如果输出流还没关闭,迟到的输出仍然能读到。这个场景防止“退出通知先到、最后几行输出丢了”的问题。

数据流:进去没有外部输入 → 用测试后端造一个假进程,让它先退出;读取并确认已退出但未关闭;等过保留时间后再发送 stdout;再次读取,确认迟到输出还在;最后关闭输出流并等 closed → 测试结束前 shutdown。

调用关系:它用 LocalProcess::default、spawn_test_process、read_process_until_change、read_process_until_closed;重点覆盖 watch_exit、stream_output、maybe_emit_closed 的时序配合。

调用图:调用 1 个内部函数(default);外部调用 9 个(from_millis, from_secs, assert!, assert_eq!, read_process_until_change, read_process_until_closed, spawn_test_process, sleep, timeout)。

tests::closed_process_is_evicted_after_retention922–953 ↗
async fn closed_process_is_evicted_after_retention()

作用:测试进程彻底关闭后,会在保留时间之后从进程表里删除。这样长期运行的服务器不会无限积累已结束进程。

数据流:进去没有外部输入 → 造一个假进程,让它退出并关闭 stdout/stderr;等待 read 返回 closed;再循环检查进程表,直到该进程编号消失 → 确认清理任务生效。

调用关系:它用 LocalProcess::default、spawn_test_process、read_process_until_closed;验证 maybe_emit_closed 里启动的延时清理任务。

调用图:调用 1 个内部函数(default);外部调用 7 个(from_millis, from_secs, assert!, read_process_until_closed, spawn_test_process, sleep, timeout)。

tests::TestProcess::exit963–969 ↗
fn exit(&mut self, exit_code: i32)

作用:让测试里的假进程发送退出码。它模拟真实进程结束时底层会发出的退出事件。

数据流:进去退出码 → 取出一次性的 exit_tx 发送器并发送这个退出码 → 之后这个假进程不能再退出第二次。

调用关系:测试用例会调用它触发 watch_exit;它是 TestProcess 这个测试替身的一部分。

tests::spawn_test_process972–1032 ↗
async fn spawn_test_process(backend: &LocalProcess, process_id: &str) -> TestProcess

作用:创建一个可控的假进程并放进 LocalProcess 的进程表。测试可以手动发送 stdout、stderr 和退出码,而不用真的启动系统命令。

数据流:进去后端和进程编号字符串 → 建立 stdout/stderr/exit 通道、通知器、事件日志和 dummy session;把 RunningProcess 插入进程表;启动 stream_output 和 watch_exit 后台任务 → 出来 TestProcess,测试可用它控制输出和退出。

调用关系:多个异步测试调用它;它复用生产代码里的 stream_output 和 watch_exit,所以能测试真实的输出、退出、关闭流程。

调用图:调用 4 个内部函数(stream_output, watch_exit, new, from);外部调用 12 个(clone, new, new, new, new, assert!, Running, dummy_session, channel, channel (+2 more))。

tests::dummy_session1034–1050 ↗
fn dummy_session() -> ExecCommandSession

作用:造一个测试用的空会话,满足 RunningProcess 需要的 session 字段。它不代表真实运行的命令,只是占位。

数据流:进去没有参数 → 创建几个不会真正使用的通道,组装 ProcessDriver → 调用 spawn_from_driver 得到 ExecCommandSession → 返回这个 session。

调用关系:tests::spawn_test_process 调用它;测试里真正的输出和退出由手动通道控制,不靠这个 session。

调用图:外部调用 4 个(spawn_from_driver, channel, channel, channel)。

tests::read_process_until_change1052–1069 ↗
async fn read_process_until_change(
        backend: &LocalProcess,
        process_id: &ProcessId,
        after_seq: Option<u64>,
    ) -> ReadResponse

作用:测试辅助函数:一直等到进程有变化或超时,然后返回一次读取结果。它让测试不用重复写等待和错误处理代码。

数据流:进去后端、进程编号、上次序号 → 调用 exec_read,并设置最多等待 1000 毫秒;外层再加 1 秒 timeout 防止测试卡死 → 出来 ReadResponse。

调用关系:多个测试调用它读取退出、输出等变化;它直接验证 exec_read 的等待行为。

调用图:调用 1 个内部函数(exec_read);外部调用 3 个(from_secs, clone, timeout)。

tests::read_process_until_closed1071–1086 ↗
async fn read_process_until_closed(
        backend: &LocalProcess,
        process_id: &ProcessId,
    ) -> ReadResponse

作用:测试辅助函数:反复读取,直到某个进程报告 closed。它用于等待“进程退出且输出流都关完”的最终状态。

数据流:进去后端和进程编号 → 从头开始读;每次如果没 closed,就根据返回的输出块或 next_seq 更新 after_seq,再继续读 → 最终出来 closed 为 true 的 ReadResponse。

调用关系:关闭和清理相关测试会调用它;它内部依赖 read_process_until_change。

调用图:外部调用 1 个(read_process_until_change)。

exec-server/src/remote_process.rs源码 ↗
io_transportrequest handling / teardown

这份文件像一个“遥控器适配器”。项目里有一套统一的进程接口:启动、读取输出、写入输入、发送信号、终止进程。但真正干活的进程不在本地,而在远端执行服务器上。所以这里的 RemoteProcess 先拿到远端客户端,注册一个会话,再请求远端启动进程;启动成功后,用 RemoteExecProcess 代表这个远端进程。之后所有读、写、发信号、结束等操作,都转交给这个会话去做。一个重要细节是:如果启动失败,它会主动注销会话,避免远端留下“空座位”。当 RemoteExecProcess 被丢弃时,也会在后台注销会话,像退房时自动把房卡还回去。

函数细节10
RemoteProcess::new29–32 ↗
fn new(client: LazyRemoteExecServerClient) -> Self

作用:创建一个远端进程后端对象,把远端客户端保存起来,后面启动进程时会用它联系服务器。

数据流:进去的是一个 LazyRemoteExecServerClient,也就是“需要时才真正连上远端服务器的客户端” → 函数把它放进 RemoteProcess 里,并记录一条跟踪日志 → 出来的是一个可以启动远端进程的 RemoteProcess。

调用关系:它通常由 remote_with_transport 在搭好通信通道后调用。它不启动进程,只是先把“遥控器”准备好,后续 RemoteProcess::start 才会真正使用这个客户端。

调用图:被 1 处调用(remote_with_transport);外部调用 1 个(trace!)。

RemoteProcess::start53–55 ↗
fn start(&self, params: ExecParams) -> ExecBackendFuture<'_>

作用:按给定参数启动一个远端进程,并把它包装成项目统一认识的进程对象。

数据流:进去的是 ExecParams,里面包含进程编号和启动参数 → 它先取出远端客户端,再为这个进程编号注册会话,然后请求远端执行;如果执行请求失败,就立刻注销刚注册的会话 → 成功时出来的是 StartedExecProcess,里面装着 RemoteExecProcess,后续可用来读写和控制这个进程。

调用关系:它是 RemoteProcess 实现 ExecBackend 接口时的核心动作。外层代码只知道“我要启动一个进程”,这里负责把这个请求变成远端服务器上的一次会话注册和执行请求,并把后续操作交给 RemoteExecProcess。

调用图:调用 1 个内部函数(get);外部调用 2 个(new, pin)。

RemoteExecProcess::process_id85–87 ↗
fn process_id(&self) -> &crate::ProcessId

作用:返回这个远端进程的编号,让调用方知道自己正在操作的是哪一个进程。

数据流:进去的是当前 RemoteExecProcess 自己 → 它从内部 session,也就是远端会话记录里读取进程编号 → 出来的是这个进程编号的引用,不会修改任何东西。

调用关系:它是 ExecProcess 接口的一部分。外部代码需要标识进程时会调用它,而它只是把问题转交给 session,因为真正的编号保存在会话里。

调用图:外部调用 1 个(process_id)。

RemoteExecProcess::subscribe_wake89–91 ↗
fn subscribe_wake(&self) -> watch::Receiver<u64>

作用:订阅“有新情况了”的提醒,用来让等待读输出的人知道可以醒来看看了。

数据流:进去的是当前 RemoteExecProcess → 它向 session 要一个 watch 接收器;watch 可以理解成一条只保存最新通知的提醒线 → 出来的是 Receiver,调用方可以等它变化。

调用关系:它服务于 ExecProcess 的通知机制。外部读循环可能先订阅这个提醒,等远端会话有新输出或状态变化时,再调用 read 去取具体内容。

调用图:外部调用 1 个(subscribe_wake)。

RemoteExecProcess::subscribe_events93–95 ↗
fn subscribe_events(&self) -> ExecProcessEventReceiver

作用:订阅这个远端进程的事件,比如退出、状态变化等,让调用方不用一直轮询。

数据流:进去的是当前 RemoteExecProcess → 它从 session 里拿到事件接收器 → 出来的是 ExecProcessEventReceiver,调用方可以通过它接收后续事件。

调用关系:它也是 ExecProcess 接口的一部分。真正产生和保存事件的是 session,这个函数只是把事件通道交给需要监听进程状态的上层代码。

调用图:外部调用 1 个(subscribe_events)。

RemoteExecProcess::read97–104 ↗
fn read(
        &self,
        after_seq: Option<u64>,
        max_bytes: Option<usize>,
        wait_ms: Option<u64>,
    ) -> ExecProcessFuture<'_, ReadResponse>

作用:从远端进程读取输出,可以指定从哪个序号之后读、最多读多少、最多等多久。

数据流:进去的是 after_seq、max_bytes、wait_ms:分别表示从哪个输出位置之后开始、最多多少字节、愿意等待多久 → 它把这些条件原样交给 session.read,并包装成异步任务 → 出来的是 ReadResponse,里面是读到的数据和相关状态。

调用关系:外部代码通过 ExecProcess::read 调用它。它自己不直接接触网络细节,而是把读取请求交给 session,由 session 去和远端服务器沟通。

调用图:外部调用 2 个(pin, read)。

RemoteExecProcess::write106–108 ↗
fn write(&self, chunk: Vec<u8>) -> ExecProcessFuture<'_, WriteResponse>

作用:把一段字节写入远端进程,通常就是往进程的标准输入里塞内容。

数据流:进去的是 chunk,一段原始字节 → 它记录一条跟踪日志,然后把字节交给 session.write,并包装成异步任务 → 出来的是 WriteResponse,表示远端是否收下、写入结果如何。

调用关系:外部代码通过 ExecProcess::write 使用它,比如给远端命令输入内容。它的位置像遥控器上的“输入”按钮,真正传输由 session 完成。

调用图:外部调用 3 个(pin, write, trace!)。

RemoteExecProcess::signal110–112 ↗
fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()>

作用:向远端进程发送一个控制信号,比如中断或要求退出。

数据流:进去的是 ProcessSignal,也就是要发给进程的控制指令 → 它记录日志,把信号交给 session.signal,并包装成异步任务 → 成功时没有额外数据返回,只表示信号已经发出或被接受。

调用关系:外部代码通过 ExecProcess::signal 调用它。它不自己解释信号含义,而是把指令交给远端会话,让远端执行服务器对进程采取动作。

调用图:外部调用 3 个(pin, signal, trace!)。

RemoteExecProcess::terminate114–116 ↗
fn terminate(&self) -> ExecProcessFuture<'_, ()>

作用:请求远端结束这个进程,用于明确地停止它。

数据流:进去的是当前 RemoteExecProcess → 它记录日志,调用 session.terminate,并包装成异步任务 → 成功时返回空结果,表示终止请求完成。

调用关系:外部代码通过 ExecProcess::terminate 调用它。它是正常关闭远端进程的通道,实际关闭动作仍由 session 和远端服务器完成。

调用图:外部调用 3 个(pin, terminate, trace!)。

RemoteExecProcess::drop120–125 ↗
fn drop(&mut self)

作用:当这个远端进程对象不再被使用时,自动注销远端会话,避免服务器那边留下无主记录。

数据流:进去的是即将被销毁的 RemoteExecProcess → 它复制一份 session 句柄,然后启动一个后台异步任务去调用 unregister → 函数本身不等待注销完成,也不返回业务结果。

调用关系:这是 Rust 的 Drop 清理钩子,也就是对象离开生命周期时自动执行。它补上最后一道保险:即使调用方忘了手动清理,远端会话也会被安排注销。

调用图:外部调用 2 个(clone, spawn)。

共享启动与后端选择

这些文件暴露通用的 OS 进程启动边界,以及系统可用的顶层 unified-exec 后端族。

core/src/spawn.rs源码 ↗
orchestrationrequest handling / shell tool execution

可以把这个文件理解成“开新进程的门卫”。Codex 要执行外部命令时,不能直接随手启动,因为外部程序可能需要特定目录、特定环境变量,也可能不该访问网络,还可能因为等待键盘输入而卡住。这里的 SpawnChildRequest 像一张申请表,写清楚要启动哪个程序、带哪些参数、在哪个目录运行、网络怎么限制、标准输入输出怎么接。spawn_child_async 会按这张表创建 tokio 的异步子进程。它会先清空默认环境,再只放入指定环境,必要时加入网络代理或“网络被禁用”的标记。对于 shell 工具,它会关闭标准输入,并把输出和错误管道接回来,方便 Codex 收集结果;对于普通继承模式,则让子进程直接使用父进程的输入输出。在 Unix 上,它还会做一些进程组和父进程死亡信号设置,尽量保证 Codex 死掉时,相关子进程也会被收掉,不变成没人管的“孤儿进程”。

函数细节1
spawn_child_async51–126 ↗
async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io::Result<Child>

作用:这个函数根据一份启动请求,真正创建并启动外部子进程。别人用它是为了安全、可控地运行命令,而不是直接把系统默认环境和输入输出都原样交给外部程序。

数据流:进去的是 SpawnChildRequest:里面有程序路径、参数、可选的 arg0、工作目录、网络沙箱策略、网络代理、标准输入输出策略和环境变量。函数先把这些信息拆开,记录一条 trace 调试日志;然后创建 Command,把参数和工作目录放进去;如果有网络代理,就把代理需要的环境变量写入环境;接着清空子进程默认环境,只放入请求指定的环境。如果网络策略表示网络不可用,就额外设置 CODEX_SANDBOX_NETWORK_DISABLED 这个环境变量,让子进程知道网络被关了。在 Unix 系统上,它还会在启动前安排一些进程控制动作,比如需要时脱离终端、在 Linux 上设置“父进程死了就给子进程发终止信号”。最后它按策略设置标准输入、标准输出、标准错误:shell 工具模式会关闭输入并捕获输出;继承模式会直接沿用父进程的输入输出。出来的是一个已经启动的 Child;如果系统启动失败,就返回 io 错误。

调用关系:它处在“准备执行外部命令”的最后一步。上游的 exec 和 spawn_command_under_linux_sandbox 会在需要运行命令时调用它,把已经决定好的程序、参数和沙箱设置交过来。它自己不判断业务上该执行什么命令,而是把启动细节交给 tokio::process::Command,以及标准库的 Stdio;在 Unix/Linux 上还会调用系统相关能力,比如 getpid 和启动前钩子,来保证子进程的输入输出和生命周期符合 Codex 的要求。

调用图:被 2 处调用(exec, spawn_command_under_linux_sandbox);外部调用 7 个(inherit, null, piped, new, getpid, matches!, trace!)。

windows-sandbox-rs/src/unified_exec/backends/mod.rs源码 ↗
orchestrationcompile-time / cross-cutting

这个文件本身不做具体工作,也没有函数。它的作用是把三个子模块挂到“backends”这个名字下面:elevated、legacy 和 windows_common。可以把它想成商场楼层导览图:导览图不卖东西,但它告诉你电梯、老店铺、公共服务区分别在哪里。这里的 elevated 通常表示需要更高权限的执行方式,legacy 表示旧式或兼容用的执行方式,windows_common 表示 Windows 后端之间共用的东西。这个文件的重要性在于整理结构:上层代码只要进入 unified_exec/backends 这个入口,就能按名字找到不同实现,而不用知道文件夹里具体怎么摆放。

可移植 PTY 和管道接口

这些文件提供公共 PTY crate 接口,以及其跨平台进程组、管道和 PTY 支持的会话实现。

utils/pty/src/lib.rs源码 ↗
othercross-cutting

这个文件本身不做复杂计算,更像一家店的前台目录:它把内部几个模块整理好,对外暴露一组好用的入口。这里区分了两类启动方式:一种是普通管道,也就是用标准输入、标准输出、标准错误来和子进程说话;另一种是 PTY(伪终端,可以把程序骗成“我正在真实终端里运行”),适合 shell、编辑器这类交互式程序。它还公开了进程句柄、退出结果、终端尺寸、信号等通用概念,让调用者不用关心内部文件怎么拆。DEFAULT_OUTPUT_BYTES_CAP 给输出缓存设了默认上限,避免子进程疯狂打印时把内存撑爆。文件里还有一些旧名字的别名,保证老代码升级后不容易坏。Windows 上才有的 ConPTY 相关能力也只在 Windows 编译时开放。

utils/pty/src/process_group.rs源码 ↗
utilprocess spawn, signal handling, teardown

很多命令不是只启动一个进程,它还可能再启动孩子、孙子进程。只杀最外层那个进程,里面的进程可能还在跑,就像只关了总店门,分店还在营业。这个文件用“进程组”(操作系统里把一批相关进程放到同一个组)来解决这个问题:启动子进程前,让它成为自己组的头;需要停止时,对整个组发信号,而不是只找一个进程。它还提供“脱离终端”的能力,防止后台命令误用当前终端;在 Linux 上还能设置“父进程死了就通知子进程退出”。在非 Unix 系统上,这些函数大多安全地什么也不做,保证同一套代码能跨平台编译。

函数细节9
set_parent_death_signal43–45 ↗
fn set_parent_death_signal(_parent_pid: i32) -> io::Result<()>

作用:在 Linux 上,它给即将启动的子进程加一道保险:如果原来的父进程死了,子进程会收到 SIGTERM(一个礼貌要求退出的信号)。这样主程序崩了或退出了,外部命令不容易偷偷留在后台。

数据流:进去的是启动前记下来的父进程编号 → 它让操作系统记住“父进程死时给我发 SIGTERM”,然后再检查现在的父进程是不是还是原来的那个 → 如果父进程已经在这段空隙里死了,它会立刻给自己发退出信号;成功时返回空结果,失败时返回操作系统错误。非 Linux 平台上则直接成功返回,不做实际动作。

调用关系:这个函数通常会在创建子进程的 pre_exec 阶段使用,也就是子进程刚 fork 出来、还没真正执行目标命令前。它直接和操作系统接口打交道,不把工作再交给本文件的其他函数。

调用图:外部调用 4 个(last_os_error, getppid, prctl, raise)。

detach_from_tty63–65 ↗
fn detach_from_tty() -> io::Result<()>

作用:让子进程脱离当前控制终端,适合启动不需要互动的后台命令。这样它不会继续占着或受当前终端影响。

数据流:进去没有业务参数 → 它尝试让当前进程开启一个新会话,也就是从原来的终端环境里分出去 → 如果操作系统说当前进程不能这么做,它会退一步调用 set_process_group,把进程至少放进自己的进程组;最后返回成功或具体错误。非 Unix 平台上不做事并返回成功。

调用关系:它一般在启动子进程前的准备阶段被调用。它自己先找操作系统创建新会话;如果遇到权限限制,会把后续处理交给 set_process_group,尽量仍然把子进程隔离出来。

调用图:调用 1 个内部函数(set_process_group);外部调用 2 个(last_os_error, setsid)。

set_process_group82–84 ↗
fn set_process_group() -> io::Result<()>

作用:把当前进程放进一个新的进程组里,通常让刚启动的子进程成为这一组的组长。以后要停止这个命令及它拉起的其他进程,就能按组处理。

数据流:进去没有参数 → 它调用操作系统把当前进程的进程组设置成自己这一组 → 成功就返回空结果,失败就把操作系统错误带回来。非 Unix 平台上只是成功返回。

调用关系:它是启动子进程时的基础小工具。detach_from_tty 在无法创建新会话时会调用它,作为较弱但仍有用的隔离办法。

调用图:被 1 处调用(detach_from_tty);外部调用 2 个(last_os_error, setpgid)。

kill_process_group_by_pid116–118 ↗
fn kill_process_group_by_pid(_pid: u32) -> io::Result<()>

作用:给一个进程编号,它会先找到这个进程所在的进程组,然后强制杀掉整个组。它适合只知道子进程 PID(进程编号),但想清掉它和它派生出的所有相关进程的场景。

数据流:进去的是一个 PID → 它向操作系统查询这个 PID 属于哪个进程组 → 如果组还存在,就向整个组发送 SIGKILL(强制杀死信号);如果进程或组已经没了,就当作已经清理完成;如果是其他系统错误,则返回错误。

调用关系:kill_child_process_group 会用它来处理 tokio 启动的子进程。它本身直接调用操作系统查询进程组并发送信号,是按 PID 清理整组进程的核心步骤。

调用图:被 1 处调用(kill_child_process_group);外部调用 3 个(last_os_error, getpgid, killpg)。

signal_process_group_id121–134 ↗
fn signal_process_group_id(pgid: libc::pid_t, signal: libc::c_int) -> io::Result<bool>

作用:这是本文件里给进程组发信号的通用小工具。它不关心信号具体是“礼貌退出”“键盘中断”还是“强制杀死”,只负责把指定信号送到指定进程组。

数据流:进去的是进程组编号和一个信号编号 → 它调用操作系统把这个信号发给整个进程组 → 如果组存在并收到信号,返回 true;如果组已经不存在,返回 false;如果发生别的错误,就返回错误。

调用关系:terminate_process_group、interrupt_process_group 和 kill_process_group 都把实际发信号的工作交给它。它相当于统一的发信号出口,避免每种信号都重复写一遍同样的错误处理。

调用图:被 3 处调用(interrupt_process_group, kill_process_group, terminate_process_group);外部调用 2 个(last_os_error, killpg)。

terminate_process_group147–149 ↗
fn terminate_process_group(_process_group_id: u32) -> io::Result<bool>

作用:向指定进程组发送 SIGTERM,也就是“请你们正常退出”的信号。它比强制杀死温和,给程序机会保存数据、收尾。

数据流:进去的是进程组编号 → 它把这个编号和 SIGTERM 交给 signal_process_group_id → 出来是 true 或 false,表示这个组是否还存在并收到了信号;如果操作系统报了严重错误,则返回错误。

调用关系:更高层的 terminate_process_tree 和 terminate 会在需要优雅停止一棵进程树时调用它。它不直接碰操作系统,而是复用 signal_process_group_id 的统一发送逻辑。

调用图:调用 1 个内部函数(signal_process_group_id);被 2 处调用(terminate_process_tree, terminate)。

interrupt_process_group159–161 ↗
fn interrupt_process_group(_process_group_id: u32) -> io::Result<()>

作用:向指定进程组发送 SIGINT,也就是类似用户按 Ctrl+C 的中断信号。它用于模拟“请当前命令停下来”的交互式操作。

数据流:进去的是进程组编号 → 它把编号和 SIGINT 交给 signal_process_group_id → 不关心目标组是否已经消失,只要没有严重错误就返回成功;有系统错误时返回错误。

调用关系:上层的 signal 流程会在用户或程序要求发送中断时调用它。它把具体发信号的活儿交给 signal_process_group_id,自己只负责选择 SIGINT 这个信号。

调用图:调用 1 个内部函数(signal_process_group_id);被 3 处调用(signal, signal, signal)。

kill_process_group171–173 ↗
fn kill_process_group(_process_group_id: u32) -> io::Result<()>

作用:向指定进程组发送 SIGKILL,也就是不给商量余地的强制杀死信号。它通常用于温和停止无效、或者必须立刻清理时。

数据流:进去的是进程组编号 → 它把编号和 SIGKILL 交给 signal_process_group_id → 如果组不存在也当作清理完成;如果出现其他系统错误,就返回错误。

调用关系:drop、kill_process_tree 和多个 kill 流程会在最终清理或强制终止时调用它。它复用 signal_process_group_id,只负责选择最强硬的 SIGKILL 信号。

调用图:调用 1 个内部函数(signal_process_group_id);被 5 处调用(drop, kill_process_tree, kill, kill, kill)。

kill_child_process_group187–189 ↗
fn kill_child_process_group(_child: &mut Child) -> io::Result<()>

作用:专门处理 tokio 子进程对象:如果这个子进程还有 PID,就杀掉它所在的整个进程组。tokio 是 Rust 里常用的异步运行库,这里的 Child 表示异步启动出来的外部进程。

数据流:进去的是一个可变的 Child 对象 → 它先从对象里取出进程编号 → 如果能取到,就调用 kill_process_group_by_pid 清理整组;如果取不到,说明进程可能还没启动成功或已经结束,就直接返回成功。

调用关系:它是异步子进程清理时的便捷入口。它不自己查进程组或发信号,而是拿到 PID 后把核心清理工作交给 kill_process_group_by_pid。

调用图:调用 1 个内部函数(kill_process_group_by_pid);外部调用 1 个(id)。

utils/pty/src/pipe.rs源码 ↗
io_transportrequest handling / process lifetime

这个文件解决的是“我要运行一个命令,并可靠地和它说话”的问题。这里不用 PTY(伪终端,像假装有个终端窗口),而是用普通管道:一根管子往子进程 stdin 写数据,两根管子分别读 stdout 和 stderr。核心函数会设置工作目录、环境变量、参数、stdin 是否打开,然后启动子进程。启动后,它会开几个异步任务:一个负责把上层发来的字节写进子进程,一个负责读 stdout,一个负责读 stderr,还有一个等子进程结束并记录退出码。它还准备了终止器,外部要中断或杀掉进程时,可以按 Unix 的进程组或 Windows 的进程号来处理。整体像一个插线板:把外部命令的几根线整理成统一的 ProcessHandle,交给项目其他地方使用。

函数细节8
PipeChildTerminator::signal37–51 ↗
fn signal(&mut self, signal: ProcessSignal) -> io::Result<()>

作用:这个函数给已经启动的子进程发送“温和一点”的信号,比如中断。它的作用类似按下 Ctrl+C,让程序有机会自己收尾退出。

数据流:进去的是一个信号类型;它判断是不是 Interrupt。若在 Unix 系统上,就把中断发给整个进程组;若系统不支持这种信号,就返回一个“不支持”的错误。它不产生新数据,只反馈发送是否成功。

调用关系:它是 ProcessHandle 里终止器的一部分。上层想中断管道启动的进程时,会走到这里;这里再把活交给 interrupt_process_group,或者在不支持的平台上交给 unsupported_signal 生成错误。

调用图:调用 2 个内部函数(unsupported_signal, interrupt_process_group)。

PipeChildTerminator::kill53–68 ↗
fn kill(&mut self) -> io::Result<()>

作用:这个函数强制杀掉子进程。它用于程序不响应正常中断、必须立刻结束时。

数据流:进去不需要额外参数,因为它已经记住了进程组编号或进程号。Unix 上它杀整个进程组;Windows 上它按进程号杀单个进程;其他平台上这里什么也不做并返回成功。

调用关系:它同样挂在 ProcessHandle 的终止器上。外部要求强制停止时会调用它;Unix 路径交给 kill_process_group,Windows 路径交给本文件里的 kill_process。

调用图:调用 2 个内部函数(kill_process, kill_process_group)。

kill_process72–87 ↗
fn kill_process(pid: u32) -> io::Result<()>

作用:这是 Windows 专用的杀进程小工具。因为 Windows 没有这里使用的 Unix 进程组方式,所以需要用 Windows 系统接口按进程号终止。

数据流:进去的是进程号 pid。它先向系统申请一个能终止该进程的句柄,失败就返回系统错误;成功后调用终止进程接口,再关闭句柄;最后把成功或失败返回给调用者。

调用关系:它只在 Windows 编译时存在,并由 PipeChildTerminator::kill 调用。它把项目里的“杀掉进程”动作翻译成 OpenProcess、TerminateProcess、CloseHandle 这些 Windows 原生调用。

调用图:被 1 处调用(kill);外部调用 4 个(last_os_error, CloseHandle, OpenProcess, TerminateProcess)。

read_output_stream89–104 ↗
async fn read_output_stream(mut reader: R, output_tx: mpsc::Sender<Vec<u8>>)

作用:这个函数持续读取子进程的一条输出流,比如 stdout 或 stderr,并把读到的字节块送到通道里。它让上层可以异步收到命令输出,而不是卡在那里等。

数据流:进去的是一个可异步读取的 reader,以及一个发送输出的通道。它每次最多读 8192 字节;读到内容就复制成一块 Vec<u8> 发出去;读到结尾就停止;如果只是被系统临时打断就重试;其他读取错误就结束。

调用关系:spawn_process_with_stdin_mode 启动子进程后,会为 stdout 和 stderr 分别开任务调用它。它不直接处理业务含义,只负责把原始字节从管道搬到 mpsc 通道。

调用图:外部调用 3 个(read, send, vec!)。

spawn_process_with_stdin_mode112–264 ↗
async fn spawn_process_with_stdin_mode(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
    stdin_mode: PipeStdinMode,
    inherit

作用:这是本文件真正的核心启动器。它按指定方式启动外部程序,并把 stdin、stdout、stderr、退出码、终止能力都包装成统一的 SpawnedProcess。

数据流:进去的是程序名、参数、工作目录、环境变量、可选 arg0、stdin 模式,以及 Unix 上要保留的文件描述符。它检查程序名,创建 Command,设置目录和环境,决定 stdin 是管道还是空输入,接好 stdout/stderr,然后启动子进程。启动后,它建立写入通道、两个输出通道、读写异步任务、等待退出的任务,并记录退出状态和退出码。出来的是 SpawnedProcess;同时后台任务已经开始搬运输入输出和等待进程结束。

调用关系:spawn_process 和 spawn_process_no_stdin_with_inherited_fds 都只是不同参数的外壳,最后都会调用它。它内部会调用 read_output_stream 读取输出,用 exit_code_from_status 把系统退出状态变成项目使用的数字退出码,并创建 PipeChildTerminator 让 ProcessHandle 以后能中断或杀掉进程。

调用图:调用 1 个内部函数(exit_code_from_status);被 2 处调用(spawn_process, spawn_process_no_stdin_with_inherited_fds);外部调用 13 个(clone, new, new, new, new, null, piped, new, bail!, new (+3 more))。

spawn_process267–275 ↗
async fn spawn_process(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
) -> Result<SpawnedProcess>

作用:这是最常用的启动入口:用普通管道启动程序,并保留可写的 stdin。需要和子进程互动、给它喂输入时会用它。

数据流:进去的是程序名、参数、目录、环境变量和可选 arg0。它不自己启动进程,而是把 stdin 模式设为 Piped,再转交给 spawn_process_with_stdin_mode。出来的是同样的 SpawnedProcess。

调用关系:它是一个方便入口,把“我要有 stdin 管道”这个选择固定下来。真正复杂的启动、读写、等待和终止设置,都交给 spawn_process_with_stdin_mode。

调用图:调用 1 个内部函数(spawn_process_with_stdin_mode)。

spawn_process_no_stdin278–286 ↗
async fn spawn_process_no_stdin(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
) -> Result<SpawnedProcess>

作用:这个函数启动外部程序,但一开始就关闭它的 stdin。适合那些不需要输入的命令,避免子进程一直等用户输入。

数据流:进去的是程序名、参数、目录、环境变量和可选 arg0。它补上一个空的 inherited_fds 列表,然后调用 spawn_process_no_stdin_with_inherited_fds。出来的是 SpawnedProcess。

调用关系:它是更简单的无 stdin 入口。它自己不碰底层启动细节,而是把工作交给 spawn_process_no_stdin_with_inherited_fds,再由后者交给核心启动函数。

调用图:调用 1 个内部函数(spawn_process_no_stdin_with_inherited_fds)。

spawn_process_no_stdin_with_inherited_fds290–308 ↗
async fn spawn_process_no_stdin_with_inherited_fds(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
    inherited_fds: &[i32],
) -

作用:这个函数启动外部程序并关闭 stdin,同时在 Unix 上允许指定少数文件描述符继续传给子进程。文件描述符可以理解成系统打开文件、管道等资源时给的编号。

数据流:进去的是程序信息、运行环境、可选 arg0,以及要保留的 inherited_fds。它把 stdin 模式设为 Null,并把这些文件描述符传给 spawn_process_with_stdin_mode。出来的是包装好的 SpawnedProcess。

调用关系:它被 spawn_process_no_stdin 作为简化入口调用,也会被 open_session_with_exec_env 以及相关测试使用。它的价值是把“无 stdin”和“保留特定底层资源”这两个需求,统一交给 spawn_process_with_stdin_mode 执行。

调用图:调用 1 个内部函数(spawn_process_with_stdin_mode);被 3 处调用(open_session_with_exec_env, spawn_process_no_stdin, pipe_spawn_no_stdin_can_preserve_inherited_fds)。

utils/pty/src/pty.rs源码 ↗
io_transportrequest handling / process lifetime

普通管道只能像水管一样传字节,但终端程序还需要“我是在终端里”的感觉,比如窗口大小、Ctrl-C、中断信号、回显等。这个文件就是给子进程搭一个 PTY(伪终端,可以理解成程序用的“假键盘和假屏幕”)。它会创建终端主从两端:父进程拿主端读写,子进程拿从端当自己的输入输出。然后它开几个后台任务:一个负责把终端输出读出来,一个负责把外部写入的数据送进去,一个负责等待子进程退出。它还处理不同系统的差异:Windows 用 ConPTY,Unix 用 native PTY;Unix 上如果需要保留某些文件描述符,还会走更底层的启动方式,并清理不该泄漏给子进程的文件句柄。终止进程时,它尽量按进程组杀掉整棵子进程,避免交互式 shell 里启动的孙进程偷偷留下。

函数细节13
conpty_supported49–51 ↗
fn conpty_supported() -> bool

作用:检查当前系统是否支持 ConPTY。ConPTY 是 Windows 上的伪终端能力,也就是让程序能像在真实命令行窗口里运行一样。

数据流:输入为空 → 在 Windows 上把问题交给 Windows 专用检查函数;在非 Windows 系统上直接认为可用 → 返回 true 或 false,告诉上层能不能走伪终端方案。

调用关系:这是启动 PTY 前的能力判断。Windows 版本会调用外部的 conpty_supported;非 Windows 不需要额外检查,因为这里默认本机 PTY 可用。

调用图:外部调用 1 个(conpty_supported)。

PtyChildTerminator::signal60–71 ↗
fn signal(&mut self, signal: ProcessSignal) -> std::io::Result<()>

作用:给通过 portable-pty 启动的子进程发送“温和信号”,目前主要是中断信号,也就是类似用户按下 Ctrl-C。

数据流:输入是一个 ProcessSignal,例如 Interrupt → 如果在 Unix 上记录了进程组编号,就向整个进程组发送中断;否则说明这个信号在当前情况下不支持 → 返回成功或一个不支持信号的错误。

调用关系:它是 ProcessHandle 需要停止或打断 PTY 子进程时会用到的终止器方法。它会把 Unix 上的中断动作交给 interrupt_process_group;如果做不到,就用 unsupported_signal 生成错误。

调用图:调用 2 个内部函数(unsupported_signal, interrupt_process_group)。

PtyChildTerminator::kill73–90 ↗
fn kill(&mut self) -> std::io::Result<()>

作用:强制结束通过 portable-pty 启动的子进程。它不只盯着直接子进程,还尽量连同这个终端里派生出的后代进程一起清掉。

数据流:输入为空,但它读取自己保存的 child killer 和 Unix 进程组编号 → 如果有进程组编号,先杀进程组,再尝试杀直接子进程;如果没有,就只调用 portable-pty 提供的 killer → 返回是否成功杀掉进程,或返回系统错误。

调用关系:这是关闭会话、清理进程时的重要兜底。它会调用 kill_process_group 来清整组进程,同时也调用 portable-pty 的直接 child killer,避免缓存的进程组信息过期时漏杀。

调用图:调用 1 个内部函数(kill_process_group)。

RawPidTerminator::signal100–106 ↗
fn signal(&mut self, signal: ProcessSignal) -> std::io::Result<()>

作用:给用底层 Unix 方式启动的 PTY 子进程发送中断信号。这里直接按进程组处理,因为这种启动方式自己掌握了子进程的进程组编号。

数据流:输入是 ProcessSignal,目前支持 Interrupt → 使用保存的 process_group_id 向整组进程发送中断 → 返回系统调用是否成功。

调用关系:它服务于 spawn_process_preserving_fds 创建出来的进程句柄。中断动作交给 interrupt_process_group,这样 shell 里再启动的程序也能一起收到 Ctrl-C。

调用图:调用 1 个内部函数(interrupt_process_group)。

RawPidTerminator::kill108–110 ↗
fn kill(&mut self) -> std::io::Result<()>

作用:强制杀掉用底层 Unix 方式启动的 PTY 子进程组。它的目标是防止交互式程序退出后留下后台残留进程。

数据流:输入为空,但读取保存的 process_group_id → 调用系统层的 kill_process_group 杀掉这一组进程 → 返回成功或失败。

调用关系:它是 RawPidTerminator 对 ProcessHandle 提供的强制关闭能力。spawn_process_preserving_fds 会把它装进进程句柄,后续会话结束时由句柄调用它。

调用图:调用 1 个内部函数(kill_process_group)。

platform_native_pty_system113–123 ↗
fn platform_native_pty_system() -> Box<dyn portable_pty::PtySystem + Send>

作用:按操作系统选出真正用来创建 PTY 的底层实现。简单说,它是“我要开一个假终端,具体找谁办”的选择器。

数据流:输入为空 → Windows 上创建项目自己的 ConPtySystem;非 Windows 上使用 portable-pty 提供的 native_pty_system → 返回一个统一的 PtySystem 对象,后面可以用它 openpty。

调用关系:spawn_process_portable 在真正创建终端前会先调用它。它屏蔽了 Windows 和 Unix 的差异,让后面的启动流程不用到处写系统判断。

调用图:被 1 处调用(spawn_process_portable);外部调用 3 个(new, native_pty_system, default)。

spawn_process126–135 ↗
async fn spawn_process(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
    size: TerminalSize,
) -> Result<SpawnedProcess>

作用:启动一个普通的 PTY 子进程,不额外保留特殊文件描述符。大多数调用者只需要用这个简单入口。

数据流:输入包括程序名、参数、工作目录、环境变量、可选的 arg0 和终端大小 → 把这些原样交给 spawn_process_with_inherited_fds,并传入空的继承文件描述符列表 → 返回 SpawnedProcess,里面有写输入、读输出、等待退出所需的通道和句柄。

调用关系:它是方便入口,自己不做复杂判断。真正分流到 portable 启动还是 Unix 特殊启动的工作,由 spawn_process_with_inherited_fds 完成。

调用图:调用 1 个内部函数(spawn_process_with_inherited_fds)。

spawn_process_with_inherited_fds139–162 ↗
async fn spawn_process_with_inherited_fds(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
    size: TerminalSize,
    inherited_f

作用:启动 PTY 子进程,并且在 Unix 上可以指定哪些文件描述符要留给子进程。文件描述符可以理解成系统打开文件、管道或连接时发的小票号。

数据流:输入是程序启动信息、终端大小,以及要继承的 fd 列表 → 先检查程序名不能为空;非 Unix 忽略 fd 列表;Unix 上如果 fd 列表不空,就走保留 fd 的专用启动流程,否则走通用 portable 流程 → 输出一个 SpawnedProcess,或者在缺少程序名等情况下返回错误。

调用关系:这是外部调用最关键的分发点。open_session_with_exec_env 和多个测试会调用它;它根据是否需要继承 fd,把工作交给 spawn_process_portable 或 spawn_process_preserving_fds。

调用图:调用 2 个内部函数(spawn_process_portable, spawn_process_preserving_fds);被 6 处调用(open_session_with_exec_env, spawn_process, pty_preserving_inherited_fds_keeps_python_repl_running, pty_spawn_can_preserve_inherited_fds, pty_spawn_with_inherited_fds_reports_exec_failures, pty_spawn_with_inherited_fds_supports_resize);外部调用 1 个(bail!)。

spawn_process_portable164–278 ↗
async fn spawn_process_portable(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
    size: TerminalSize,
) -> Result<SpawnedProces

作用:用 portable-pty 这套跨平台工具启动一个带 PTY 的子进程。这是默认路线,适合不需要特殊 Unix 文件描述符处理的情况。

数据流:输入是程序、参数、目录、环境变量、arg0 和终端大小 → 创建 PTY,配置命令,启动子进程;同时创建输入通道、输出通道、退出通知通道;后台读主端输出、写主端输入、等待子进程退出;最后把这些能力包装成 ProcessHandle 和 SpawnedProcess → 调用者拿到后就可以像操作一个终端会话一样读写和关闭。

调用关系:它由 spawn_process_with_inherited_fds 在普通情况下调用。它先找 platform_native_pty_system 拿到底层 PTY 系统,再把启动出来的进程交给 PtyChildTerminator 管停止和杀进程。

调用图:调用 1 个内部函数(platform_native_pty_system);被 1 处调用(spawn_process_with_inherited_fds);外部调用 14 个(clone, new, new, new, new, new, new, cfg!, new, spawn (+4 more))。

spawn_process_preserving_fds281–432 ↗
async fn spawn_process_preserving_fds(
    program: &str,
    args: &[String],
    cwd: &Path,
    env: &HashMap<String, String>,
    arg0: &Option<String>,
    size: TerminalSize,
    inherited_fds:

作用:这是 Unix 专用的更底层启动路线,用来在启动 PTY 子进程时保留指定文件描述符。它解决的问题是:有些场景既要像终端运行,又不能把调用者特意传下去的 fd 关掉。

数据流:输入是程序启动信息、终端大小和需要保留的 fd 列表 → 自己打开 Unix PTY,把 PTY 从端接到子进程的 stdin、stdout、stderr;在 exec 前重置信号、创建新会话、设置控制终端,并关闭不该继承的 fd;启动后创建读输出、写输入、等退出的后台任务 → 返回带有会话句柄、输出接收器和退出通知的 SpawnedProcess。

调用关系:spawn_process_with_inherited_fds 在 Unix 且 inherited_fds 非空时会调用它。它依赖 open_unix_pty 创建终端,pre_exec 阶段调用 close_inherited_fds_except 做 fd 清理,并用 RawPidTerminator 管中断和强杀。

调用图:调用 1 个内部函数(open_unix_pty);被 1 处调用(spawn_process_with_inherited_fds);外部调用 13 个(clone, new, new, new, to_vec, new, new, from, new, new (+3 more))。

open_unix_pty435–463 ↗
fn open_unix_pty(size: TerminalSize) -> Result<(File, File)>

作用:在 Unix 上打开一对 PTY 文件:一个主端给父进程读写,一个从端给子进程当终端。

数据流:输入是 TerminalSize,也就是终端行数和列数 → 调用系统的 openpty 创建 master 和 slave 两个文件描述符,并设置初始窗口大小;然后给两个 fd 设置 close-on-exec 标记,避免它们被不小心传给新程序 → 返回两个 File 对象,分别代表 PTY 主端和从端。

调用关系:它只服务于 spawn_process_preserving_fds 这条 Unix 特殊路线。它会调用 set_cloexec 给新开的 fd 加保险;如果 openpty 失败,就直接返回错误。

调用图:调用 1 个内部函数(set_cloexec);被 1 处调用(spawn_process_preserving_fds);外部调用 5 个(from_raw_fd, bail!, openpty, addr_of_mut!, null_mut)。

set_cloexec466–476 ↗
fn set_cloexec(fd: RawFd) -> std::io::Result<()>

作用:给一个文件描述符设置 close-on-exec。意思是:当前进程可以用它,但以后启动新程序时,默认不要把它漏传过去。

数据流:输入是一个 RawFd,也就是 Unix 的文件描述符编号 → 先读取它现有的 fd 标志,再把 FD_CLOEXEC 标志加上写回去 → 成功返回空结果,失败返回系统错误。

调用关系:open_unix_pty 创建 PTY 后马上调用它。它用 fcntl 这个 Unix 系统接口读写标志,出错时用 last_os_error 把系统错误带回来。

调用图:被 1 处调用(open_unix_pty);外部调用 2 个(last_os_error, fcntl)。

close_inherited_fds_except479–507 ↗
fn close_inherited_fds_except(preserved_fds: &[RawFd])

作用:在子进程真正执行新程序前,关闭那些不该被继承的文件描述符,只留下标准输入输出错误和明确要求保留的 fd。

数据流:输入是 preserved_fds,也就是允许保留的 fd 列表 → 扫描 /dev/fd 里当前打开的 fd;跳过 0、1、2 和保留列表里的 fd;也跳过已经带 FD_CLOEXEC 的 fd,因为标准库可能还要用它报告启动失败;其余 fd 收集后逐个关闭 → 不返回数据,但会改变即将执行程序时能看到的打开文件集合。

调用关系:它在 spawn_process_preserving_fds 的 pre_exec 阶段运行,也就是子进程 fork 后、exec 新程序前。它是防止资源泄漏的清洁工,但会小心不关掉调用者指定要传下去的 fd。

调用图:外部调用 5 个(contains, new, close, fcntl, read_dir)。

Windows ConPTY 实现

这些文件提供 PTY 层使用的 Windows 专用伪控制台、子进程封装器和 ConPTY 系统集成。

utils/pty/src/win/mod.rs源码 ↗
io_transportWindows PTY 子进程运行期间,从启动后查询状态,到等待结束或强制结束时都会用到

这个文件把 Windows 原生的进程句柄,包装成 portable_pty 这套通用接口能理解的“子进程”。可以把它想成一个翻译员:上层代码只说“等这个程序结束”“杀掉它”“给我进程号”,它负责把这些话翻译成 Windows API 调用。核心类型是 WinChild,里面用互斥锁(一把锁,防止多个任务同时动同一个句柄)保护进程句柄;WinChildKiller 是一个单独的“遥控器”,可以复制出去专门用来杀进程。这里还有一个很重要的修正:Windows 的 TerminateProcess 返回非 0 才表示成功,原上游代码把成功和失败看反了,这里特意改正,避免杀进程结果被颠倒。

函数细节11
WinChild::is_complete61–74 ↗
fn is_complete(&mut self) -> IoResult<Option<ExitStatus>>

作用:检查这个 Windows 子进程是不是已经结束了。如果还在运行,就告诉调用者“还没结束”;如果结束了,就给出退出码。

数据流:进去的是 WinChild 里保存的进程句柄 → 它向 Windows 查询进程退出码 → 如果 Windows 说进程仍在运行,就返回 None;如果已经退出,就把退出码包装成 ExitStatus 返回;如果查询失败,这里按“还没得到结果”处理。

调用关系:这是查询状态的底层小工具。WinChild::try_wait 直接用它做非阻塞检查;WinChild::poll 也先用它判断异步等待是否已经可以完成。

调用图:被 2 处调用(poll, try_wait);外部调用 1 个(with_exit_code)。

WinChild::do_kill76–85 ↗
fn do_kill(&mut self) -> IoResult<()>

作用:真正执行“杀掉这个子进程”的动作。它直接调用 Windows 的 TerminateProcess。

数据流:进去的是 WinChild 保存的进程句柄 → 它复制一份句柄并交给 Windows 终止进程 → 如果 Windows 返回 0,就把系统错误拿出来返回;如果返回非 0,就表示终止成功。

调用关系:它是 WinChild::kill 背后的实际执行者。这里特别修正了 Windows 返回值的含义,避免把成功当失败、失败当成功。

调用图:被 1 处调用(kill);外部调用 1 个(last_os_error)。

WinChild::kill89–92 ↗
fn kill(&mut self) -> IoResult<()>

作用:给外部提供“杀掉这个子进程”的统一接口。它会尝试终止进程,但最后总是返回成功。

数据流:进去的是一个可变的 WinChild → 它调用 WinChild::do_kill 尝试结束进程 → 不管 do_kill 有没有报错,它都会吞掉错误并返回 Ok。

调用关系:这是 portable_pty 的 ChildKiller 接口要求的方法。上层代码想结束 WinChild 时会调用它,它把具体工作交给 WinChild::do_kill。

调用图:调用 1 个内部函数(do_kill)。

WinChild::clone_killer94–97 ↗
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync>

作用:复制出一个专门用来杀这个进程的“小遥控器”。这样别的线程或组件不用拿到整个 WinChild,也能请求终止进程。

数据流:进去的是当前 WinChild 的进程句柄 → 它在锁里复制一份 Windows 句柄 → 用这份句柄创建 WinChildKiller,并装进通用的 ChildKiller 盒子里返回。

调用关系:这是 ChildKiller 接口的一部分。需要把“杀进程能力”交给别处时,上层会调用它;它产出的 WinChildKiller 后续会执行自己的 kill。

调用图:外部调用 1 个(new)。

WinChildKiller::kill106–114 ↗
fn kill(&mut self) -> IoResult<()>

作用:用 WinChildKiller 保存的句柄强制结束对应的 Windows 进程。它是被复制出去的杀进程入口。

数据流:进去的是 WinChildKiller 里的进程句柄 → 它把句柄交给 Windows 的 TerminateProcess → 返回 0 时取系统错误并返回失败,返回非 0 时表示成功。

调用关系:这是复制版“杀进程遥控器”的主要动作。它不依赖 WinChild 本体,所以适合被其他线程或异步任务拿去在需要时终止进程。

调用图:外部调用 2 个(last_os_error, as_raw_handle)。

WinChildKiller::clone_killer116–119 ↗
fn clone_killer(&self) -> Box<dyn ChildKiller + Send + Sync>

作用:再复制一个 WinChildKiller。用途是把同一个杀进程能力继续分发给更多地方。

数据流:进去的是当前 WinChildKiller 保存的句柄 → 它复制这份句柄 → 用复制出的句柄创建新的 WinChildKiller,并作为通用 ChildKiller 返回。

调用关系:这是 ChildKiller 接口的一部分。调用者如果需要多个地方都能终止同一个进程,就会通过它继续复制杀手柄。

调用图:外部调用 2 个(new, try_clone)。

WinChild::try_wait123–125 ↗
fn try_wait(&mut self) -> IoResult<Option<ExitStatus>>

作用:非阻塞地看一眼子进程有没有结束。它不会卡住当前线程。

数据流:进去的是 WinChild → 它调用 WinChild::is_complete 查询状态 → 如果进程结束,返回退出状态;如果没结束,返回 None;如果查询过程中有错误,则返回错误。

调用关系:这是 Child 接口里的“试着等一下”。WinChild::wait 会先调用它,避免在进程其实已经结束时还去做一次阻塞等待。

调用图:调用 1 个内部函数(is_complete);被 1 处调用(wait)。

WinChild::wait127–142 ↗
fn wait(&mut self) -> IoResult<ExitStatus>

作用:一直等到子进程结束,并返回它的退出状态。适合调用者明确需要“等它跑完再继续”的场景。

数据流:进去的是 WinChild → 它先用 WinChild::try_wait 快速检查是否已经结束 → 如果没结束,就调用 Windows 的 WaitForSingleObject 一直等 → 等到结束后再查询退出码,成功就返回 ExitStatus,失败就返回系统错误。

调用关系:这是 Child 接口里的阻塞等待方法。它会先借用 try_wait 的结果,必要时再交给 Windows 的等待函数来真正挂起等待。

调用图:调用 1 个内部函数(try_wait);外部调用 3 个(with_exit_code, last_os_error, WaitForSingleObject)。

WinChild::process_id144–147 ↗
fn process_id(&self) -> Option<u32>

作用:取出这个子进程在 Windows 里的进程号。进程号可以用来显示、记录日志,或让别的工具识别它。

数据流:进去的是 WinChild 里的进程句柄 → 它调用 Windows 查询对应的进程 ID → 如果查到的是 0,就返回 None;否则返回具体的 u32 进程号。

调用关系:这是 Child 接口提供给上层的信息查询方法。它不启动、不等待、也不杀进程,只负责把 Windows 句柄翻译成可读的进程号。

WinChild::as_raw_handle149–152 ↗
fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle>

作用:把内部保存的 Windows 原始句柄拿出来。这个方法给少数需要直接调用底层 Windows API 的代码使用。

数据流:进去的是 WinChild → 它加锁读取内部 OwnedHandle → 返回里面的 RawHandle;不会复制句柄,也不会改变进程状态。

调用关系:这是 Child 接口里的底层逃生口。一般上层不用碰,但如果某段代码必须和 Windows 原生接口对接,就可以通过它拿到原始句柄。

WinChild::poll158–174 ↗
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<anyhow::Result<ExitStatus>>

作用:让 WinChild 可以被当作异步任务来等待。也就是说,上层可以不用堵住当前执行流,而是在进程结束时再被叫醒。

数据流:进去的是异步运行时传来的轮询上下文和 WinChild → 它先用 WinChild::is_complete 看进程是否已经结束 → 如果结束就立刻返回结果;如果出错就返回带说明的错误;如果还没结束,就复制进程句柄,启动一个线程等待 Windows 通知,等到进程结束后唤醒当前异步任务,并先返回 Pending。

调用关系:这是 Rust Future 接口的方法。异步运行时轮询 WinChild 时会调用它;它先复用 is_complete,必要时再把阻塞等待交给新线程,并通过 waker 通知运行时稍后再来检查。

调用图:调用 1 个内部函数(is_complete);外部调用 3 个(waker, Ready, spawn)。

utils/pty/src/win/psuedocon.rs源码 ↗
io_transport启动检查、创建终端、运行中调整大小、启动子进程、退出清理

Windows 10 新版提供了 ConPTY,也就是“伪控制台”:它像一间假终端房间,程序把 cmd、powershell 或其他命令放进去运行,再通过管道读写内容。这个文件负责检查系统够不够新,动态加载 Windows 提供的相关函数,创建和关闭伪控制台,调整它的行列大小,并把用户指定的命令启动到这个伪控制台里。它还要替 Windows 准备一些很挑剔的东西,比如宽字符命令行、环境变量块、当前目录、PATH 搜索和参数引号转义。最重要的一点是,输入输出管道必须一直活到伪控制台关闭,所以 PsuedoCon 会把这些句柄保存起来,避免 Windows 还在用时被提前释放。

函数细节14
load_conpty87–97 ↗
fn load_conpty() -> ConPtyFuncs

作用:加载 Windows 伪控制台需要用到的系统函数。它优先尝试项目旁边的 conpty.dll,如果没有,就用系统自带的 kernel32.dll。

数据流:进去的是固定的 DLL 文件名 → 它打开 kernel32.dll,并尝试额外打开 conpty.dll → 出来的是一组可以调用的 ConPTY 函数;如果系统完全不支持,会直接报错停止。

调用关系:它在 CONPTY 这个全局对象第一次被用到时运行。后面的创建、调整大小、关闭伪控制台都会通过它加载出来的函数去找 Windows 办事。

调用图:外部调用 2 个(open, new)。

conpty_supported103–105 ↗
fn conpty_supported() -> bool

作用:判断当前 Windows 是否支持 ConPTY。外部代码可以先问它,避免在太旧的系统上硬创建终端导致失败。

数据流:进去没有参数 → 它读取 Windows 构建号 → 如果构建号不低于 17763,就返回 true,否则返回 false。

调用关系:它把具体的系统版本读取工作交给 windows_build_number。通常在创建 Windows 伪终端前被用来做能力检查。

调用图:调用 1 个内部函数(windows_build_number)。

windows_build_number107–117 ↗
fn windows_build_number() -> Option<u32>

作用:读取 Windows 的内部构建号,也就是判断系统新旧的数字。ConPTY 只有 Windows 10 2018 年 10 月更新之后才可靠可用。

数据流:进去没有参数 → 它打开 ntdll.dll,调用 RtlGetVersion,把系统版本信息填进结构体 → 成功就返回构建号,失败就返回空。

调用关系:conpty_supported 用它来判断能不能启用伪控制台;测试函数 tests::windows_build_number_returns_value 也会调用它,确认在测试环境里能读到版本。

调用图:被 2 处调用(conpty_supported, windows_build_number_returns_value);外部调用 3 个(open, new, zeroed)。

PsuedoCon::drop131–133 ↗
fn drop(&mut self)

作用:在 PsuedoCon 对象不用时关闭 Windows 伪控制台。它像把临时开的终端房间退掉,防止系统资源泄漏。

数据流:进去的是即将被销毁的 PsuedoCon,里面带着 Windows 控制台句柄 → 它调用 ClosePseudoConsole → 结果是系统里的伪控制台被关闭,相关底层资源开始释放。

调用关系:这是 Rust 自动调用的清理函数,不需要别人手动喊它。只要 PsuedoCon 生命周期结束,它就通过全局 CONPTY 函数表把关闭动作交给 Windows。

PsuedoCon::raw_handle137–139 ↗
fn raw_handle(&self) -> HPCON

作用:取出 PsuedoCon 里面保存的 Windows 原始句柄。句柄可以理解成系统发的一张“号码牌”,凭它能找到那间伪控制台。

数据流:进去的是一个 PsuedoCon 引用 → 它不修改任何东西,只把内部的 con 字段拿出来 → 出来的是 HPCON 原始句柄。

调用关系:pseudoconsole_handle 会调用它,把底层句柄交给需要直接和 Windows API 打交道的地方。

调用图:被 1 处调用(pseudoconsole_handle)。

PsuedoCon::new141–161 ↗
fn new(size: COORD, input: FileDescriptor, output: FileDescriptor) -> Result<Self, Error>

作用:创建一间新的 Windows 伪控制台,并把输入、输出管道接进去。调用者用它来准备一个可以承载 shell 或命令的终端环境。

数据流:进去的是终端大小,以及两根管道句柄:一根给输入,一根给输出 → 它把这些交给 CreatePseudoConsole,并带上一个调整大小相关的兼容标志 → 成功就返回 PsuedoCon,失败就返回错误;输入输出管道会被 PsuedoCon 保存,避免提前关闭。

调用关系:create_conpty_handles 会调用它来真正建立 ConPTY。它依赖全局 CONPTY 里加载好的 Windows 函数,并用 ensure! 把失败变成清楚的错误信息。

调用图:被 1 处调用(create_conpty_handles);外部调用 2 个(ensure!, as_raw_handle)。

PsuedoCon::resize163–173 ↗
fn resize(&self, size: COORD) -> Result<(), Error>

作用:改变伪控制台的宽高,比如用户拖大或缩小终端窗口时就需要它。这样里面运行的程序才能知道新的行列数。

数据流:进去的是目标大小 COORD,里面有列数和行数 → 它调用 ResizePseudoConsole 通知 Windows → 成功返回空结果,失败返回带有目标尺寸和错误码的信息。

调用关系:外层的 resize 流程会调用它。它只负责把“改成多大”这件事传给 Windows,不处理界面本身。

调用图:被 1 处调用(resize);外部调用 1 个(ensure!)。

PsuedoCon::spawn_command175–227 ↗
fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result<WinChild>

作用:把一个命令启动到这间伪控制台里运行。比如打开 cmd、powershell,或运行用户指定的交互式程序,都要经过这里。

数据流:进去的是 CommandBuilder,里面有程序名、参数、环境变量和当前目录等设置 → 它准备 Windows 启动信息,把伪控制台句柄塞进进程属性,生成命令行、环境变量块和工作目录,然后调用 CreateProcessW → 成功返回 WinChild,里面握着子进程句柄;失败会记录并返回清楚的错误。

调用关系:这是创建好 PsuedoCon 后启动实际命令的核心步骤。它把命令行拼接交给 build_cmdline,把环境变量交给 build_environment_block,把当前目录交给 resolve_current_directory,并用 ProcThreadAttributeList 把子进程和伪控制台连起来。

调用图:调用 4 个内部函数(with_capacity, build_cmdline, build_environment_block, resolve_current_directory);外部调用 10 个(new, from_wide, last_os_error, bail!, format!, error!, zeroed, null, null_mut, from_raw_handle)。

resolve_current_directory230–251 ↗
fn resolve_current_directory(cmd: &CommandBuilder) -> Option<Vec<u16>>

作用:决定新启动的命令应该在哪个目录里运行。它会优先用命令指定的目录,不行再考虑用户主目录。

数据流:进去的是 CommandBuilder → 它读取 cwd 和 USERPROFILE,并检查这些路径是否真的是目录;如果是相对路径,还会尽量拼到当前进程目录下面 → 出来的是 Windows 需要的宽字符、以 0 结尾的目录字符串;找不到合适目录就返回空。

调用关系:spawn_command 在调用 CreateProcessW 前会用它准备工作目录。这样子进程一启动,就会站在正确的文件夹里。

调用图:被 1 处调用(spawn_command);外部调用 5 个(get_cwd, get_env, new, new, current_dir)。

build_environment_block253–263 ↗
fn build_environment_block(cmd: &CommandBuilder) -> Vec<u16>

作用:把环境变量整理成 Windows 创建进程时要求的格式。环境变量就是给子进程看的“随身说明书”,比如 PATH、USERPROFILE 等。

数据流:进去的是 CommandBuilder 中完整的环境变量列表 → 它把每个 key=value 转成 Windows 宽字符,中间用 0 分隔,最后再加一个 0 表示结束 → 出来的是一个 Vec<u16>,可以直接交给 CreateProcessW。

调用关系:spawn_command 用它准备 env_block。没有这个转换,Windows 无法按预期把环境变量传给新进程。

调用图:被 1 处调用(spawn_command);外部调用 3 个(iter_full_env_as_str, new, new)。

build_cmdline265–294 ↗
fn build_cmdline(cmd: &CommandBuilder) -> anyhow::Result<(Vec<u16>, Vec<u16>)>

作用:把程序名和参数拼成 Windows 能理解的命令行,同时也准备可执行文件路径。Windows 创建进程时对这一串文字很挑剔,所以这里专门处理。

数据流:进去的是 CommandBuilder → 如果是默认程序,就从 ComSpec 找 cmd.exe;否则取第一个参数当程序名,并可能通过 PATH 搜索真实位置;随后把程序名和每个参数安全加引号、转义,转成宽字符并以 0 结尾 → 出来的是可执行文件路径和完整命令行两份数据。

调用关系:spawn_command 在真正启动进程前调用它。它把查找程序交给 search_path,把参数安全拼接交给 append_quoted;如果没有程序名,会直接返回错误。

调用图:调用 2 个内部函数(append_quoted, search_path);被 1 处调用(spawn_command);外部调用 7 个(get_argv, get_env, is_default_prog, new, new, bail!, ensure!)。

search_path296–318 ↗
fn search_path(cmd: &CommandBuilder, exe: &OsStr) -> OsString

作用:按 Windows 的 PATH 和 PATHEXT 规则寻找要运行的程序文件。比如用户写 python,它可能实际找到 python.exe。

数据流:进去的是 CommandBuilder 和一个程序名 → 它读取 PATH 里的目录,再读取 PATHEXT 里的可执行扩展名,逐个拼出候选路径并检查是否存在 → 找到就返回完整路径,找不到就原样返回程序名。

调用关系:build_cmdline 在处理非默认程序时会调用它。它让命令启动更像普通 Windows 命令行的行为,而不是只认完整路径。

调用图:被 1 处调用(build_cmdline);外部调用 4 个(get_env, new, to_os_string, split_paths)。

append_quoted320–363 ↗
fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>)

作用:把一个命令行参数安全地追加到命令行里,必要时加双引号并处理反斜杠和引号。它的作用是避免带空格的路径或特殊字符被 Windows 误拆。

数据流:进去的是一个参数和正在构造的命令行数组 → 如果参数没有空白或引号,就直接追加;否则加上外层双引号,并按 Windows 规则转义内部的反斜杠和双引号 → 结果是命令行数组多了一段不会被错误解析的参数文本。

调用关系:build_cmdline 会对程序名和每个参数调用它。它是命令行拼接里最细但很关键的保险丝。

调用图:被 1 处调用(build_cmdline);外部调用 3 个(encode_wide, is_empty, len)。

tests::windows_build_number_returns_value371–376 ↗
fn windows_build_number_returns_value()

作用:这是一个测试,确认在 Windows 测试环境里能读到系统构建号,并且构建号高于 ConPTY 的最低要求。

数据流:进去没有参数 → 它调用 windows_build_number 读取版本号,然后断言这个数字大于最低构建号 → 如果读不到或数字太低,测试失败。

调用关系:它只在测试时运行,不参与真实终端启动流程。它给 windows_build_number 提供一个基本安全网,防止版本读取逻辑悄悄坏掉。

调用图:调用 1 个内部函数(windows_build_number);外部调用 1 个(assert!)。

utils/pty/src/win/conpty.rs源码 ↗
io_transport创建终端会话、启动子进程、读写终端数据、调整窗口大小期间活跃

Windows 里的命令行程序通常需要一个控制台环境,不能只靠普通输入输出就完全模拟。这个文件用 Windows 的 ConPTY(Console Pseudo Terminal,控制台伪终端)来搭一个“假终端房间”:子进程以为自己在真实终端里运行,而外面的程序可以通过管道给它喂输入、拿到输出。它先创建两根管道,一根把用户输入送进伪终端,一根把程序输出读出来;再创建 PsuedoCon 这个 Windows 伪控制台对象。ConPtySystem 对外提供标准的 openpty 接口,返回一对 master/slave:master 像终端这一侧,负责读写和改大小;slave 像程序这一侧,负责把命令放进伪终端里运行。内部状态用 Arc 和 Mutex 包起来,意思是多处可以共享同一份状态,但每次只能有一个地方修改,避免抢着改同一份数据。

函数细节11
create_conpty_handles42–58 ↗
fn create_conpty_handles(
    size: PtySize,
) -> anyhow::Result<(PsuedoCon, FileDescriptor, FileDescriptor)>

作用:创建一个 Windows 伪终端,并准备好外部程序和它通信要用的两端管道。简单说,就是搭好“输入通道、输出通道、伪终端房间”这一整套基础设施。

数据流:进去的是终端大小,比如多少行多少列;它创建两组管道,把其中一端交给 Windows 伪终端使用,另一端留给调用者读写;出来的是伪终端对象、可写入输入的句柄、可读取输出的句柄。

调用关系:这是底层搭建函数。ConPtySystem::openpty 用它来创建标准的 master/slave 终端对,RawConPty::new 也用它来创建更原始的伪终端对象。它内部调用 Pipe::new 这类外部构造函数来建管道,也调用 PsuedoCon::new 来真正向 Windows 要一个伪控制台。

调用图:调用 1 个内部函数(new);被 2 处调用(openpty, new);外部调用 1 个(new)。

RawConPty::new67–79 ↗
fn new(cols: i16, rows: i16) -> anyhow::Result<Self>

作用:创建一个比较“裸”的 ConPTY 对象,直接拿到伪终端和读写句柄。它适合那些不想走 portable_pty 标准接口、而是需要更底层控制的地方。

数据流:进去的是列数和行数;它把这些数字包装成 PtySize,然后交给 create_conpty_handles 创建伪终端和管道;出来的是 RawConPty,里面保存了伪终端、输入写端和输出读端。

调用关系:它是 RawConPty 的入口构造函数,被 create_conpty、spawn_conpty_process_as_user 这类更上层流程使用。真正创建管道和伪终端的工作没有放在这里,而是交给 create_conpty_handles。

调用图:调用 1 个内部函数(create_conpty_handles);被 2 处调用(create_conpty, spawn_conpty_process_as_user)。

RawConPty::pseudoconsole_handle81–83 ↗
fn pseudoconsole_handle(&self) -> RawHandle

作用:取出 Windows 伪终端的原始系统句柄。句柄可以理解成 Windows 给某个系统资源发的“号码牌”。

数据流:进去的是一个已经创建好的 RawConPty;它从内部的 PsuedoCon 对象里读取原始句柄;出来的是 RawHandle,供需要直接和 Windows API 打交道的代码使用,不改变对象本身。

调用关系:这个函数处在比较底层的位置。它把 RawConPty 里封装好的 PsuedoCon 句柄暴露出去,内部调用 PsuedoCon 的 raw_handle。上层如果要把伪终端句柄传给 Windows 进程创建相关代码,就会用到它。

调用图:调用 1 个内部函数(raw_handle)。

RawConPty::into_handles85–94 ↗
fn into_handles(self) -> (PsuedoCon, FileDescriptor, FileDescriptor)

作用:把 RawConPty 拆开,交出里面的三个核心零件:伪终端对象、输入写端、输出读端。它用于调用者想接管这些资源的所有权时。

数据流:进去的是一个 RawConPty 本身;它用特殊方式阻止 Rust 自动清理这个外壳,然后把里面三个字段逐个搬出来;出来的是三件资源的元组,原来的 RawConPty 不再按普通方式继续使用。

调用关系:这是一个所有权转移函数,常用于更底层的进程启动流程需要自己保存这些句柄时。它不再调用 create_conpty_handles,而是把已经创建好的资源原样交出去;内部用到 ManuallyDrop 和 ptr::read 这类底层工具,目的就是避免同一份系统句柄被重复释放。

调用图:外部调用 2 个(new, read)。

ConPtySystem::openpty98–118 ↗
fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair>

作用:按 portable_pty 这个通用伪终端接口的要求,打开一个新的 Windows 伪终端。调用者拿到的是一对对象:一个代表终端控制端,一个代表子进程端。

数据流:进去的是想要的终端大小;它先调用 create_conpty_handles 创建伪终端和管道,再把这些东西放进共享的 Inner 状态里;出来的是 PtyPair,里面的 master 能读写和改大小,slave 能启动命令。

调用关系:这是这个文件面向通用终端框架的主要入口。它把 Windows 专用的 ConPTY 包装成 portable_pty 能理解的形状。后续 ConPtyMasterPty::resize、try_clone_reader、take_writer 和 ConPtySlavePty::spawn_command 都会围绕这里创建的共享 Inner 工作。

调用图:调用 1 个内部函数(create_conpty_handles);外部调用 3 个(new, new, new)。

Inner::resize129–147 ↗
fn resize(
        &mut self,
        num_rows: u16,
        num_cols: u16,
        pixel_width: u16,
        pixel_height: u16,
    ) -> Result<(), Error>

作用:真正执行伪终端改大小的动作,并把内部记录的大小同步更新。没有它,外部窗口变大变小时,里面运行的程序可能还以为窗口尺寸没变。

数据流:进去的是新的行数、列数,以及像素宽高;它把行列数转成 Windows 需要的 COORD 格式,调用伪终端对象的 resize;成功后更新 Inner 里保存的 PtySize;出来是成功或错误。

调用关系:这是内部状态的实际修改点。ConPtyMasterPty::resize 会先拿到锁,确保没人同时改同一份 Inner,然后把具体改大小的工作交给它。它再调用 PsuedoCon 的 resize 去通知 Windows。

调用图:调用 1 个内部函数(resize)。

ConPtyMasterPty::resize160–163 ↗
fn resize(&self, size: PtySize) -> anyhow::Result<()>

作用:对外提供“调整终端窗口大小”的接口。调用者只要传入新大小,不需要知道 Windows ConPTY 具体怎么改。

数据流:进去的是 PtySize,也就是新的行列数和像素尺寸;它先锁住共享的 Inner,防止并发修改;然后调用 Inner::resize;出来是成功或错误,内部大小记录和 Windows 伪终端大小都会被更新。

调用关系:这是 master 端的公开操作。终端窗口大小变化时,上层会调用它;它自己不直接碰 Windows API,而是把实际工作交给 Inner::resize。

ConPtyMasterPty::get_size165–168 ↗
fn get_size(&self) -> Result<PtySize, Error>

作用:读取当前记录的终端大小。调用者可以用它知道这个伪终端现在是多少行、多少列。

数据流:进去的是 ConPtyMasterPty;它锁住共享的 Inner,读取里面保存的 size;出来的是 PtySize,不改变任何状态。

调用关系:这是 master 端的查询接口。它和 ConPtyMasterPty::resize 配套:resize 更新大小,get_size 读回当前大小。它只读共享状态,不把工作交给其他函数。

ConPtyMasterPty::try_clone_reader170–172 ↗
fn try_clone_reader(&self) -> anyhow::Result<Box<dyn std::io::Read + Send>>

作用:给调用者一份可读取终端输出的读取器。也就是说,外部可以用它拿到子进程在伪终端里打印出来的内容。

数据流:进去的是 ConPtyMasterPty;它锁住 Inner,克隆 readable 这个输出读取句柄;出来的是一个实现 Read 的对象,可以被其他代码持续读取,不会拿走原始 readable。

调用关系:这是 master 端读取输出的入口。上层终端显示逻辑通常会调用它来拿到输出流。它内部依赖 FileDescriptor 的 try_clone 来复制读端,并把复制品装进 Box 里返回。

调用图:外部调用 1 个(new)。

ConPtyMasterPty::take_writer174–183 ↗
fn take_writer(&self) -> anyhow::Result<Box<dyn std::io::Write + Send>>

作用:取走写入终端输入的写入器。调用者拿到它后,就可以把用户键盘输入写进伪终端。

数据流:进去的是 ConPtyMasterPty;它锁住 Inner,从 writable 里把写端取出来;出来的是一个实现 Write 的对象。这个写端只能取一次,第二次调用会得到“writer already taken”的错误。

调用关系:这是 master 端发送输入的入口。上层输入处理逻辑会调用它,把键盘数据写给伪终端。它和 try_clone_reader 不同:reader 可以克隆,writer 被设计成只交出去一次,避免多个写入者混在一起乱写。

调用图:外部调用 1 个(new)。

ConPtySlavePty::spawn_command187–191 ↗
fn spawn_command(&self, cmd: CommandBuilder) -> anyhow::Result<Box<dyn Child + Send + Sync>>

作用:在这个伪终端里启动一个命令。这样新进程会以为自己连接在真实终端上,而不是普通管道后面。

数据流:进去的是 CommandBuilder,也就是要启动的程序、参数、环境等启动信息;它锁住共享 Inner,调用伪终端对象的 spawn_command;出来的是 Child 对象,用来代表已经启动的子进程。

调用关系:这是 slave 端最重要的动作。ConPtySystem::openpty 创建出 slave 后,上层用它来启动 shell 或其他命令。它把真正的进程创建工作交给 Inner 里的 PsuedoCon,自己负责把结果包装成 portable_pty 需要的 Child 接口。

调用图:外部调用 1 个(new)。