Skip to content

Commit c768673

Browse files
committed
refactor(tac): replace nix with uucore signals for stdin closed detection
Remove dependency on nix crate and use uucore's new signals feature to detect if stdin was closed before Rust reopens it as /dev/null. This centralizes stdio state handling and improves consistency across utilities. Add libc dependency for signal functions. Update error handling to set exit code properly on stdin closure.
1 parent be6ed78 commit c768673

File tree

5 files changed

+122
-42
lines changed

5 files changed

+122
-42
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/tac/Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ memchr = { workspace = true }
2424
memmap2 = { workspace = true }
2525
regex = { workspace = true }
2626
clap = { workspace = true }
27-
uucore = { workspace = true }
27+
libc = { workspace = true }
28+
uucore = { workspace = true, features = ["signals"] }
2829
thiserror = { workspace = true }
2930
fluent = { workspace = true }
3031

31-
[target.'cfg(unix)'.dependencies]
32-
nix = { workspace = true, features = ["fs"] }
33-
3432
[[bin]]
3533
name = "tac"
3634
path = "src/main.rs"

src/uu/tac/src/tac.rs

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,23 @@
44
// file that was distributed with this source code.
55

66
// spell-checker:ignore (ToDO) sbytes slen dlen memmem memmap Mmap mmap SIGBUS
7+
#[cfg(unix)]
8+
uucore::init_stdio_state_capture!();
9+
710
mod error;
811

912
use clap::{Arg, ArgAction, Command};
1013
use memchr::memmem;
1114
use memmap2::Mmap;
12-
#[cfg(unix)]
13-
use nix::{
14-
errno::Errno,
15-
fcntl::{FcntlArg, fcntl},
16-
};
1715
use std::ffi::OsString;
1816
use std::io::{BufWriter, Read, Write, stdin, stdout};
19-
#[cfg(unix)]
20-
use std::os::fd::{AsRawFd, BorrowedFd};
2117
use std::{
2218
fs::{File, read},
2319
path::Path,
2420
};
25-
use uucore::error::UError;
26-
use uucore::error::UResult;
21+
#[cfg(unix)]
22+
use uucore::error::set_exit_code;
23+
use uucore::error::{UError, UResult};
2724
use uucore::{format_usage, show};
2825

2926
use crate::error::TacError;
@@ -244,19 +241,24 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR
244241
let buf;
245242

246243
let data: &[u8] = if filename == "-" {
247-
if let Err(e) = ensure_stdin_open() {
248-
let e: Box<dyn UError> = TacError::ReadError(OsString::from("stdin"), e).into();
244+
#[cfg(unix)]
245+
if uucore::signals::stdin_was_closed() {
246+
let e: Box<dyn UError> = TacError::ReadError(
247+
OsString::from("-"),
248+
std::io::Error::from_raw_os_error(libc::EBADF),
249+
)
250+
.into();
249251
show!(e);
252+
set_exit_code(1);
250253
continue;
251254
}
252-
253255
if let Some(mmap1) = try_mmap_stdin() {
254256
mmap = mmap1;
255257
&mmap
256258
} else {
257259
let mut buf1 = Vec::new();
258260
if let Err(e) = stdin().read_to_end(&mut buf1) {
259-
let e: Box<dyn UError> = TacError::ReadError(OsString::from("stdin"), e).into();
261+
let e: Box<dyn UError> = TacError::ReadError(OsString::from("-"), e).into();
260262
show!(e);
261263
continue;
262264
}
@@ -336,27 +338,3 @@ fn try_mmap_path(path: &Path) -> Option<Mmap> {
336338

337339
Some(mmap)
338340
}
339-
340-
#[cfg(unix)]
341-
fn stdin_closed() -> std::io::Result<bool> {
342-
let stdin_fd = unsafe { BorrowedFd::borrow_raw(stdin().as_raw_fd()) };
343-
match fcntl(stdin_fd, FcntlArg::F_GETFL) {
344-
Ok(_) => Ok(false),
345-
Err(_) => Ok(true),
346-
}
347-
}
348-
349-
fn ensure_stdin_open() -> std::io::Result<()> {
350-
#[cfg(unix)]
351-
{
352-
if stdin_closed()? {
353-
return Err(nix_error_to_io(Errno::EBADF));
354-
}
355-
}
356-
Ok(())
357-
}
358-
359-
#[cfg(unix)]
360-
fn nix_error_to_io(errno: Errno) -> std::io::Error {
361-
std::io::Error::from_raw_os_error(errno as i32)
362-
}

src/uucore/src/lib/features/signals.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,90 @@ pub fn ignore_interrupts() -> Result<(), Errno> {
426426
unsafe { signal(SIGINT, SigIgn) }.map(|_| ())
427427
}
428428

429+
// Detect closed stdin/stdout/stderr before Rust reopens them as /dev/null (see issue #2873)
430+
#[cfg(unix)]
431+
use std::sync::atomic::{AtomicBool, Ordering};
432+
433+
#[cfg(unix)]
434+
static STDIN_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
435+
#[cfg(unix)]
436+
static STDOUT_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
437+
#[cfg(unix)]
438+
static STDERR_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
439+
440+
#[cfg(unix)]
441+
#[allow(clippy::missing_safety_doc)]
442+
pub unsafe extern "C" fn capture_stdio_state() {
443+
use nix::libc;
444+
445+
unsafe {
446+
STDIN_WAS_CLOSED.store(
447+
libc::fcntl(libc::STDIN_FILENO, libc::F_GETFD) == -1,
448+
Ordering::Relaxed,
449+
);
450+
STDOUT_WAS_CLOSED.store(
451+
libc::fcntl(libc::STDOUT_FILENO, libc::F_GETFD) == -1,
452+
Ordering::Relaxed,
453+
);
454+
STDERR_WAS_CLOSED.store(
455+
libc::fcntl(libc::STDERR_FILENO, libc::F_GETFD) == -1,
456+
Ordering::Relaxed,
457+
);
458+
}
459+
}
460+
461+
#[macro_export]
462+
#[cfg(unix)]
463+
macro_rules! init_stdio_state_capture {
464+
() => {
465+
#[cfg(not(target_os = "macos"))]
466+
#[used]
467+
#[unsafe(link_section = ".init_array")]
468+
static CAPTURE_STDIO_STATE: unsafe extern "C" fn() = $crate::signals::capture_stdio_state;
469+
470+
#[cfg(target_os = "macos")]
471+
#[used]
472+
#[unsafe(link_section = "__DATA,__mod_init_func")]
473+
static CAPTURE_STDIO_STATE: unsafe extern "C" fn() = $crate::signals::capture_stdio_state;
474+
};
475+
}
476+
477+
#[macro_export]
478+
#[cfg(not(unix))]
479+
macro_rules! init_stdio_state_capture {
480+
() => {};
481+
}
482+
483+
#[cfg(unix)]
484+
pub fn stdin_was_closed() -> bool {
485+
STDIN_WAS_CLOSED.load(Ordering::Relaxed)
486+
}
487+
488+
#[cfg(not(unix))]
489+
pub const fn stdin_was_closed() -> bool {
490+
false
491+
}
492+
493+
#[cfg(unix)]
494+
pub fn stdout_was_closed() -> bool {
495+
STDOUT_WAS_CLOSED.load(Ordering::Relaxed)
496+
}
497+
498+
#[cfg(not(unix))]
499+
pub const fn stdout_was_closed() -> bool {
500+
false
501+
}
502+
503+
#[cfg(unix)]
504+
pub fn stderr_was_closed() -> bool {
505+
STDERR_WAS_CLOSED.load(Ordering::Relaxed)
506+
}
507+
508+
#[cfg(not(unix))]
509+
pub const fn stderr_was_closed() -> bool {
510+
false
511+
}
512+
429513
#[test]
430514
fn signal_by_value() {
431515
assert_eq!(signal_by_name_or_value("0"), Some(0));

tests/by-util/test_tac.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,23 @@ fn test_stdin_dev_null_rdwr_is_supported() {
377377
.no_stderr()
378378
.stdout_is("");
379379
}
380+
381+
#[test]
382+
#[cfg(unix)]
383+
fn test_stdin_closed_with_dash_args_fails() {
384+
use std::os::unix::process::CommandExt;
385+
386+
let ts = TestScenario::new(util_name!());
387+
let mut cmd = std::process::Command::new(&ts.bin_path);
388+
cmd.arg("tac").args(["-", "-"]);
389+
390+
unsafe {
391+
cmd.pre_exec(|| {
392+
libc::close(libc::STDIN_FILENO);
393+
Ok(())
394+
});
395+
}
396+
397+
let output = cmd.output().unwrap();
398+
assert_eq!(output.status.code(), Some(1));
399+
}

0 commit comments

Comments
 (0)