Raw模式下实现后台监听Ctrl+C中断命令并退出进程的方法
解决方法
问题出在command.output()是阻塞式调用,会一直等待子进程执行完毕才进入后续按键监听逻辑,导致子进程运行期间无法响应Ctrl+C。同时终端处于raw模式时,系统默认的SIGINT信号处理被禁用,需要手动实现中断逻辑。
可以通过以下步骤修改代码:
- 使用
Command::spawn()启动子进程(非阻塞),获得子进程句柄以便后续控制。 - 同时监听终端按键输入和子进程状态,避免阻塞。
- 捕获到Ctrl+C时,立即杀死子进程并恢复终端正常模式后退出。
修改后的代码(线程监听按键版)
use std::process::{Command, Child}; use termion::event::Key; use termion::input::TermRead; use termion::raw::{IntoRawMode, RawTerminal}; use std::io::{stdin, stdout, Write}; use std::thread; fn main() { let stdin = stdin(); let mut stdout = stdout().into_raw_mode().unwrap(); let mut command = Command::new("script"); command .arg("-qec") .arg("sleep 5 && echo test") .arg("/dev/null"); // 启动子进程,非阻塞 let mut child = command.spawn().expect("Failed to start command"); // 启动线程监听按键输入 let child_id = child.id(); thread::spawn(move || { for c in stdin.keys() { match c.unwrap() { Key::Ctrl('c') => { // 发送SIGINT信号终止子进程 Command::new("kill") .arg("-INT") .arg(child_id.to_string()) .output() .expect("Failed to kill child process"); println!("Ctrl + C pressed, terminating...\r\n"); std::process::exit(0); } _ => {} } } }); // 等待子进程结束 let status = child.wait().expect("Failed to wait for child"); println!("Child process exited with status: {:?}\r\n", status); // 恢复终端正常显示 write!(stdout, "{}", termion::cursor::Show).unwrap(); stdout.flush().unwrap(); }
关键改动说明
spawn()替代output():spawn()启动子进程后立即返回Child实例,不会阻塞主线程,让我们能同时处理输入监听和子进程状态。- 线程独立监听按键:单独开线程处理终端输入,避免主线程在等待子进程时无法响应按键。
- 捕获Ctrl+C后终止子进程:通过子进程ID发送SIGINT信号,模拟非raw模式下的中断行为,确保子进程能被及时终止。
- 终端状态恢复:无论子进程正常结束还是被中断,都确保终端回到正常模式。
另一种方案(监听系统信号版)
如果更倾向于贴近系统默认的中断逻辑,可以使用signal-hook crate监听SIGINT信号(需在Cargo.toml中添加signal-hook = "0.3"):
use std::process::{Command, Child}; use termion::raw::{IntoRawMode, RawTerminal}; use std::io::{stdout, Write}; use signal_hook::{consts::SIGINT, iterator::Signals}; use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { let mut stdout = stdout().into_raw_mode().unwrap(); let mut command = Command::new("script"); command .arg("-qec") .arg("sleep 5 && echo test") .arg("/dev/null"); let mut child = command.spawn().expect("Failed to start command"); // 设置信号监听通道 let (tx, rx) = mpsc::channel(); let mut signals = Signals::new(&[SIGINT]).expect("Failed to create signal handler"); thread::spawn(move || { for sig in signals.forever() { if sig == SIGINT { tx.send(()).expect("Failed to send interrupt signal"); break; } } }); // 等待子进程结束或中断信号触发 let status = loop { match child.try_wait() { Ok(Some(status)) => break status, Ok(None) => { // 检查是否收到中断信号 if rx.try_recv().is_ok() { child.kill().expect("Failed to kill child process"); println!("Terminated by Ctrl+C\r\n"); break child.wait().expect("Failed to wait for child after kill"); } // 短暂休眠避免CPU占用过高 thread::sleep(Duration::from_millis(100)); } Err(e) => panic!("Failed to check child status: {}", e), } }; println!("Child process exited with status: {:?}\r\n", status); write!(stdout, "{}", termion::cursor::Show).unwrap(); stdout.flush().unwrap(); }
这种方式直接监听系统SIGINT信号,不需要手动处理终端按键,更符合系统原生的中断行为,同时适配raw模式环境。
内容的提问来源于stack exchange,提问作者wyc
相关产品推荐
相关产品推荐

