Merge pull request from kov/setsid

Add setsid implementation
This commit is contained in:
Sylvestre Ledru 2024-10-17 21:53:27 +02:00 committed by GitHub
commit 20737f09c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 753 additions and 28 deletions

11
Cargo.lock generated

@ -869,6 +869,7 @@ dependencies = [
"clap_mangen",
"dns-lookup",
"libc",
"nix",
"phf",
"phf_codegen",
"pretty_assertions",
@ -884,6 +885,7 @@ dependencies = [
"uu_lsmem",
"uu_mountpoint",
"uu_rev",
"uu_setsid",
"uucore",
"xattr",
]
@ -942,6 +944,15 @@ dependencies = [
"uucore",
]
[[package]]
name = "uu_setsid"
version = "0.0.1"
dependencies = [
"clap",
"libc",
"uucore",
]
[[package]]
name = "uucore"
version = "0.0.27"

@ -31,6 +31,7 @@ feat_common_core = [
"lsmem",
"ctrlaltdel",
"rev",
"setsid",
"last"
]
@ -46,6 +47,7 @@ phf = "0.11.2"
phf_codegen = "0.11.2"
textwrap = { version = "0.16.0", features = ["terminal_size"] }
xattr = "1.3.1"
nix = { version = "0.28", default-features = false }
tempfile = "3.9.0"
rand = { version = "0.8", features = ["small_rng"] }
serde = { version = "1.0", features = ["derive"] }
@ -68,6 +70,7 @@ lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/
mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" }
ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" }
rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" }
setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" }
last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" }
[dev-dependencies]
@ -80,6 +83,7 @@ uucore = { workspace = true, features = ["entries", "process", "signals"] }
[target.'cfg(unix)'.dev-dependencies]
xattr = { workspace = true }
nix = { workspace = true, features = ["term"] }
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies]
procfs = { version = "0.17", default-features = false }

16
src/uu/setsid/Cargo.toml Normal file

@ -0,0 +1,16 @@
[package]
name = "uu_setsid"
version = "0.0.1"
edition = "2021"
[dependencies]
uucore = { workspace = true }
libc = { workspace = true }
clap = { workspace = true }
[lib]
path = "src/setsid.rs"
[[bin]]
name = "setsid"
path = "src/main.rs"

7
src/uu/setsid/setsid.md Normal file

@ -0,0 +1,7 @@
# setsid
```
setsid [options] <program> [argument ...]
```
Run a program in a new session.

@ -0,0 +1 @@
uucore::bin!(uu_setsid);

199
src/uu/setsid/src/setsid.rs Normal file

@ -0,0 +1,199 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::builder::ValueParser;
use clap::{crate_version, Command};
use clap::{Arg, ArgAction};
use uucore::{error::UResult, format_usage, help_about, help_usage};
const ABOUT: &str = help_about!("setsid.md");
const USAGE: &str = help_usage!("setsid.md");
#[cfg(target_family = "unix")]
mod unix {
pub use std::ffi::{OsStr, OsString};
pub use std::os::unix::process::CommandExt;
pub use std::{io, process};
pub use uucore::error::{FromIo, UIoError, UResult};
// The promise made by setsid(1) is that it forks if the process
// is already a group leader, not session leader.
pub fn already_group_leader() -> bool {
let leader = unsafe { libc::getpgrp() };
leader == process::id() as i32
}
pub fn report_failure_to_exec(error: io::Error, executable: &OsStr, set_error: bool) {
let kind = error.kind();
// FIXME: POSIX wants certain exit statuses for specific errors, should
// these be handled by uucore::error? We should be able to just return
// the UError here.
uucore::show_error!(
"failed to execute {}: {}",
executable.to_string_lossy(),
UIoError::from(error)
);
if set_error {
if kind == io::ErrorKind::NotFound {
uucore::error::set_exit_code(127);
} else if kind == io::ErrorKind::PermissionDenied {
uucore::error::set_exit_code(126);
}
}
}
// This function will be potentially called after a fork(), so what it can do
// is quite restricted. This is the meat of the program.
pub fn prepare_child(take_controlling_tty: bool) -> io::Result<()> {
// SAFETY: this is effectively a wrapper to the setsid syscall.
let pid = unsafe { libc::setsid() };
// We fork if we are already a group leader, so an error
// here should be impossible.
assert_eq!(pid, process::id() as i32);
// On some platforms (i.e. aarch64 Linux) TIOCSCTTY is the same type as the second argument,
// but on some it is u64, while the expected type is u32.
// SAFETY: the ioctl should not make any changes to memory, basically a wrapper
// to the syscall.
#[allow(clippy::useless_conversion)]
if take_controlling_tty && unsafe { libc::ioctl(0, libc::TIOCSCTTY.into(), 1) } < 0 {
// This is unfortunate, but we are bound by the Result type pre_exec requires,
// as well as the limitations imposed by this being executed post-fork().
// Ideally we would return an io::Error of the Other kind so that we could handle
// everything at the same place, but that would require an allocation.
uucore::show_error!(
"failed to set the controlling terminal: {}",
UIoError::from(io::Error::last_os_error())
);
// SAFETY: this is actually safer than calling process::exit(), as that may
// allocate, which is not safe post-fork.
unsafe { libc::_exit(1) };
}
Ok(())
}
pub fn spawn_command(
mut to_run: process::Command,
executable: &OsStr,
wait_child: bool,
) -> UResult<()> {
let mut child = match to_run.spawn() {
Ok(child) => child,
Err(error) => {
report_failure_to_exec(error, executable, wait_child);
return Ok(());
}
};
if !wait_child {
return Ok(());
}
match child.wait() {
Ok(status) => {
uucore::error::set_exit_code(status.code().unwrap());
Ok(())
}
Err(error) => {
Err(error.map_err_context(|| format!("failed to wait on PID {}", child.id())))
}
}
}
}
#[cfg(target_family = "unix")]
use unix::*;
#[cfg(target_family = "unix")]
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
let force_fork = matches.get_flag("fork");
let wait_child = matches.get_flag("wait");
let take_controlling_tty = matches.get_flag("ctty");
let command: Vec<_> = match matches.get_many::<OsString>("command") {
Some(v) => v.collect(),
None => return Err(uucore::error::USimpleError::new(1, "no command specified")),
};
// We know we have at least one item, as none was
// handled as an error on the match above.
let executable = command[0];
let arguments = command.get(1..).unwrap_or(&[]);
let mut to_run = process::Command::new(executable);
to_run.args(arguments.iter());
// SAFETY: pre_exec() happens post-fork, so the process can potentially
// be in a broken state; allocations are not safe, and we should exit
// as soon as possible if we cannot go ahead.
unsafe {
to_run.pre_exec(move || prepare_child(take_controlling_tty));
};
if force_fork || already_group_leader() {
spawn_command(to_run, executable, wait_child)?;
} else {
let error = to_run.exec();
report_failure_to_exec(error, executable, true);
}
Ok(())
}
#[cfg(not(target_family = "unix"))]
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
Err(uucore::error::USimpleError::new(
1,
"`setsid` is unavailable on non-UNIX-like platforms.",
))
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(
Arg::new("ctty")
.short('c')
.action(ArgAction::SetTrue)
.value_parser(ValueParser::bool())
.help("Take the current controlling terminal"),
)
.arg(
Arg::new("fork")
.short('f')
.action(ArgAction::SetTrue)
.value_parser(ValueParser::bool())
.long_help("Always create a new process. By default this is only done if we are already a process group lead."),
)
.arg(
Arg::new("wait")
.short('w')
.action(ArgAction::SetTrue)
.value_parser(ValueParser::bool())
.help("Wait for the command to finish and exit with its exit code."),
)
.arg(
Arg::new("command")
.help("Program to be executed, followed by its arguments")
.index(1)
.action(ArgAction::Set)
.trailing_var_arg(true)
.value_parser(ValueParser::os_string())
.num_args(1..),
)
}

@ -0,0 +1,174 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
#[cfg(target_family = "unix")]
mod unix {
use crate::common::util::{TestScenario, UCommand, TESTS_BINARY};
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
new_ucmd!()
.arg("-f")
.fails()
.code_is(1)
.stderr_is("setsid: no command specified\n");
}
#[test]
fn fork_isolates_child_exit_code() {
new_ucmd!()
.arg("-f")
.arg("/usr/bin/false")
.succeeds()
.no_output();
}
#[test]
fn non_fork_returns_child_exit_code() {
new_ucmd!()
.arg("/usr/bin/false")
.fails()
.code_is(1)
.no_output();
}
#[test]
fn fork_wait_returns_child_exit_code() {
new_ucmd!()
.arg("-f")
.arg("-w")
.arg("/usr/bin/false")
.fails()
.code_is(1)
.no_output();
}
#[test]
fn non_fork_returns_not_found_error() {
new_ucmd!()
.arg("/usr/bin/this-tool-does-not-exist-hopefully")
.fails()
.code_is(127)
.stderr_is("setsid: failed to execute /usr/bin/this-tool-does-not-exist-hopefully: No such file or directory\n");
}
#[test]
fn non_fork_on_non_executable_returns_permission_denied_error() {
new_ucmd!()
.arg("/etc/passwd")
.fails()
.code_is(126)
.stderr_is("setsid: failed to execute /etc/passwd: Permission denied\n");
}
#[test]
fn fork_isolates_not_found_error() {
new_ucmd!()
.arg("-f")
.arg("/usr/bin/this-tool-does-not-exist-hopefully")
.succeeds();
// no test for output, as it's a race whether the not found error gets printed
// quickly enough, potential flakyness
}
#[test]
fn unprivileged_user_cannot_steal_controlling_tty() {
let shell_cmd =
format!("{TESTS_BINARY} setsid -w -c {TESTS_BINARY} setsid -w -c /b/usrin/true");
UCommand::new()
.terminal_simulation(true)
.arg(&shell_cmd)
.fails()
.code_is(1)
.no_stdout()
.stderr_is("setsid: failed to set the controlling terminal: Permission denied\r\n");
}
#[cfg(target_os = "linux")]
#[test]
fn unprivileged_user_can_take_new_controlling_tty() {
let shell_cmd = format!(
"/usr/bin/cat /proc/self/stat; {TESTS_BINARY} setsid -w -c /usr/bin/cat /proc/self/stat"
);
let cmd_result = UCommand::new()
.terminal_simulation(true)
.arg(&shell_cmd)
.succeeds();
let output = cmd_result.code_is(0).no_stderr().stdout_str();
// /proc/self/stat format has controlling terminal as the 7th
// space-separated item; if we managed to change the controlling
// terminal, we should see a difference there
let (before, after) = output
.split_once('\n')
.expect("expected 2 lines of output at least");
let before = before
.split_whitespace()
.nth(6)
.expect("unexpected stat format");
let after = after
.split_whitespace()
.nth(6)
.expect("unexpected stat format");
assert_ne!(before, after);
}
#[cfg(target_os = "linux")]
#[test]
fn setsid_takes_session_leadership() {
let shell_cmd = format!(
"/usr/bin/cat /proc/self/stat; {TESTS_BINARY} setsid /usr/bin/cat /proc/self/stat"
);
let cmd_result = UCommand::new()
.terminal_simulation(true)
.arg(&shell_cmd)
.run();
let output = cmd_result.code_is(0).no_stderr().stdout_str();
// /proc/self/stat format has sessiion ID as the 6th space-separated
// item; if we managed to get session leadership, we should see a
// difference there...
let (before, after) = output
.split_once('\n')
.expect("expected 2 lines of output at least");
let before = before
.split_whitespace()
.nth(5)
.expect("unexpected stat format");
let after = after
.split_whitespace()
.nth(5)
.expect("unexpected stat format");
assert_ne!(before, after);
// ...and it should actually be the PID of our child! We take the child
// PID here to avoid differences in handling by different shells or
// distributions.
let pid = after.split_whitespace().next().unwrap();
assert_eq!(after, pid);
}
}
#[cfg(not(target_family = "unix"))]
mod non_unix {
use crate::common::util::{TestScenario, UCommand};
#[test]
fn unsupported_platforms() {
new_ucmd!()
.arg("/usr/bin/true")
.fails()
.code_is(1)
.stderr_is("setsid: `setsid` is unavailable on non-UNIX-like platforms.\n");
}
}

@ -3,10 +3,12 @@
// For the full copyright and license information, please view the LICENSE
// 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, unexpected_cfgs)]
#[cfg(unix)]
use nix::pty::OpenptyResult;
use pretty_assertions::assert_eq;
#[cfg(any(target_os = "linux", target_os = "android"))]
use rlimit::prlimit;
@ -19,6 +21,8 @@ use std::ffi::{OsStr, OsString};
use std::fs::{self, hard_link, remove_file, File, OpenOptions};
use std::io::{self, BufWriter, Read, Result, Write};
#[cfg(unix)]
use std::os::fd::OwnedFd;
#[cfg(unix)]
use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file, PermissionsExt};
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
@ -32,7 +36,7 @@ use std::rc::Rc;
use std::sync::mpsc::{self, RecvTimeoutError};
use std::thread::{sleep, JoinHandle};
use std::time::{Duration, Instant};
use std::{env, hint, thread};
use std::{env, hint, mem, thread};
use tempfile::{Builder, TempDir};
static TESTS_DIR: &str = "tests";
@ -44,6 +48,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 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_util-linux");
pub const PATH: &str = env!("PATH");
@ -385,7 +390,10 @@ impl CmdResult {
pub fn success(&self) -> &Self {
assert!(
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.stderr_str()
);
@ -1218,6 +1226,10 @@ pub struct UCommand {
limits: Vec<(rlimit::Resource, u64, u64)>,
stderr_to_stdout: bool,
timeout: Option<Duration>,
#[cfg(unix)]
terminal_simulation: bool,
#[cfg(unix)]
terminal_size: Option<libc::winsize>,
tmpd: Option<Rc<TempDir>>, // drop last
}
@ -1396,6 +1408,68 @@ impl UCommand {
self
}
/// Set if process should be run in a simulated terminal
///
/// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`].
/// (unix: pty, windows: ConPTY[not yet supported])
#[cfg(unix)]
pub fn terminal_simulation(&mut self, enable: bool) -> &mut Self {
self.terminal_simulation = enable;
self
}
/// Set if process should be run in a simulated terminal with specific size
///
/// This is useful to test behavior that is only active if [`stdout.is_terminal()`] is [`true`].
/// And the size of the terminal matters additionally.
#[cfg(unix)]
pub fn terminal_size(&mut self, win_size: libc::winsize) -> &mut Self {
self.terminal_simulation(true);
self.terminal_size = Some(win_size);
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);
panic!("error forwarding output of pty");
}
}
}
#[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
/// by the user.
///
@ -1415,7 +1489,14 @@ impl UCommand {
/// * `stderr_to_stdout`: `false`
/// * `bytes_into_stdin`: `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 let Some(util_name) = &self.util_name {
self.args.push_front(util_name.into());
@ -1494,6 +1575,10 @@ impl UCommand {
let mut captured_stdout = 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 {
let mut output = CapturedOutput::default();
@ -1527,7 +1612,39 @@ impl UCommand {
.stderr(stderr);
};
(command, captured_stdout, captured_stderr)
#[cfg(unix)]
if self.terminal_simulation {
let terminal_size = self.terminal_size.unwrap_or(libc::winsize {
ws_col: 80,
ws_row: 30,
ws_xpixel: 80 * 8,
ws_ypixel: 30 * 10,
});
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
@ -1536,7 +1653,7 @@ impl UCommand {
assert!(!self.has_run, "{}", ALREADY_RUN);
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());
let child = command.spawn().unwrap();
@ -1552,7 +1669,7 @@ impl UCommand {
.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() {
child.pipe_in(input);
@ -1617,6 +1734,7 @@ impl std::fmt::Display for UCommand {
struct CapturedOutput {
current_file: File,
output: tempfile::NamedTempFile, // drop last
reader_thread_handle: Option<thread::JoinHandle<()>>,
}
impl CapturedOutput {
@ -1625,6 +1743,7 @@ impl CapturedOutput {
Self {
current_file: output.reopen().unwrap(),
output,
reader_thread_handle: None,
}
}
@ -1701,6 +1820,7 @@ impl Default for CapturedOutput {
Self {
current_file: file.reopen().unwrap(),
output: file,
reader_thread_handle: None,
}
}
}
@ -1834,6 +1954,7 @@ pub struct UChild {
util_name: Option<String>,
captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>,
stdin_pty: Option<File>,
ignore_stdin_write_error: bool,
stderr_to_stdout: bool,
join_handle: Option<JoinHandle<io::Result<()>>>,
@ -1847,6 +1968,7 @@ impl UChild {
child: Child,
captured_stdout: Option<CapturedOutput>,
captured_stderr: Option<CapturedOutput>,
stdin_pty: Option<File>,
) -> Self {
Self {
raw: child,
@ -1854,6 +1976,7 @@ impl UChild {
util_name: ucommand.util_name.clone(),
captured_stdout,
captured_stderr,
stdin_pty,
ignore_stdin_write_error: ucommand.ignore_stdin_write_error,
stderr_to_stdout: ucommand.stderr_to_stdout,
join_handle: None,
@ -1994,11 +2117,19 @@ impl UChild {
/// error.
#[deprecated = "Please use wait() -> io::Result<CmdResult> instead."]
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 child = self.raw;
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) {
Ok(result) => {
@ -2030,9 +2161,15 @@ impl UChild {
};
if let Some(stdout) = self.captured_stdout.as_mut() {
if let Some(handle) = stdout.reader_thread_handle.take() {
handle.join().unwrap();
}
output.stdout = stdout.output_bytes();
}
if let Some(stderr) = self.captured_stderr.as_mut() {
if let Some(handle) = stderr.reader_thread_handle.take() {
handle.join().unwrap();
}
output.stderr = stderr.output_bytes();
}
@ -2194,6 +2331,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.
///
/// In contrast to [`UChild::write_in`], this method is designed to simulate a pipe on the
@ -2215,24 +2375,24 @@ impl UChild {
/// [`JoinHandle`]: std::thread::JoinHandle
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 content = content.into();
let stdin = self
.raw
.stdin
.take()
.expect("Could not pipe into child process. Was it set to Stdio::null()?");
let mut content: Vec<u8> = content.into();
if self.stdin_pty.is_some() {
content.append(&mut END_OF_TRANSMISSION_SEQUENCE.to_vec());
}
let mut writer = self.take_stdin_as_writer();
let join_handle = thread::spawn(move || {
let mut writer = BufWriter::new(stdin);
match writer.write_all(&content).and_then(|()| writer.flush()) {
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
io::ErrorKind::Other,
format!("failed to write to stdin of child: {error}"),
)),
Ok(()) | Err(_) => Ok(()),
}
});
let join_handle = std::thread::Builder::new()
.name("pipe_in".to_string())
.spawn(
move || match writer.write_all(&content).and_then(|()| writer.flush()) {
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
io::ErrorKind::Other,
format!("failed to write to stdin of child: {error}"),
)),
Ok(()) | Err(_) => Ok(()),
},
)
.unwrap();
self.join_handle = Some(join_handle);
self
@ -2275,10 +2435,11 @@ impl UChild {
/// # Errors
/// 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<()> {
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()) {
Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new(
match writer.write_all(&data.into()).and_then(|()| writer.flush()) {
Err(error) if !ignore_stdin_write_error => Err(io::Error::new(
io::ErrorKind::Other,
format!("failed to write to stdin of child: {error}"),
)),
@ -2315,6 +2476,11 @@ impl UChild {
/// Note, this does not have any effect if using the [`UChild::pipe_in`] method.
pub fn close_stdin(&mut self) -> &mut Self {
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
}
}
@ -3413,4 +3579,123 @@ mod tests {
xattr::set(&file_path2, test_attr, test_value).unwrap();
assert!(compare_xattrs(&file_path1, &file_path2));
}
#[cfg(unix)]
#[test]
fn test_simulation_of_terminal_false() {
let scene = TestScenario::new("util");
let out = scene.cmd("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
.cmd("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\nterminal size: 30 80\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_size_information() {
let scene = TestScenario::new("util");
let out = scene
.cmd("env")
.arg("sh")
.arg("is_atty.sh")
.terminal_size(libc::winsize {
ws_col: 40,
ws_row: 10,
ws_xpixel: 40 * 8,
ws_ypixel: 10 * 10,
})
.succeeds();
std::assert_eq!(
String::from_utf8_lossy(out.stdout()),
"stdin is atty\r\nstdout is atty\r\nstderr is atty\r\nterminal size: 10 40\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.cmd("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.cmd("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.cmd("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()), "");
}
}

24
tests/fixtures/util/is_atty.sh vendored Normal file

@ -0,0 +1,24 @@
#!/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"
echo "terminal size: $(stty size)"
else
echo "stderr is not atty"
fi
>&2 echo "This is an error message."
true

@ -25,6 +25,10 @@ mod test_ctrlaltdel;
#[path = "by-util/test_rev.rs"]
mod test_rev;
#[cfg(feature = "setsid")]
#[path = "by-util/test_setsid.rs"]
mod test_setsid;
#[cfg(feature = "last")]
#[path = "by-util/test_last.rs"]
mod test_last;