Search code examples
unixrustprocesssignalspty

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

  • After a lot of research I figured out it's not too much work to do the pty fork myself. There's pty-rs, but it has bugs and seems unmaintained.

    The following code needs pty module of nix which is not yet on crates.io, so Cargo.toml needs this for now:

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

    The following code runs cat in a tty and then writes/reads from it and sends Ctrl-C (3):

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

    Output:

    hello world
    hello world
    ^C
    cat exit code: Signaled(2906, SIGINT, false)