//! Add [`pipe_out()`][PipeOut::pipe_out] to any [`Iterator`] of [`std::fmt::Display`] use std::{ io::Write, marker::Sized, sync::atomic::{AtomicBool, Ordering::Relaxed}, }; use crate::pipe_out_recovered_iterator::PipeOutRecoveredIterator; /// Allows [`WriteLineResult::PreviousError`] without custom [`std::io::Error`] #[must_use = "this may be an error variant (`BrokenPipe`, `OtherError`, \ `PreviousError`), which should be handled"] pub enum WriteLineResult { /// [std::io::ErrorKind::BrokenPipe] state BrokenPipe(std::io::Error), /// Other error state OtherError(std::io::Error), /// Other error occurred in last [PipeOut::pipe_out] PreviousError, /// `STDOUT` is already locked StdOutLock, /// No error occurred Ok, } impl WriteLineResult { /// Compare variants of [`WriteLineResult`], ignoring contained [`std::io::Error`] /// /// Per it is illogical to /// compare [`std::io::Error`]. For testing reasons, this function compares /// the variants of two [`WriteLineResult`] so some degree of [`PartialEq`] /// can be emulated #[must_use] pub fn same_variant_as(&self, variant: &Self) -> bool { self.variant_to_usize() == variant.variant_to_usize() } fn variant_to_usize(&self) -> usize { match self { Self::BrokenPipe(_) => 1, Self::OtherError(_) => 2, Self::PreviousError => 3, Self::StdOutLock => 4, Self::Ok => 5, } } } /// Convert from [`std::io::Error`], catch [`std::io::ErrorKind::BrokenPipe`] impl From for WriteLineResult { fn from(e: std::io::Error) -> Self { if e.kind() == std::io::ErrorKind::BrokenPipe { Self::BrokenPipe(e) } else { Self::OtherError(e) } } } /// Convert [`WriteLineResult`] to [`std::io::Error`] /// /// # Panic /// /// Will panic of non [`std::io::Error`] state: /// - [`WriteLineResult::Ok`] /// - [`WriteLineResult::PreviousError`] impl From for std::io::Error { fn from(e: WriteLineResult) -> Self { match e { WriteLineResult::OtherError(e) | WriteLineResult::BrokenPipe(e) => { e } _ => panic!( "1656622268 - Can't unwrap io::Error from non-std::io::Error \ variant" ), } } } impl From for Result<(), std::io::Error> { fn from(value: WriteLineResult) -> Self { match value { WriteLineResult::Ok => Ok(()), WriteLineResult::OtherError(e) | WriteLineResult::BrokenPipe(e) => { Err(e) } WriteLineResult::PreviousError => Err(std::io::Error::new( std::io::ErrorKind::Other, "1669225745 - Other error occurred in last [Self::pipe_out]", )), WriteLineResult::StdOutLock => Err(std::io::Error::new( std::io::ErrorKind::Other, "1669225823 - STDOUT is already locked", )), } } } /// Add [`pipe_out()`][PipeOut::pipe_out] to any [`Iterator`] of [`std::fmt::Display`] pub trait PipeOut: Iterator { /// Pipe to `STDOUT` the elements of the parent iterator, return /// [`PipeOutRecoveredIterator`] /// /// # Warning /// /// At this time, there is no reliable way to ensure when or if the pipe was /// broken. Due to some unknown reason (probably related to buffering) the /// output is allowed to continue past the broken pipe. /// /// For the below test: /// /// ```sh /// cargo run --example="example_handles_broken_pipe" | cargo run --example="break_pipe" /// ``` /// /// The results should ALWAYS be: /// /// ```text /// Launched /// Test1 /// Test2 /// Test3 - Recovered /// Recovery Done /// ``` /// /// However, most times the test returns other values like: /// /// ```text /// Launched /// Finished /// Test1 /// Test2 /// ``` /// /// or /// /// ```text /// Launched /// Test1 /// Finished /// Test2 /// ``` /// /// There appears to be no way to fix this inconsistent behavior. If you /// know of a fix, please submit a pull request or e-mail /// /// /// # Errors /// /// If something fails to pipe, a [`PipeOutRecoveredIterator`] is returned /// /// # Example /// /// ```no_run #[doc = include_str!("examples/pipe_out.rs")] /// ``` fn pipe_out(mut self) -> Result<(), PipeOutRecoveredIterator> where Self: Sized, { if check_error_state() || set_lock_state().is_err() { return Err(PipeOutRecoveredIterator { iter: self, recovered_datum: None, result: WriteLineResult::StdOutLock, }); } let stdout = std::io::stdout(); let mut lock = stdout.lock(); for datum in self.by_ref() { if check_error_state() { release_lock_state(); return Err(PipeOutRecoveredIterator { iter: self, recovered_datum: Some(datum), result: WriteLineResult::PreviousError, }); } if let Err(error) = lock.write_all(format!("{datum}\n").as_bytes()) { set_error_state(); release_lock_state(); return Err(PipeOutRecoveredIterator { iter: self, recovered_datum: Some(datum), result: error.into(), }); } } release_lock_state(); Ok(()) } } impl> PipeOut for I {} static mut ERROR: AtomicBool = AtomicBool::new(false); fn check_error_state() -> bool { unsafe { ERROR.load(Relaxed) } } fn set_error_state() { unsafe { ERROR.store(true, Relaxed) } } static mut LOCKED: AtomicBool = AtomicBool::new(false); fn set_lock_state() -> Result { // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicBool.html#method.compare_exchange unsafe { LOCKED.compare_exchange(false, true, Relaxed, Relaxed) } } fn release_lock_state() { unsafe { LOCKED.store(false, Relaxed) } } #[cfg(test)] mod test { use super::*; fn check_lock_state() -> bool { unsafe { LOCKED.load(Relaxed) } } #[test] fn test1656794821() { assert!(!check_error_state()); // set_lock_state(); // Make sure correct variable assert!(!check_error_state()); set_error_state(); assert!(check_error_state()); assert!(check_error_state()); } // Commented code for single-test runs only as state will maintain between // tests #[test] fn test1656795696() { assert!(!check_lock_state()); // set_error_state(); // Make sure correct variable assert!(!check_lock_state()); set_lock_state().unwrap(); assert!(check_lock_state()); assert!(check_lock_state()); release_lock_state(); assert!(!check_lock_state()); assert!(!check_lock_state()); } // Commented code for single-test runs only as state will maintain between // tests }