关于rust:通过将ctrl-c发送到stdin将SIGINT发送到进程

Send SIGINT to a process by sending ctrl-c to stdin

我正在寻找一种模仿终端以进行一些自动化测试的方法:即启动一个过程,然后通过将数据发送到stdin并从stdout读取与之交互。 例如。 向stdin发送一些输入行,包括ctrl-cctrl-\\,这将导致向该进程发送信号。

使用std::process::Commannd我可以将输入发送到 cat,我也在stdout上看到了它的输出,但是发送ctrl-c(据我了解是3)不会导致SIGINT发送到外壳。 例如。 该程序应终止:

1
2
3
4
5
6
7
8
9
10
11
12
use std::process::{Command, Stdio};
use std::io::Write;

fn main() {
    let mut child = Command::new("sh")
        .arg("-c").arg("-i").arg("cat")
        .stdin(Stdio::piped())
        .spawn().unwrap();
    let mut stdin = child.stdin.take().unwrap();
    stdin.write(&[3]).expect("cannot send ctrl-c");
    child.wait();
}

我怀疑问题在于发送ctrl-c需要一些tty,并且通过sh -i仅在"交互模式"下。

我需要完全成熟并使用例如 termionncurses

更新:我在原始问题中混淆了外壳和端子。 我现在清除了。 我也提到了ssh应该是sh


最简单的方法是直接将SIGINT信号发送到子进程。使用nixsignal::kill函数可以轻松完成此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// add `nix ="0.15.0"` to your Cargo.toml
use std::process::{Command, Stdio};
use std::io::Write;

fn main() {
    // spawn child process
    let mut child = Command::new("cat")
        .stdin(Stdio::piped())
        .spawn().unwrap();

    // send"echo\
" to child's stdin
    let mut stdin = child.stdin.take().unwrap();
    writeln!(stdin,"echo");

    // sleep a bit so that child can process the input
    std::thread::sleep(std::time::Duration::from_millis(500));

    // send SIGINT to the child
    nix::sys::signal::kill(
        nix::unistd::Pid::from_raw(child.id() as i32),
        nix::sys::signal::Signal::SIGINT
    ).expect("cannot send ctrl-c");

    // wait for child to terminate
    child.wait().unwrap();
}

您应该能够使用此方法发送各种信号。对于更高级的"交互性"(例如查询终端大小的vi之类的子程序),您需要创建伪终端,例如@hansaplast在其解决方案中所做的那样。


经过大量研究,我发现自己完成pty fork并不需要太多工作。有pty-rs,但是它有错误,而且似乎无法维护。

以下代码需要crates.io上尚未安装的nix模块,因此Cargo.toml现在需要它:

1
2
[dependencies]
nix = {git ="https://github.com/nix-rust/nix.git"}

以下代码在tty中运行cat,然后从中写入/读取并发送Ctrl-C(3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
extern crate nix;

use std::path::Path;
use nix::pty::{posix_openpt, grantpt, unlockpt, ptsname};
use nix::fcntl::{O_RDWR, open};
use nix::sys::stat;
use nix::unistd::{fork, ForkResult, setsid, dup2};
use nix::libc::{STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO};
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::io::prelude::*;
use std::io::{BufReader, LineWriter};


fn run() -> std::io::Result<()> {
    // Open a new PTY master
    let master_fd = posix_openpt(O_RDWR)?;

    // Allow a slave to be generated for it
    grantpt(&master_fd)?;
    unlockpt(&master_fd)?;

    // Get the name of the slave
    let slave_name = ptsname(&master_fd)?;

    match fork() {
        Ok(ForkResult::Child) => {
            setsid()?; // create new session with child as session leader
            let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty())?;

            // assign stdin, stdout, stderr to the tty, just like a terminal does
            dup2(slave_fd, STDIN_FILENO)?;
            dup2(slave_fd, STDOUT_FILENO)?;
            dup2(slave_fd, STDERR_FILENO)?;
            std::process::Command::new("cat").status()?;
        }
        Ok(ForkResult::Parent { child: _ }) => {
            let f = unsafe { std::fs::File::from_raw_fd(master_fd.as_raw_fd()) };
            let mut reader = BufReader::new(&f);
            let mut writer = LineWriter::new(&f);

            writer.write_all(b"hello world\
")?;
            let mut s = String::new();
            reader.read_line(&mut s)?; // what we just wrote in
            reader.read_line(&mut s)?; // what cat wrote out
            writer.write(&[3])?; // send ^C
            writer.flush()?;
            let mut buf = [0; 2]; // needs bytewise read as ^C has no newline
            reader.read(&mut buf)?;
            s += &String::from_utf8_lossy(&buf).to_string();
            println!("{}", s);
            println!("cat exit code: {:?}", wait::wait()?); // make sure cat really exited
        }
        Err(_) => println!("error"),
    }
    Ok(())
}

fn main() {
    run().expect("could not execute command");
}

输出:

1
2
3
4
hello world
hello world
^C
cat exit code: Signaled(2906, SIGINT, false)


尝试添加-t选项TWICE强制伪伪分配。即

1
2
3
4
klar (16:14) ~>echo foo | ssh [email protected] tty
not a tty
klar (16:14) ~>echo foo | ssh -t -t [email protected] tty
/dev/pts/0

当您拥有伪tty时,我认为它应该按照您的意愿将其转换为SIGINT。

在您的简单示例中,您还可以在写入后关闭stdin,在这种情况下,服务器应退出。对于这种特殊情况,它会更优雅并且可能更可靠。