You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Raw模式下实现后台监听Ctrl+C中断命令并退出进程的方法

解决方法

问题出在command.output()是阻塞式调用,会一直等待子进程执行完毕才进入后续按键监听逻辑,导致子进程运行期间无法响应Ctrl+C。同时终端处于raw模式时,系统默认的SIGINT信号处理被禁用,需要手动实现中断逻辑。

可以通过以下步骤修改代码:

  1. 使用Command::spawn()启动子进程(非阻塞),获得子进程句柄以便后续控制。
  2. 同时监听终端按键输入和子进程状态,避免阻塞。
  3. 捕获到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 02:32:41