Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 741
hansaplast Avatar asked Mar 10 '23 04:03

hansaplast


2 Answers

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.

like image 164
Felix Avatar answered Mar 16 '23 01:03

Felix


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)
like image 30
hansaplast Avatar answered Mar 16 '23 02:03

hansaplast