diff --git a/Cargo.lock b/Cargo.lock index f1f3373..9c57e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "libc", "regex", "regex-lite", + "rustix", "serde", "serde_json", "tap", diff --git a/Cargo.toml b/Cargo.toml index 41fe13c..3223b6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ itertools = "0.14.0" libc = { version = "0.2.180", features = ["extra_traits"] } regex = { version = "1.12.3", optional = true } regex-lite = { version = "0.1.9", optional = true } +rustix = { version = "1.1.4", features = ["event", "fs"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tap = "1.0.1" diff --git a/src/daemon_io.rs b/src/daemon_io.rs new file mode 100644 index 0000000..f080ca8 --- /dev/null +++ b/src/daemon_io.rs @@ -0,0 +1,106 @@ +use std::{ + mem::ManuallyDrop, + os::{ + fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd}, + unix::prelude::RawFd, + }, +}; + +use rustix::{ + fs::{OFlags, fcntl_getfl, fcntl_setfl}, + io::Errno, +}; + +use crate::prelude::*; + +/// An [`OwnedFd`] that captures file status flags on init and restores them on: +/// +/// - [`IntoRawFd`] +/// - [`drop()`] +#[derive(Debug)] +pub struct OwnedFdWithFlags { + fd: OwnedFd, + oflags: OFlags, +} + +impl OwnedFdWithFlags { + pub fn new(fd: OwnedFd) -> Result { + let oflags = fcntl_getfl(&fd)?; + Ok(Self { fd, oflags }) + } + + /// If, for some ungodly reason, `fcntl(F_GETFL)` fails, then empty flags are used. + /// Empty flags should not be restored on close. + /// + /// I'm pretty sure this should never happen if the program is IO-safe. + pub fn new_with_fallback(fd: OwnedFd) -> Self { + let oflags = Self::get_flags_or_log(fd.as_fd()); + Self { fd, oflags } + } +} + +impl Drop for OwnedFdWithFlags { + fn drop(&mut self) { + self.restore_flags_or_log(); + } +} + +/// Private helpers. +impl OwnedFdWithFlags { + fn restore_flags_or_log(&self) { + // As far as we can tell there's no such thing as a file with entirely empty + // flags (at minimum, it needs read/write mode, right?). + // So empty `oflags` is our sentinel that `fcntl(F_GETFL)` somehow failed. + // Empty oflags is our sentinel that + if self.oflags.is_empty() { + return; + } + + if let Err(e) = fcntl_setfl(&self.fd, self.oflags) { + error!( + "fcntl(F_SETFL) to restore flags {:?} on fd {:?} failed: {e}\nIO safety violation?", + self.oflags, self.fd, + ); + }; + } + + fn get_flags_or_log(fd: BorrowedFd) -> OFlags { + fcntl_getfl(fd) + .tap_err(|e| error!("fcntl(F_GETFL) failed on fd {fd:?}: {e}\nIO-safety violation?")) + .unwrap_or(OFlags::empty()) + } +} + +impl AsRawFd for OwnedFdWithFlags { + fn as_raw_fd(&self) -> RawFd { + AsRawFd::as_raw_fd(&self.fd) + } +} + +impl IntoRawFd for OwnedFdWithFlags { + fn into_raw_fd(self) -> RawFd { + self.restore_flags_or_log(); + ManuallyDrop::new(self).fd.as_raw_fd() + } +} + +impl FromRawFd for OwnedFdWithFlags { + unsafe fn from_raw_fd(fd: RawFd) -> Self { + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + let oflags = Self::get_flags_or_log(fd.as_fd()); + Self { fd, oflags } + } +} + +impl AsFd for OwnedFdWithFlags { + fn as_fd(&self) -> BorrowedFd<'_> { + AsFd::as_fd(&self.fd) + } +} + +impl From for OwnedFdWithFlags { + fn from(fd: OwnedFd) -> Self { + let oflags = Self::get_flags_or_log(fd.as_fd()); + Self { fd, oflags } + } +} diff --git a/src/lib.rs b/src/lib.rs index 58d98a1..7533c04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ pub(crate) mod prelude { #[cfg(unix)] pub use fs_err::os::unix::fs::{FileExt, OpenOptionsExt}; - pub use tap::{Pipe, Tap}; + pub use tap::{Pipe, Tap, TapFallible}; pub use tracing::{Level, debug, error, info, trace, warn}; } @@ -44,6 +44,8 @@ pub mod args; pub use args::{AppendCmd, Args}; mod color; pub use color::{_CLI_ENABLE_COLOR, SHOULD_COLOR}; +mod daemon_io; +pub use daemon_io::OwnedFdWithFlags; pub mod line; mod nixcmd; pub use line::Line;