Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use walkdir::{DirEntry, WalkDir};

use crate::{
CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes, copy_file,
write_stdout_line,
};

/// Ensure a Windows path starts with a `\\?`.
Expand Down Expand Up @@ -271,10 +272,10 @@ fn copy_direntry(
Some(&entry.source_absolute),
)?;
if options.verbose {
println!(
write_stdout_line(format_args!(
"{}",
context_for(&entry.source_relative, &entry.local_to_target)
);
))?;
}
Ok(())
};
Expand Down Expand Up @@ -392,7 +393,7 @@ pub(crate) fn copy_directory(
// a/b -> d/a/b
//
for (x, y) in aligned_ancestors(root, &target.join(root)) {
println!("{} -> {}", x.display(), y.display());
write_stdout_line(format_args!("{} -> {}", x.display(), y.display()))?;
}
}

Expand Down
64 changes: 42 additions & 22 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ impl UError for CpError {

pub type CopyResult<T> = Result<T, CpError>;

fn write_stdout_line(args: fmt::Arguments) -> CopyResult<()> {
use std::io::Write;

uucore::check_stdout_write(1)?;
let mut stdout = io::stdout().lock();
stdout.write_fmt(args)?;
stdout.write_all(b"\n")?;
Ok(())
}

/// Specifies how to overwrite files.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub enum ClobberMode {
Expand Down Expand Up @@ -446,11 +456,16 @@ impl Display for SparseDebug {
/// This function prints the debug information of a file copy operation if
/// no hard link or symbolic link is required, and data copy is required.
/// It prints the debug information of the offload, reflink, and sparse detection actions.
fn show_debug(copy_debug: &CopyDebug) {
println!(
fn show_debug(copy_debug: &CopyDebug) -> CopyResult<()> {
write_stdout_line(format_args!(
"{}",
translate!("cp-debug-copy-offload", "offload" => copy_debug.offload, "reflink" => copy_debug.reflink, "sparse" => copy_debug.sparse_detection)
);
translate!(
"cp-debug-copy-offload",
"offload" => copy_debug.offload,
"reflink" => copy_debug.reflink,
"sparse" => copy_debug.sparse_detection
)
))
}

static EXIT_ERR: i32 = 1;
Expand Down Expand Up @@ -1573,7 +1588,10 @@ impl OverwriteMode {
match *self {
Self::NoClobber => {
if debug {
println!("{}", translate!("cp-debug-skipped", "path" => path.quote()));
write_stdout_line(format_args!(
"{}",
translate!("cp-debug-skipped", "path" => path.quote())
))?;
}
Err(CpError::Skipped(false))
}
Expand Down Expand Up @@ -1897,7 +1915,7 @@ fn handle_existing_dest(

if options.update == UpdateMode::None {
if options.debug {
println!("skipped {}", dest.quote());
write_stdout_line(format_args!("skipped {}", dest.quote()))?;
}
return Err(CpError::Skipped(false));
}
Expand Down Expand Up @@ -2002,10 +2020,10 @@ fn delete_path(path: &Path, options: &Options) -> CopyResult<()> {
match fs::remove_file(path) {
Ok(()) => {
if options.verbose {
println!(
write_stdout_line(format_args!(
"{}",
translate!("cp-verbose-removed", "path" => path.quote())
);
))?;
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
Expand Down Expand Up @@ -2063,18 +2081,16 @@ fn print_verbose_output(
progress_bar: Option<&ProgressBar>,
source: &Path,
dest: &Path,
) {
) -> CopyResult<()> {
if let Some(pb) = progress_bar {
// Suspend (hide) the progress bar so the println won't overlap with the progress bar.
pb.suspend(|| {
print_paths(parents, source, dest);
});
pb.suspend(|| print_paths(parents, source, dest))
} else {
print_paths(parents, source, dest);
print_paths(parents, source, dest)
}
}

fn print_paths(parents: bool, source: &Path, dest: &Path) {
fn print_paths(parents: bool, source: &Path, dest: &Path) -> CopyResult<()> {
if parents {
// For example, if copying file `a/b/c` and its parents
// to directory `d/`, then print
Expand All @@ -2083,14 +2099,18 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) {
// a/b -> d/a/b
//
for (x, y) in aligned_ancestors(source, dest) {
println!(
write_stdout_line(format_args!(
"{}",
translate!("cp-verbose-created-directory", "source" => x.display(), "dest" => y.display())
);
translate!(
"cp-verbose-created-directory",
"source" => x.display(),
"dest" => y.display()
)
))?;
}
}

println!("{}", context_for(source, dest));
write_stdout_line(format_args!("{}", context_for(source, dest)))
}

/// Handles the copy mode for a file copy operation.
Expand Down Expand Up @@ -2187,7 +2207,7 @@ fn handle_copy_mode(
}
UpdateMode::None => {
if options.debug {
println!("skipped {}", dest.quote());
write_stdout_line(format_args!("skipped {}", dest.quote()))?;
}

return Ok(PerformedAction::Skipped);
Expand Down Expand Up @@ -2432,7 +2452,7 @@ fn copy_file(
fs::hard_link(new_source, dest)?;

if options.verbose {
print_verbose_output(options.parents, progress_bar, source, dest);
print_verbose_output(options.parents, progress_bar, source, dest)?;
}

return Ok(());
Expand Down Expand Up @@ -2495,7 +2515,7 @@ fn copy_file(
)?;

if options.verbose && performed_action != PerformedAction::Skipped {
print_verbose_output(options.parents, progress_bar, source, dest);
print_verbose_output(options.parents, progress_bar, source, dest)?;
}

// TODO: implement something similar to gnu's lchown
Expand Down Expand Up @@ -2652,7 +2672,7 @@ fn copy_helper(
)?;

if !options.attributes_only && options.debug {
show_debug(&copy_debug);
show_debug(&copy_debug)?;
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/uu/printf/src/printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {

let mut format_seen = false;
// Parse and process the format string
let mut stdout = uucore::TrackingWriter::new(stdout());
let mut args = FormatArguments::new(&values);
for item in parse_spec_and_escape(format) {
if let Ok(FormatItem::Spec(_)) = item {
format_seen = true;
}
match item?.write(stdout(), &mut args)? {
match item?.write(&mut stdout, &mut args)? {
ControlFlow::Continue(()) => {}
ControlFlow::Break(()) => return Ok(()),
}
Expand All @@ -70,7 +71,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {

while !args.is_exhausted() {
for item in parse_spec_and_escape(format) {
match item?.write(stdout(), &mut args)? {
match item?.write(&mut stdout, &mut args)? {
ControlFlow::Continue(()) => {}
ControlFlow::Break(()) => return Ok(()),
}
Expand Down
154 changes: 151 additions & 3 deletions src/uucore/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ use std::iter;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::str;
use std::str::Utf8Chunk;
use std::sync::{LazyLock, atomic::Ordering};
use std::sync::{
LazyLock,
atomic::{AtomicBool, Ordering},
};

/// Disables the custom signal handlers installed by Rust for stack-overflow handling. With those custom signal handlers processes ignore the first SIGBUS and SIGSEGV signal they receive.
/// See <https://github.com/rust-lang/rust/blob/8ac1525e091d3db28e67adcbbd6db1e1deaa37fb/src/libstd/sys/unix/stack_overflow.rs#L71-L92> for details.
Expand All @@ -165,6 +168,135 @@ pub fn disable_rust_signal_handlers() -> Result<(), Errno> {
Ok(())
}

/// Returns true if stdout is closed before running a utility.
pub fn stdout_is_closed() -> bool {
#[cfg(unix)]
{
let res = unsafe { nix::libc::fcntl(nix::libc::STDOUT_FILENO, nix::libc::F_GETFL) };
if res != -1 {
return false;
}
matches!(nix::errno::Errno::last(), nix::errno::Errno::EBADF)
}
#[cfg(not(unix))]
{
false
}
}

static STDOUT_WRITTEN: AtomicBool = AtomicBool::new(false);
static STDOUT_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
static STDOUT_WAS_CLOSED_SET: AtomicBool = AtomicBool::new(false);

/// Reset the stdout-written flag for the current process.
pub fn reset_stdout_written() {
STDOUT_WRITTEN.store(false, Ordering::Relaxed);
}

/// Record whether stdout was closed at process start.
pub fn set_stdout_was_closed(value: bool) {
STDOUT_WAS_CLOSED.store(value, Ordering::Relaxed);
STDOUT_WAS_CLOSED_SET.store(true, Ordering::Relaxed);
}

/// Returns true if stdout was closed at process start.
pub fn stdout_was_closed() -> bool {
STDOUT_WAS_CLOSED.load(Ordering::Relaxed)
}

/// Initialize stdout state if it wasn't set by early startup code.
pub fn init_stdout_state() {
if !STDOUT_WAS_CLOSED_SET.load(Ordering::Relaxed) {
set_stdout_was_closed(stdout_is_closed());
}
}

/// Mark that at least one non-empty write to stdout was attempted.
pub fn mark_stdout_written() {
STDOUT_WRITTEN.store(true, Ordering::Relaxed);
}

/// Returns true if any write to stdout was attempted.
pub fn stdout_was_written() -> bool {
STDOUT_WRITTEN.load(Ordering::Relaxed)
}

#[cfg(unix)]
mod early_stdout_state {
use super::set_stdout_was_closed;

extern "C" fn init() {
set_stdout_was_closed(super::stdout_is_closed());
}

#[used]
#[cfg_attr(target_os = "macos", unsafe(link_section = "__DATA,__mod_init_func"))]
#[cfg_attr(not(target_os = "macos"), unsafe(link_section = ".init_array"))]
static INIT: extern "C" fn() = init;
}

/// Record a pending stdout write and error if stdout was closed at startup.
pub fn check_stdout_write(len: usize) -> std::io::Result<()> {
if len == 0 {
return Ok(());
}
mark_stdout_written();
if stdout_was_closed() {
#[cfg(unix)]
{
return Err(std::io::Error::from_raw_os_error(
nix::errno::Errno::EBADF as i32,
));
}
#[cfg(not(unix))]
{
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"stdout was closed",
));
}
}
Ok(())
}

/// A writer wrapper that marks stdout as written when non-empty data is written.
pub struct TrackingWriter<W> {
inner: W,
}

impl<W> TrackingWriter<W> {
pub fn new(inner: W) -> Self {
Self { inner }
}
}

impl<W: std::io::Write> std::io::Write for TrackingWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
check_stdout_write(buf.len())?;
self.inner.write(buf)
}

fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}

/// Returns true if the error corresponds to writing to a closed stdout.
pub fn is_closed_stdout_error(err: &std::io::Error, stdout_was_closed: bool) -> bool {
if !stdout_was_closed {
return false;
}
#[cfg(unix)]
{
err.raw_os_error() == Some(nix::errno::Errno::EBADF as i32)
}
#[cfg(not(unix))]
{
let _ = err;
false
}
}

pub fn get_canonical_util_name(util_name: &str) -> &str {
// remove the "uu_" prefix
let util_name = &util_name[3..];
Expand Down Expand Up @@ -195,6 +327,12 @@ macro_rules! bin {
use std::io::Write;
use uucore::locale;

// Capture whether stdout was already closed before we run the utility.
// This must happen before any setup that might open files and reuse fd 1.
uucore::init_stdout_state();
uucore::reset_stdout_written();
let stdout_was_closed = uucore::stdout_was_closed();

// Preserve inherited SIGPIPE settings (e.g., from env --default-signal=PIPE)
uucore::panic::preserve_inherited_sigpipe();

Expand All @@ -213,10 +351,20 @@ macro_rules! bin {
});

// execute utility code
let code = $util::uumain(uucore::args_os());
let mut code = $util::uumain(uucore::args_os());
// (defensively) flush stdout for utility prior to exit; see <https://github.com/rust-lang/rust/issues/23818>
if let Err(e) = std::io::stdout().flush() {
eprintln!("Error flushing stdout: {e}");
// Treat write errors as a failure, but ignore BrokenPipe to avoid
// breaking utilities that intentionally silence it (e.g., seq).
let stdout_was_written = uucore::stdout_was_written();
let ignore_closed_stdout =
uucore::is_closed_stdout_error(&e, stdout_was_closed) && !stdout_was_written;
if e.kind() != std::io::ErrorKind::BrokenPipe && !ignore_closed_stdout {
eprintln!("Error flushing stdout: {e}");
if code == 0 {
code = 1;
}
}
}

std::process::exit(code);
Expand Down
Loading
Loading