commit
20737f09c4
11
Cargo.lock
generated
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
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
7
src/uu/setsid/setsid.md
Normal file
@ -0,0 +1,7 @@
|
||||
# setsid
|
||||
|
||||
```
|
||||
setsid [options] <program> [argument ...]
|
||||
```
|
||||
|
||||
Run a program in a new session.
|
1
src/uu/setsid/src/main.rs
Normal file
1
src/uu/setsid/src/main.rs
Normal file
@ -0,0 +1 @@
|
||||
uucore::bin!(uu_setsid);
|
199
src/uu/setsid/src/setsid.rs
Normal file
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..),
|
||||
)
|
||||
}
|
174
tests/by-util/test_setsid.rs
Normal file
174
tests/by-util/test_setsid.rs
Normal file
@ -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
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user