diff --git a/AGENTS.md b/AGENTS.md index 96fd982..f7cdabc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2850,5 +2850,38 @@ chacha20-poly1305@openssh.com --- -**最后更新**:2026-06-20 13:45 -**版本**:1.30(Phase 8.3 Docker 测试完成) +--- + +## exit-status 修复完成(2026-06-20)⭐⭐⭐⭐⭐ + +**背景**:SSH `ssh -p 2024 demo@127.0.0.1 "echo hello"` 返回 `exit status -1` + +**根本原因**(RFC 4254 §6.10): +- OpenSSH client 要求 server 在通道关闭前发送 `SSH_MSG_CHANNEL_REQUEST "exit-status"` +- `handle_child_exited()` 之前只发送 EOF + CLOSE,从未发送 `exit-status` 请求 +- 因此 OpenSSH client 不知道子进程的退出码,报告 `exit status -1` + +**修复内容**(`channel.rs`): + +| 修改 | 位置 | 说明 | +|------|------|------| +| `exit_status: Option` | `Channel` struct | 存储子进程退出码 | +| `channel.exit_status = Some(exit_code)` | `poll_exec_stdout_and_client()` | 在 `child.try_wait()` 返回 status 时保存退出码 | +| `build_channel_exit_status()` | 新函数 | 构建 `SSH_MSG_CHANNEL_REQUEST "exit-status" FALSE ` | +| 发送 exit-status → EOF → CLOSE | `handle_child_exited()` | 按正确顺序发送三个消息 | + +**验证结果**: +```bash +$ ssh -p 2024 demo@127.0.0.1 "echo hello" +hello # ✅ exit status 0 (默认) + +$ ssh -p 2024 demo@127.0.0.1 "exit 42" + # 无输出(exit 42 不产生 stdout) +$ echo $? +42 # ✅ exit status 42 正确传递 +``` + +**累计代码**:5061 行(新增 31 行) + +**最后更新**:2026-06-20 14:15 +**版本**:1.31(exit-status 修复完成) diff --git a/markbase-core/src/ssh_server/channel.rs b/markbase-core/src/ssh_server/channel.rs index 3bdfac7..7513258 100644 --- a/markbase-core/src/ssh_server/channel.rs +++ b/markbase-core/src/ssh_server/channel.rs @@ -176,6 +176,7 @@ impl ChannelManager { scp_handler: None, rsync_handler: None, exec_process: None, // Phase 14: 交互式exec + exit_status: None, // ⭐⭐⭐⭐⭐ exit status from child process sftp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.2修复:SFTP packet累积 scp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.4修复:SCP packet累积 scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine @@ -254,6 +255,7 @@ impl ChannelManager { scp_handler: None, rsync_handler: None, exec_process: None, + exit_status: None, // ⭐⭐⭐⭐⭐ exit status from child process sftp_input_buffer: Vec::new(), scp_input_buffer: Vec::new(), scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine @@ -329,6 +331,7 @@ impl ChannelManager { scp_handler: None, rsync_handler: None, exec_process: None, // Phase 14: 交互式exec + exit_status: None, // ⭐⭐⭐⭐⭐ exit status from child process sftp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.2修复 scp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.4修复 scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine @@ -1303,6 +1306,19 @@ impl ChannelManager { /// ⭐⭐⭐⭐⭐ 关键:非阻塞读取数据,不等待子进程完成 /// ⭐⭐⭐⭐⭐ Phase 14.2: 处理child exited(发送EOF + CLOSE) /// 参考:OpenSSH session.c: do_exec_no_pty() + /// Build SSH_MSG_CHANNEL_REQUEST "exit-status" (RFC 4254 §6.10) + pub fn build_channel_exit_status(&self, channel: u32, exit_code: u32) -> Result { + let mut payload = Vec::new(); + payload.write_u8(PacketType::SSH_MSG_CHANNEL_REQUEST as u8)?; + payload.write_u32::(channel)?; + let name = "exit-status"; + payload.write_u32::(name.len() as u32)?; + payload.write_all(name.as_bytes())?; + payload.write_u8(0)?; // FALSE (want_reply) + payload.write_u32::(exit_code)?; + Ok(SshPacket::new(payload)) + } + pub fn handle_child_exited(&mut self) -> Result> { // 1. 收集需要处理的channel IDs (exec_process OR rsync_handler) let channel_ids: Vec = self @@ -1320,6 +1336,14 @@ impl ChannelManager { // 2. 构建packets(避免borrow冲突) let mut packets = Vec::new(); for channel_id in &channel_ids { + // Send exit-status first (RFC 4254 §6.10) + let exit_code = self.channels.get(channel_id) + .and_then(|c| c.exit_status) + .unwrap_or(255); + let exit_packet = self.build_channel_exit_status(*channel_id, exit_code)?; + packets.push(exit_packet); + + // Then EOF + CLOSE let eof_packet = self.build_channel_eof(*channel_id)?; packets.push(eof_packet); @@ -1332,12 +1356,13 @@ impl ChannelManager { if let Some(channel) = self.channels.get_mut(channel_id) { channel.exec_process = None; channel.rsync_handler = None; + channel.exit_status = None; } } if !channel_ids.is_empty() { info!( - "Child/rsync exited, sent EOF + CLOSE for {} channels", + "Child/rsync exited, sent exit-status + EOF + CLOSE for {} channels", channel_ids.len() ); } @@ -1498,11 +1523,13 @@ impl ChannelManager { if let Some(exec_process) = &mut channel.exec_process { match exec_process.child.try_wait() { Ok(Some(status)) => { + let exit_code = status.code().unwrap_or(-1) as u32; info!( - "Child process exited (channel {}, status: {:?})", - channel_id, status + "Child process exited (channel {}, exit_status: {}, status: {:?})", + channel_id, exit_code, status ); child_exited = true; + channel.exit_status = Some(exit_code); let command_str = exec_process.command.clone(); let should_trigger_hook = status.success() @@ -2005,6 +2032,7 @@ struct Channel { scp_handler: Option, // Phase 8: SCP处理器 rsync_handler: Option, // Phase 8: rsync处理器 exec_process: Option, // Phase 14: 交互式exec进程 + exit_status: Option, // ⭐⭐⭐⭐⭐ exit status from child process // ⭐⭐⭐⭐⭐ Critical修复:SFTP packet累积buffer sftp_input_buffer: Vec, // Phase 14.2修复:累积不完整的SFTP packets // ⭐⭐⭐⭐⭐ Phase 14.4:SCP packet累积buffer