mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 03:27:44 +00:00
simulate terminal utility (squash)
This commit is contained in:
parent
5a2e0c700e
commit
a4d5defeef
5 changed files with 348 additions and 29 deletions
|
@ -498,7 +498,7 @@ procfs = { version = "0.16", default-features = false }
|
||||||
rlimit = "0.10.1"
|
rlimit = "0.10.1"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dev-dependencies]
|
[target.'cfg(unix)'.dev-dependencies]
|
||||||
nix = { workspace = true, features = ["process", "signal", "user"] }
|
nix = { workspace = true, features = ["process", "signal", "user", "term"] }
|
||||||
rand_pcg = "0.3"
|
rand_pcg = "0.3"
|
||||||
xattr = { workspace = true }
|
xattr = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
//
|
//
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
// spell-checker:ignore winsize Openpty openpty xpixel ypixel ptyprocess
|
||||||
use crate::common::util::TestScenario;
|
use crate::common::util::TestScenario;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
|
|
||||||
|
@ -31,3 +32,32 @@ fn test_nohup_multiple_args_and_flags() {
|
||||||
assert!(at.file_exists("file1"));
|
assert!(at.file_exists("file1"));
|
||||||
assert!(at.file_exists("file2"));
|
assert!(at.file_exists("file2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(any(
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "android",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_vendor = "apple"
|
||||||
|
))]
|
||||||
|
fn test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced() {
|
||||||
|
let ts = TestScenario::new(util_name!());
|
||||||
|
let result = ts
|
||||||
|
.ucmd()
|
||||||
|
.terminal_simulation(true)
|
||||||
|
.args(&["sh", "is_atty.sh"])
|
||||||
|
.succeeds();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(result.stderr()).trim(),
|
||||||
|
"nohup: ignoring input and appending output to 'nohup.out'"
|
||||||
|
);
|
||||||
|
|
||||||
|
sleep(std::time::Duration::from_millis(10));
|
||||||
|
|
||||||
|
// this proves that nohup was exchanging the stdio file descriptors
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(ts.fixtures.plus_as_string("nohup.out")).unwrap(),
|
||||||
|
"stdin is not atty\nstdout is not atty\nstderr is not atty\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized
|
//spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty winsize xpixel ypixel
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use nix::pty::OpenptyResult;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
use rlimit::prlimit;
|
use rlimit::prlimit;
|
||||||
|
@ -21,6 +23,8 @@ use std::ffi::{OsStr, OsString};
|
||||||
use std::fs::{self, hard_link, remove_file, File, OpenOptions};
|
use std::fs::{self, hard_link, remove_file, File, OpenOptions};
|
||||||
use std::io::{self, BufWriter, Read, Result, Write};
|
use std::io::{self, BufWriter, Read, Result, Write};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
use std::os::fd::OwnedFd;
|
||||||
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt};
|
use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
@ -34,7 +38,7 @@ use std::rc::Rc;
|
||||||
use std::sync::mpsc::{self, RecvTimeoutError};
|
use std::sync::mpsc::{self, RecvTimeoutError};
|
||||||
use std::thread::{sleep, JoinHandle};
|
use std::thread::{sleep, JoinHandle};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{env, hint, thread};
|
use std::{env, hint, mem, thread};
|
||||||
use tempfile::{Builder, TempDir};
|
use tempfile::{Builder, TempDir};
|
||||||
|
|
||||||
static TESTS_DIR: &str = "tests";
|
static TESTS_DIR: &str = "tests";
|
||||||
|
@ -46,6 +50,7 @@ static ALREADY_RUN: &str = " you have already run this UCommand, if you want to
|
||||||
static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly.";
|
static MULTIPLE_STDIN_MEANINGLESS: &str = "Ucommand is designed around a typical use case of: provide args and input stream -> spawn process -> block until completion -> return output streams. For verifying that a particular section of the input stream is what causes a particular behavior, use the Command type directly.";
|
||||||
|
|
||||||
static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin";
|
static NO_STDIN_MEANINGLESS: &str = "Setting this flag has no effect if there is no stdin";
|
||||||
|
static END_OF_TRANSMISSION_SEQUENCE: &[u8] = &[b'\n', 0x04];
|
||||||
|
|
||||||
pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_coreutils");
|
pub const TESTS_BINARY: &str = env!("CARGO_BIN_EXE_coreutils");
|
||||||
pub const PATH: &str = env!("PATH");
|
pub const PATH: &str = env!("PATH");
|
||||||
|
@ -387,7 +392,10 @@ impl CmdResult {
|
||||||
pub fn success(&self) -> &Self {
|
pub fn success(&self) -> &Self {
|
||||||
assert!(
|
assert!(
|
||||||
self.succeeded(),
|
self.succeeded(),
|
||||||
"Command was expected to succeed.\nstdout = {}\n stderr = {}",
|
"Command was expected to succeed. Exit code: {}.\nstdout = {}\n stderr = {}",
|
||||||
|
self.exit_status()
|
||||||
|
.code()
|
||||||
|
.map_or("n/a".to_string(), |code| code.to_string()),
|
||||||
self.stdout_str(),
|
self.stdout_str(),
|
||||||
self.stderr_str()
|
self.stderr_str()
|
||||||
);
|
);
|
||||||
|
@ -1220,6 +1228,7 @@ pub struct UCommand {
|
||||||
limits: Vec<(rlimit::Resource, u64, u64)>,
|
limits: Vec<(rlimit::Resource, u64, u64)>,
|
||||||
stderr_to_stdout: bool,
|
stderr_to_stdout: bool,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
|
terminal_simulation: bool,
|
||||||
tmpd: Option<Rc<TempDir>>, // drop last
|
tmpd: Option<Rc<TempDir>>, // drop last
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1398,6 +1407,55 @@ impl UCommand {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set if process should be run in a simulated terminal (unix: pty, windows: ConPTY[not yet supported])
|
||||||
|
/// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`].
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self {
|
||||||
|
self.terminal_simulation = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn read_from_pty(pty_fd: std::os::fd::OwnedFd, out: File) {
|
||||||
|
let read_file = std::fs::File::from(pty_fd);
|
||||||
|
let mut reader = std::io::BufReader::new(read_file);
|
||||||
|
let mut writer = std::io::BufWriter::new(out);
|
||||||
|
let result = std::io::copy(&mut reader, &mut writer);
|
||||||
|
match result {
|
||||||
|
Ok(_) => {}
|
||||||
|
// Input/output error (os error 5) is returned due to pipe closes. Buffer gets content anyway.
|
||||||
|
Err(e) if e.raw_os_error().unwrap_or_default() == 5 => {}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Unexpected error: {:?}", e);
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn spawn_reader_thread(
|
||||||
|
&self,
|
||||||
|
captured_output: Option<CapturedOutput>,
|
||||||
|
pty_fd_master: OwnedFd,
|
||||||
|
name: String,
|
||||||
|
) -> Option<CapturedOutput> {
|
||||||
|
if let Some(mut captured_output_i) = captured_output {
|
||||||
|
let fd = captured_output_i.try_clone().unwrap();
|
||||||
|
|
||||||
|
let handle = std::thread::Builder::new()
|
||||||
|
.name(name)
|
||||||
|
.spawn(move || {
|
||||||
|
Self::read_from_pty(pty_fd_master, fd);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
captured_output_i.reader_thread_handle = Some(handle);
|
||||||
|
Some(captured_output_i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the `std::process::Command` and apply the defaults on fields which were not specified
|
/// Build the `std::process::Command` and apply the defaults on fields which were not specified
|
||||||
/// by the user.
|
/// by the user.
|
||||||
///
|
///
|
||||||
|
@ -1417,7 +1475,14 @@ impl UCommand {
|
||||||
/// * `stderr_to_stdout`: `false`
|
/// * `stderr_to_stdout`: `false`
|
||||||
/// * `bytes_into_stdin`: `None`
|
/// * `bytes_into_stdin`: `None`
|
||||||
/// * `limits`: `None`.
|
/// * `limits`: `None`.
|
||||||
fn build(&mut self) -> (Command, Option<CapturedOutput>, Option<CapturedOutput>) {
|
fn build(
|
||||||
|
&mut self,
|
||||||
|
) -> (
|
||||||
|
Command,
|
||||||
|
Option<CapturedOutput>,
|
||||||
|
Option<CapturedOutput>,
|
||||||
|
Option<File>,
|
||||||
|
) {
|
||||||
if self.bin_path.is_some() {
|
if self.bin_path.is_some() {
|
||||||
if let Some(util_name) = &self.util_name {
|
if let Some(util_name) = &self.util_name {
|
||||||
self.args.push_front(util_name.into());
|
self.args.push_front(util_name.into());
|
||||||
|
@ -1496,6 +1561,10 @@ impl UCommand {
|
||||||
|
|
||||||
let mut captured_stdout = None;
|
let mut captured_stdout = None;
|
||||||
let mut captured_stderr = None;
|
let mut captured_stderr = None;
|
||||||
|
#[cfg(unix)]
|
||||||
|
let mut stdin_pty: Option<File> = None;
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let stdin_pty: Option<File> = None;
|
||||||
if self.stderr_to_stdout {
|
if self.stderr_to_stdout {
|
||||||
let mut output = CapturedOutput::default();
|
let mut output = CapturedOutput::default();
|
||||||
|
|
||||||
|
@ -1529,7 +1598,39 @@ impl UCommand {
|
||||||
.stderr(stderr);
|
.stderr(stderr);
|
||||||
};
|
};
|
||||||
|
|
||||||
(command, captured_stdout, captured_stderr)
|
#[cfg(unix)]
|
||||||
|
if self.terminal_simulation {
|
||||||
|
let terminal_size = libc::winsize {
|
||||||
|
ws_col: 80,
|
||||||
|
ws_row: 30,
|
||||||
|
ws_xpixel: 800,
|
||||||
|
ws_ypixel: 300,
|
||||||
|
};
|
||||||
|
|
||||||
|
let OpenptyResult {
|
||||||
|
slave: pi_slave,
|
||||||
|
master: pi_master,
|
||||||
|
} = nix::pty::openpty(&terminal_size, None).unwrap();
|
||||||
|
let OpenptyResult {
|
||||||
|
slave: po_slave,
|
||||||
|
master: po_master,
|
||||||
|
} = nix::pty::openpty(&terminal_size, None).unwrap();
|
||||||
|
let OpenptyResult {
|
||||||
|
slave: pe_slave,
|
||||||
|
master: pe_master,
|
||||||
|
} = nix::pty::openpty(&terminal_size, None).unwrap();
|
||||||
|
|
||||||
|
stdin_pty = Some(File::from(pi_master));
|
||||||
|
|
||||||
|
captured_stdout =
|
||||||
|
self.spawn_reader_thread(captured_stdout, po_master, "stdout_reader".to_string());
|
||||||
|
captured_stderr =
|
||||||
|
self.spawn_reader_thread(captured_stderr, pe_master, "stderr_reader".to_string());
|
||||||
|
|
||||||
|
command.stdin(pi_slave).stdout(po_slave).stderr(pe_slave);
|
||||||
|
}
|
||||||
|
|
||||||
|
(command, captured_stdout, captured_stderr, stdin_pty)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the command, feeds the stdin if any, and returns the
|
/// Spawns the command, feeds the stdin if any, and returns the
|
||||||
|
@ -1538,7 +1639,7 @@ impl UCommand {
|
||||||
assert!(!self.has_run, "{}", ALREADY_RUN);
|
assert!(!self.has_run, "{}", ALREADY_RUN);
|
||||||
self.has_run = true;
|
self.has_run = true;
|
||||||
|
|
||||||
let (mut command, captured_stdout, captured_stderr) = self.build();
|
let (mut command, captured_stdout, captured_stderr, stdin_pty) = self.build();
|
||||||
log_info("run", self.to_string());
|
log_info("run", self.to_string());
|
||||||
|
|
||||||
let child = command.spawn().unwrap();
|
let child = command.spawn().unwrap();
|
||||||
|
@ -1554,7 +1655,7 @@ impl UCommand {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut child = UChild::from(self, child, captured_stdout, captured_stderr);
|
let mut child = UChild::from(self, child, captured_stdout, captured_stderr, stdin_pty);
|
||||||
|
|
||||||
if let Some(input) = self.bytes_into_stdin.take() {
|
if let Some(input) = self.bytes_into_stdin.take() {
|
||||||
child.pipe_in(input);
|
child.pipe_in(input);
|
||||||
|
@ -1619,6 +1720,7 @@ impl std::fmt::Display for UCommand {
|
||||||
struct CapturedOutput {
|
struct CapturedOutput {
|
||||||
current_file: File,
|
current_file: File,
|
||||||
output: tempfile::NamedTempFile, // drop last
|
output: tempfile::NamedTempFile, // drop last
|
||||||
|
reader_thread_handle: Option<thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CapturedOutput {
|
impl CapturedOutput {
|
||||||
|
@ -1627,6 +1729,7 @@ impl CapturedOutput {
|
||||||
Self {
|
Self {
|
||||||
current_file: output.reopen().unwrap(),
|
current_file: output.reopen().unwrap(),
|
||||||
output,
|
output,
|
||||||
|
reader_thread_handle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1703,6 +1806,7 @@ impl Default for CapturedOutput {
|
||||||
Self {
|
Self {
|
||||||
current_file: file.reopen().unwrap(),
|
current_file: file.reopen().unwrap(),
|
||||||
output: file,
|
output: file,
|
||||||
|
reader_thread_handle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1836,6 +1940,7 @@ pub struct UChild {
|
||||||
util_name: Option<String>,
|
util_name: Option<String>,
|
||||||
captured_stdout: Option<CapturedOutput>,
|
captured_stdout: Option<CapturedOutput>,
|
||||||
captured_stderr: Option<CapturedOutput>,
|
captured_stderr: Option<CapturedOutput>,
|
||||||
|
stdin_pty: Option<File>,
|
||||||
ignore_stdin_write_error: bool,
|
ignore_stdin_write_error: bool,
|
||||||
stderr_to_stdout: bool,
|
stderr_to_stdout: bool,
|
||||||
join_handle: Option<JoinHandle<io::Result<()>>>,
|
join_handle: Option<JoinHandle<io::Result<()>>>,
|
||||||
|
@ -1849,6 +1954,7 @@ impl UChild {
|
||||||
child: Child,
|
child: Child,
|
||||||
captured_stdout: Option<CapturedOutput>,
|
captured_stdout: Option<CapturedOutput>,
|
||||||
captured_stderr: Option<CapturedOutput>,
|
captured_stderr: Option<CapturedOutput>,
|
||||||
|
stdin_pty: Option<File>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
raw: child,
|
raw: child,
|
||||||
|
@ -1856,6 +1962,7 @@ impl UChild {
|
||||||
util_name: ucommand.util_name.clone(),
|
util_name: ucommand.util_name.clone(),
|
||||||
captured_stdout,
|
captured_stdout,
|
||||||
captured_stderr,
|
captured_stderr,
|
||||||
|
stdin_pty,
|
||||||
ignore_stdin_write_error: ucommand.ignore_stdin_write_error,
|
ignore_stdin_write_error: ucommand.ignore_stdin_write_error,
|
||||||
stderr_to_stdout: ucommand.stderr_to_stdout,
|
stderr_to_stdout: ucommand.stderr_to_stdout,
|
||||||
join_handle: None,
|
join_handle: None,
|
||||||
|
@ -1996,11 +2103,19 @@ impl UChild {
|
||||||
/// error.
|
/// error.
|
||||||
#[deprecated = "Please use wait() -> io::Result<CmdResult> instead."]
|
#[deprecated = "Please use wait() -> io::Result<CmdResult> instead."]
|
||||||
pub fn wait_with_output(mut self) -> io::Result<Output> {
|
pub fn wait_with_output(mut self) -> io::Result<Output> {
|
||||||
|
// some apps do not stop execution until their stdin gets closed.
|
||||||
|
// to prevent a endless waiting here, we close the stdin.
|
||||||
|
self.join(); // ensure that all pending async input is piped in
|
||||||
|
self.close_stdin();
|
||||||
|
|
||||||
let output = if let Some(timeout) = self.timeout {
|
let output = if let Some(timeout) = self.timeout {
|
||||||
let child = self.raw;
|
let child = self.raw;
|
||||||
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
let handle = thread::spawn(move || sender.send(child.wait_with_output()));
|
let handle = thread::Builder::new()
|
||||||
|
.name("wait_with_output".to_string())
|
||||||
|
.spawn(move || sender.send(child.wait_with_output()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
match receiver.recv_timeout(timeout) {
|
match receiver.recv_timeout(timeout) {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
|
@ -2032,9 +2147,17 @@ impl UChild {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(stdout) = self.captured_stdout.as_mut() {
|
if let Some(stdout) = self.captured_stdout.as_mut() {
|
||||||
|
stdout
|
||||||
|
.reader_thread_handle
|
||||||
|
.take()
|
||||||
|
.map(|handle| handle.join().unwrap());
|
||||||
output.stdout = stdout.output_bytes();
|
output.stdout = stdout.output_bytes();
|
||||||
}
|
}
|
||||||
if let Some(stderr) = self.captured_stderr.as_mut() {
|
if let Some(stderr) = self.captured_stderr.as_mut() {
|
||||||
|
stderr
|
||||||
|
.reader_thread_handle
|
||||||
|
.take()
|
||||||
|
.map(|handle| handle.join().unwrap());
|
||||||
output.stderr = stderr.output_bytes();
|
output.stderr = stderr.output_bytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2196,6 +2319,29 @@ impl UChild {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn access_stdin_as_writer<'a>(&'a mut self) -> Box<dyn Write + Send + 'a> {
|
||||||
|
if let Some(stdin_fd) = &self.stdin_pty {
|
||||||
|
Box::new(BufWriter::new(stdin_fd.try_clone().unwrap()))
|
||||||
|
} else {
|
||||||
|
let stdin: &mut std::process::ChildStdin = self.raw.stdin.as_mut().unwrap();
|
||||||
|
Box::new(BufWriter::new(stdin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_stdin_as_writer(&mut self) -> Box<dyn Write + Send> {
|
||||||
|
if let Some(stdin_fd) = mem::take(&mut self.stdin_pty) {
|
||||||
|
Box::new(BufWriter::new(stdin_fd))
|
||||||
|
} else {
|
||||||
|
let stdin = self
|
||||||
|
.raw
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.expect("Could not pipe into child process. Was it set to Stdio::null()?");
|
||||||
|
|
||||||
|
Box::new(BufWriter::new(stdin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pipe data into [`Child`] stdin in a separate thread to avoid deadlocks.
|
/// Pipe data into [`Child`] stdin in a separate thread to avoid deadlocks.
|
||||||
///
|
///
|
||||||
/// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the
|
/// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the
|
||||||
|
@ -2217,24 +2363,24 @@ impl UChild {
|
||||||
/// [`JoinHandle`]: std::thread::JoinHandle
|
/// [`JoinHandle`]: std::thread::JoinHandle
|
||||||
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, content: T) -> &mut Self {
|
pub fn pipe_in<T: Into<Vec<u8>>>(&mut self, content: T) -> &mut Self {
|
||||||
let ignore_stdin_write_error = self.ignore_stdin_write_error;
|
let ignore_stdin_write_error = self.ignore_stdin_write_error;
|
||||||
let content = content.into();
|
let mut content: Vec<u8> = content.into();
|
||||||
let stdin = self
|
if self.stdin_pty.is_some() {
|
||||||
.raw
|
content.append(&mut END_OF_TRANSMISSION_SEQUENCE.to_vec());
|
||||||
.stdin
|
}
|
||||||
.take()
|
let mut writer = self.take_stdin_as_writer();
|
||||||
.expect("Could not pipe into child process. Was it set to Stdio::null()?");
|
|
||||||
|
|
||||||
let join_handle = thread::spawn(move || {
|
let join_handle = std::thread::Builder::new()
|
||||||
let mut writer = BufWriter::new(stdin);
|
.name("pipe_in".to_string())
|
||||||
|
.spawn(
|
||||||
match writer.write_all(&content).and_then(|()| writer.flush()) {
|
move || match writer.write_all(&content).and_then(|()| writer.flush()) {
|
||||||
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
|
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("failed to write to stdin of child: {error}"),
|
format!("failed to write to stdin of child: {error}"),
|
||||||
)),
|
)),
|
||||||
Ok(()) | Err(_) => Ok(()),
|
Ok(()) | Err(_) => Ok(()),
|
||||||
}
|
},
|
||||||
});
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
self.join_handle = Some(join_handle);
|
self.join_handle = Some(join_handle);
|
||||||
self
|
self
|
||||||
|
@ -2277,10 +2423,11 @@ impl UChild {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// If [`ChildStdin::write_all`] or [`ChildStdin::flush`] returned an error
|
/// If [`ChildStdin::write_all`] or [`ChildStdin::flush`] returned an error
|
||||||
pub fn try_write_in<T: Into<Vec<u8>>>(&mut self, data: T) -> io::Result<()> {
|
pub fn try_write_in<T: Into<Vec<u8>>>(&mut self, data: T) -> io::Result<()> {
|
||||||
let stdin = self.raw.stdin.as_mut().unwrap();
|
let ignore_stdin_write_error = self.ignore_stdin_write_error;
|
||||||
|
let mut writer = self.access_stdin_as_writer();
|
||||||
|
|
||||||
match stdin.write_all(&data.into()).and_then(|()| stdin.flush()) {
|
match writer.write_all(&data.into()).and_then(|()| writer.flush()) {
|
||||||
Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new(
|
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
|
||||||
io::ErrorKind::Other,
|
io::ErrorKind::Other,
|
||||||
format!("failed to write to stdin of child: {error}"),
|
format!("failed to write to stdin of child: {error}"),
|
||||||
)),
|
)),
|
||||||
|
@ -2317,6 +2464,11 @@ impl UChild {
|
||||||
/// Note, this does not have any effect if using the [`UChild::pipe_in`] method.
|
/// Note, this does not have any effect if using the [`UChild::pipe_in`] method.
|
||||||
pub fn close_stdin(&mut self) -> &mut Self {
|
pub fn close_stdin(&mut self) -> &mut Self {
|
||||||
self.raw.stdin.take();
|
self.raw.stdin.take();
|
||||||
|
if self.stdin_pty.is_some() {
|
||||||
|
// a pty can not be closed. We need to send a EOT:
|
||||||
|
let _ = self.try_write_in(END_OF_TRANSMISSION_SEQUENCE);
|
||||||
|
self.stdin_pty.take();
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3415,4 +3567,97 @@ mod tests {
|
||||||
xattr::set(&file_path2, test_attr, test_value).unwrap();
|
xattr::set(&file_path2, test_attr, test_value).unwrap();
|
||||||
assert!(compare_xattrs(&file_path1, &file_path2));
|
assert!(compare_xattrs(&file_path1, &file_path2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_simulation_of_terminal_false() {
|
||||||
|
let scene = TestScenario::new("util");
|
||||||
|
|
||||||
|
let out = scene.ccmd("env").arg("sh").arg("is_atty.sh").succeeds();
|
||||||
|
std::assert_eq!(
|
||||||
|
String::from_utf8_lossy(out.stdout()),
|
||||||
|
"stdin is not atty\nstdout is not atty\nstderr is not atty\n"
|
||||||
|
);
|
||||||
|
std::assert_eq!(
|
||||||
|
String::from_utf8_lossy(out.stderr()),
|
||||||
|
"This is an error message.\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_simulation_of_terminal_true() {
|
||||||
|
let scene = TestScenario::new("util");
|
||||||
|
|
||||||
|
let out = scene
|
||||||
|
.ccmd("env")
|
||||||
|
.arg("sh")
|
||||||
|
.arg("is_atty.sh")
|
||||||
|
.terminal_simulation(true)
|
||||||
|
.succeeds();
|
||||||
|
std::assert_eq!(
|
||||||
|
String::from_utf8_lossy(out.stdout()),
|
||||||
|
"stdin is atty\r\nstdout is atty\r\nstderr is atty\r\n"
|
||||||
|
);
|
||||||
|
std::assert_eq!(
|
||||||
|
String::from_utf8_lossy(out.stderr()),
|
||||||
|
"This is an error message.\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_simulation_of_terminal_pty_sends_eot_automatically() {
|
||||||
|
let scene = TestScenario::new("util");
|
||||||
|
|
||||||
|
let mut cmd = scene.ccmd("env");
|
||||||
|
cmd.timeout(std::time::Duration::from_secs(10));
|
||||||
|
cmd.args(&["cat", "-"]);
|
||||||
|
cmd.terminal_simulation(true);
|
||||||
|
let child = cmd.run_no_wait();
|
||||||
|
let out = child.wait().unwrap(); // cat would block if there is no eot
|
||||||
|
|
||||||
|
std::assert_eq!(String::from_utf8_lossy(out.stderr()), "");
|
||||||
|
std::assert_eq!(String::from_utf8_lossy(out.stdout()), "\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_simulation_of_terminal_pty_pipes_into_data_and_sends_eot_automatically() {
|
||||||
|
let scene = TestScenario::new("util");
|
||||||
|
|
||||||
|
let message = "Hello stdin forwarding!";
|
||||||
|
|
||||||
|
let mut cmd = scene.ccmd("env");
|
||||||
|
cmd.args(&["cat", "-"]);
|
||||||
|
cmd.terminal_simulation(true);
|
||||||
|
cmd.pipe_in(message);
|
||||||
|
let child = cmd.run_no_wait();
|
||||||
|
let out = child.wait().unwrap();
|
||||||
|
|
||||||
|
std::assert_eq!(
|
||||||
|
String::from_utf8_lossy(out.stdout()),
|
||||||
|
format!("{}\r\n", message)
|
||||||
|
);
|
||||||
|
std::assert_eq!(String::from_utf8_lossy(out.stderr()), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_simulation_of_terminal_pty_write_in_data_and_sends_eot_automatically() {
|
||||||
|
let scene = TestScenario::new("util");
|
||||||
|
|
||||||
|
let mut cmd = scene.ccmd("env");
|
||||||
|
cmd.args(&["cat", "-"]);
|
||||||
|
cmd.terminal_simulation(true);
|
||||||
|
let mut child = cmd.run_no_wait();
|
||||||
|
child.write_in("Hello stdin forwarding via write_in!");
|
||||||
|
let out = child.wait().unwrap();
|
||||||
|
|
||||||
|
std::assert_eq!(
|
||||||
|
String::from_utf8_lossy(out.stdout()),
|
||||||
|
"Hello stdin forwarding via write_in!\r\n"
|
||||||
|
);
|
||||||
|
std::assert_eq!(String::from_utf8_lossy(out.stderr()), "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
tests/fixtures/nohup/is_atty.sh
vendored
Normal file
21
tests/fixtures/nohup/is_atty.sh
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -t 0 ] ; then
|
||||||
|
echo "stdin is atty"
|
||||||
|
else
|
||||||
|
echo "stdin is not atty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -t 1 ] ; then
|
||||||
|
echo "stdout is atty"
|
||||||
|
else
|
||||||
|
echo "stdout is not atty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -t 2 ] ; then
|
||||||
|
echo "stderr is atty"
|
||||||
|
else
|
||||||
|
echo "stderr is not atty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
true
|
23
tests/fixtures/util/is_atty.sh
vendored
Normal file
23
tests/fixtures/util/is_atty.sh
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -t 0 ] ; then
|
||||||
|
echo "stdin is atty"
|
||||||
|
else
|
||||||
|
echo "stdin is not atty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -t 1 ] ; then
|
||||||
|
echo "stdout is atty"
|
||||||
|
else
|
||||||
|
echo "stdout is not atty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -t 2 ] ; then
|
||||||
|
echo "stderr is atty"
|
||||||
|
else
|
||||||
|
echo "stderr is not atty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
>&2 echo "This is an error message."
|
||||||
|
|
||||||
|
true
|
Loading…
Add table
Add a link
Reference in a new issue