如何在Rust中交互式运行可执行文件实现多轮输入输出?
问题描述
我需要搭建一个通过WebSocket让用户与可执行文件进行输入输出交互的网站(比如命令行界面的象棋程序)。核心需求是在Web服务器代码中调用二进制程序,实现两次输入,且第二次输入前必须获取第一次的输出。目前遇到的问题是:无法在单个子进程中多次向stdin写入数据,尝试操作时会触发Broken pipe错误。
被调用的二进制程序代码(reversed)
use std::io::{self, Write}; fn main() -> io::Result<()> { let mut buffer = String::new(); for i in 0..2 { print!("Output: "); let _ = io::stdout().flush(); io::stdin().read_line(&mut buffer)?; println!("{}", buffer.trim().chars().rev().collect::<String>()); buffer.clear(); } Ok(()) }
当前调用方代码(出现问题的版本)
use std::io::{self, Write, Read}; use std::process::{Command, Stdio}; fn main() { execute("reversed/target/debug/reversed"); } fn read_line(buffer: &mut String) { io::stdin().read_line(buffer).expect("Faild to read stdin"); } fn execute(program: &str) { let mut child = Command::new(program) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Failed to spawn child process"); _ = std::thread::spawn(move || { let mut stdin = child.stdin.take().expect("Failed to open stdin"); stdin.write_all("Hello, ".as_bytes()).expect("Failed to write to stdin"); drop(stdin); // trying to write to stdin more than once panics let mut buffer = String::new(); let mut stdout = child.stdout.take().expect("Failed to take stdout"); _ = stdout.read_to_string(&mut buffer); drop(stdout); println!("{}", buffer); // the program prints // Output: ,olleH // Output: let mut stdin = child.stdin.take().expect("Failed to open stdin"); // thread 'main' panicked at 'failed printing to stdout: Broken pipe (os error 32)' stdin.write_all(" world!".as_bytes()).expect("Failed to write to stdin"); drop(stdin); }).join(); }
问题原因
- 提前关闭stdin管道:第一次写入后调用
drop(stdin)关闭了子进程的stdin,子进程会认为输入已经结束,后续再尝试获取stdin并写入时,管道已经断开,触发Broken pipe错误。 - 错误的输出读取方式:
read_to_string会一直阻塞直到读取到EOF,但子进程在第一次输入后仍在等待第二次输入,不会发送EOF,导致这个调用永远阻塞,后续代码无法执行。
解决方案
修正后的调用方代码
use std::io::{self, Write, BufRead, BufReader}; use std::process::{Command, Stdio}; fn main() { execute("reversed/target/debug/reversed"); } fn execute(program: &str) { let mut child = Command::new(program) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Failed to spawn child process"); // 保留stdin和stdout句柄,避免中途关闭 let mut stdin = child.stdin.take().expect("Failed to open stdin"); let stdout = child.stdout.take().expect("Failed to take stdout"); let mut reader = BufReader::new(stdout); // 第一次输入:写入后必须flush确保数据发送到子进程 stdin.write_all("Hello, \n".as_bytes()).expect("Failed to write to stdin"); stdin.flush().expect("Failed to flush stdin"); // 逐行读取输出,直到拿到第二次输入的提示"Output: " let mut buffer = String::new(); loop { let bytes_read = reader.read_line(&mut buffer).expect("Failed to read stdout"); if bytes_read == 0 { panic!("Child process exited unexpectedly"); } print!("{}", buffer); if buffer.ends_with("Output: ") { buffer.clear(); break; } buffer.clear(); } // 第二次输入 stdin.write_all("world!\n".as_bytes()).expect("Failed to write to stdin"); stdin.flush().expect("Failed to flush stdin"); // 读取剩余输出 while reader.read_line(&mut buffer).expect("Failed to read stdout") > 0 { print!("{}", buffer); buffer.clear(); } // 等待子进程正常结束 let status = child.wait().expect("Failed to wait for child process"); println!("\n子进程退出状态: {}", status); }
关键修复点
- 保持stdin管道打开:全程使用同一个
stdin句柄,不要中途调用drop(stdin),直到所有输入完成后再自然关闭。 - 逐行读取输出:使用
BufReader的read_line方法逐行读取子进程输出,这样可以在子进程输出"Output: "提示时立刻停止等待,避免阻塞。 - 强制刷新缓冲区:每次写入stdin后调用
flush(),确保数据立刻发送到子进程,避免因为缓冲区积压导致子进程无法及时收到输入。
内容的提问来源于stack exchange,提问作者user22680114
相关产品推荐
相关产品推荐

