通过将 ctrl-c 发送到标准输入来将 SIGINT 发送到进程
Send SIGINT to a process by sending ctrl-c to stdin
我正在寻找一种模仿终端进行某些自动化测试的方法:即启动一个进程,然后通过将数据发送到标准输入并从标准输出读取数据来与其交互。例如。向 stdin 发送一些输入行,包括 ctrl-c
和 ctrl-\
,这将导致向进程发送信号。
使用 std::process::Commannd
我可以将输入发送到例如cat
并且我也在 stdout 上看到它的输出,但是发送 ctrl-c
(作为 I understand that is 3
)不会导致 SIGINT
发送到 shell。例如。该程序应该终止:
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
它只在 "interactive mode".
我需要完全成熟并使用例如termion
or ncurses
?
更新:我在原问题中混淆了shell和终端。我现在把它弄清楚了。我还提到 ssh
应该是 sh
.
尝试添加两次 -t 选项以强制分配伪 tty。即
klar (16:14) ~>echo foo | ssh user@host.ssh.com tty
not a tty
klar (16:14) ~>echo foo | ssh -t -t user@host.ssh.com tty
/dev/pts/0
当你有一个伪 tty 时,我认为它应该按照你的意愿将其转换为 SIGINT。
在您的简单示例中,您也可以在写入后关闭标准输入,在这种情况下服务器应该退出。对于这种特殊情况,它会更优雅并且可能更可靠。
经过大量研究后,我发现自己做 pty fork 并没有太多工作。有 pty-rs,但它有错误并且似乎没有维护。
以下代码需要 pty
module of nix
crates.io,因此 Cargo.toml
现在需要它:
[dependencies]
nix = {git = "https://github.com/nix-rust/nix.git"}
以下代码在 tty 中运行 cat,然后从中运行 writes/reads 并发送 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");
}
输出:
hello world
hello world
^C
cat exit code: Signaled(2906, SIGINT, false)
最简单的方法就是直接给子进程发送SIGINT信号。这可以使用 nix
的 signal::kill
函数轻松完成:
// 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();
}
您应该能够使用此方法发送各种信号。对于更高级的 "interactivity"(例如,查询终端大小的 vi
之类的子程序),您需要创建一个伪终端,就像@hansaplast 在他的解决方案中所做的那样。
我正在寻找一种模仿终端进行某些自动化测试的方法:即启动一个进程,然后通过将数据发送到标准输入并从标准输出读取数据来与其交互。例如。向 stdin 发送一些输入行,包括 ctrl-c
和 ctrl-\
,这将导致向进程发送信号。
使用 std::process::Commannd
我可以将输入发送到例如cat
并且我也在 stdout 上看到它的输出,但是发送 ctrl-c
(作为 I understand that is 3
)不会导致 SIGINT
发送到 shell。例如。该程序应该终止:
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
它只在 "interactive mode".
我需要完全成熟并使用例如termion
or ncurses
?
更新:我在原问题中混淆了shell和终端。我现在把它弄清楚了。我还提到 ssh
应该是 sh
.
尝试添加两次 -t 选项以强制分配伪 tty。即
klar (16:14) ~>echo foo | ssh user@host.ssh.com tty
not a tty
klar (16:14) ~>echo foo | ssh -t -t user@host.ssh.com tty
/dev/pts/0
当你有一个伪 tty 时,我认为它应该按照你的意愿将其转换为 SIGINT。
在您的简单示例中,您也可以在写入后关闭标准输入,在这种情况下服务器应该退出。对于这种特殊情况,它会更优雅并且可能更可靠。
经过大量研究后,我发现自己做 pty fork 并没有太多工作。有 pty-rs,但它有错误并且似乎没有维护。
以下代码需要 pty
module of nix
crates.io,因此 Cargo.toml
现在需要它:
[dependencies]
nix = {git = "https://github.com/nix-rust/nix.git"}
以下代码在 tty 中运行 cat,然后从中运行 writes/reads 并发送 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");
}
输出:
hello world
hello world
^C
cat exit code: Signaled(2906, SIGINT, false)
最简单的方法就是直接给子进程发送SIGINT信号。这可以使用 nix
的 signal::kill
函数轻松完成:
// 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();
}
您应该能够使用此方法发送各种信号。对于更高级的 "interactivity"(例如,查询终端大小的 vi
之类的子程序),您需要创建一个伪终端,就像@hansaplast 在他的解决方案中所做的那样。