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

I'm looking for a way to mimick a terminal for some automated testing: i.e. start a process and then interact with it via sending data to stdin and reading from stdout. E.g. sending some lines of input to stdin including ctrl-c and ctrl-\ which should result in sending signals to the process.

Using std::process::Commannd I'm able to send input to e.g. cat and I'm also seeing its output on stdout, but sending ctrl-c (as I understand that is 3) does not cause SIGINT sent to the shell. E.g. this program should terminate:

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();
}

I suspect the issue is that sending ctrl-c needs the some tty and via sh -i it's only in "interactive mode".

Do I need to go full fledged and use e.g. termion or ncurses?

Update: I confused shell and terminal in the original question. I cleared this up now. Also I mentioned ssh which should have been sh.



Solution 1:[1]

The simplest way is to directly send the SIGINT signal to the child process. This can be done easily using nix's signal::kill function:

// 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\n" 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();
}

You should be able to send all kinds of signals using this method. For more advanced "interactivity" (e.g. child programs like vi that query terminal size) you'd need to create a pseudoterminal like @hansaplast did in his solution.

Solution 2:[2]

Try adding -t option TWICE to force pseudo-tty allocation. I.e.

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

When you have a pseudo-tty, I think it should convert that to SIGINT as you wanted to do.

In your simple example, you could also just close stdin after the write, in which case the server should exit. For this particular case it would be more elegant and probably more reliable.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Felix
Solution 2 Tatu Ylonen