diff --git a/src/lib.rs b/src/lib.rs index 09f86c5..0234d74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,7 @@ impl StderrSilencer { #[cfg(unix)] unsafe { // Duplicate current stderr (fd 2) - let old_fd = dup(2); + let old_fd = unix_fd::dup(unix_fd::STDERR_FILENO); if old_fd < 0 { return Self { active: false, @@ -103,10 +103,10 @@ impl StderrSilencer { } // Open /dev/null for writing let devnull_cstr = std::ffi::CString::new("/dev/null").unwrap(); - let dn = open(devnull_cstr.as_ptr(), O_WRONLY); + let dn = unix_fd::open(devnull_cstr.as_ptr(), unix_fd::O_WRONLY); if dn < 0 { // failed to open devnull; restore and bail - close(old_fd); + unix_fd::close(old_fd); return Self { active: false, old_stderr_fd: -1, @@ -114,9 +114,9 @@ impl StderrSilencer { }; } // Redirect fd 2 to devnull - if dup2(dn, 2) < 0 { - close(dn); - close(old_fd); + if unix_fd::dup2(dn, unix_fd::STDERR_FILENO) < 0 { + unix_fd::close(dn); + unix_fd::close(old_fd); return Self { active: false, old_stderr_fd: -1, @@ -144,9 +144,9 @@ impl Drop for StderrSilencer { #[cfg(unix)] unsafe { // Restore old stderr and close devnull and old copies - let _ = dup2(self.old_stderr_fd, 2); - let _ = close(self.devnull_fd); - let _ = close(self.old_stderr_fd); + let _ = unix_fd::dup2(self.old_stderr_fd, unix_fd::STDERR_FILENO); + let _ = unix_fd::close(self.devnull_fd); + let _ = unix_fd::close(self.old_stderr_fd); } self.active = false; } @@ -242,7 +242,18 @@ use std::path::{Path, PathBuf}; use std::process::Command; #[cfg(unix)] -use libc::{O_WRONLY, close, dup, dup2, open}; +mod unix_fd { + pub use libc::O_WRONLY; + pub const STDERR_FILENO: i32 = 2; // libc::STDERR_FILENO isn't always available on all targets + #[inline] + pub unsafe fn dup(fd: i32) -> i32 { libc::dup(fd) } + #[inline] + pub unsafe fn dup2(fd: i32, fd2: i32) -> i32 { libc::dup2(fd, fd2) } + #[inline] + pub unsafe fn open(path: *const libc::c_char, flags: i32) -> i32 { libc::open(path, flags) } + #[inline] + pub unsafe fn close(fd: i32) -> i32 { libc::close(fd) } +} /// Re-export backend module (GPU/CPU selection and transcription). pub mod backend; diff --git a/tests/with_suppressed_stderr.rs b/tests/with_suppressed_stderr.rs new file mode 100644 index 0000000..9cf00e5 --- /dev/null +++ b/tests/with_suppressed_stderr.rs @@ -0,0 +1,58 @@ +// Unix-only tests for with_suppressed_stderr restoring file descriptors +// Skip on Windows and non-Unix targets. + +#![cfg(unix)] + +use std::panic::{catch_unwind, AssertUnwindSafe}; + +fn stat_of_fd(fd: i32) -> (u64, u64) { + unsafe { + let mut st: libc::stat = std::mem::zeroed(); + let r = libc::fstat(fd, &mut st as *mut libc::stat); + assert_eq!(r, 0, "fstat failed on fd {fd}"); + (st.st_dev as u64, st.st_ino as u64) + } +} + +fn stat_of_path(path: &str) -> (u64, u64) { + use std::ffi::CString; + unsafe { + let c = CString::new(path).unwrap(); + let fd = libc::open(c.as_ptr(), libc::O_RDONLY); + assert!(fd >= 0, "failed to open {path}"); + let s = stat_of_fd(fd); + let _ = libc::close(fd); + s + } +} + +#[test] +fn stderr_is_redirected_and_restored() { + let before = stat_of_fd(2); + let devnull = stat_of_path("/dev/null"); + + // During the call, fd 2 should be /dev/null; after, restored to before + polyscribe::with_suppressed_stderr(|| { + let inside = stat_of_fd(2); + assert_eq!(inside, devnull, "stderr should point to /dev/null during suppression"); + // This write should be suppressed + eprintln!("this should be suppressed"); + }); + + let after = stat_of_fd(2); + assert_eq!(after, before, "stderr should be restored after suppression"); +} + +#[test] +fn stderr_is_restored_even_if_closure_panics() { + let before = stat_of_fd(2); + let res = catch_unwind(AssertUnwindSafe(|| { + polyscribe::with_suppressed_stderr(|| { + // Trigger a deliberate panic inside the closure + panic!("boom inside with_suppressed_stderr"); + }); + })); + assert!(res.is_err(), "expected panic to propagate"); + let after = stat_of_fd(2); + assert_eq!(after, before, "stderr should be restored after panic"); +}