From ebb092c88820d2217860cee1c518e6a417e3f073 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Fri, 19 Jan 2024 09:04:45 -0600 Subject: [PATCH] [Stacked PR] Remove warn_later from output (#760) * Refactor background timer logic The background timer prints dots to the UI while the buildpack performs some expensive operation. This requires we use threading and Rust really wants to be sure that we're not going to have data races and that our data is secure. The original version of the code relied on wrapping our IO in an Arc> so that we don't "lose" it when we pass it to the background thread. This worked, but was overly complicated based on my limited understanding of working with lifetimes and threads. This version instead relies on the ability to retrieve a struct when we join the thread to get the IO object back out. This reduces complexity that the BuildLog interface needs to know about. # This is the commit message #2: Inject tick state The tick values for the printer was hardcoded. Passing in the values allows us to remove the `style` module dependency and makes the tests simpler. # This is the commit message #3: Rename modules and add docs The prior name `print_dots` isn't technically valid anymore as it could be printing something else. I also moved the guard struct into it's own module to make it a bit safer (ensure that Option is always Some at creation time). # This is the commit message #4: Clarify intention that option is Some in code The debug_assert adds a stronger signal that the option must be Some when it is being constructed. * [Stacked PR] Remove warn_later from output The `warn_later` functionality relies on global state in a way that we're not thrilled about putting into everyone's build packs by default. This removes the feature, plain and simple. --- libherokubuildpack/src/output/background.rs | 213 +++++++++++ .../src/output/background_timer.rs | 157 -------- libherokubuildpack/src/output/build_log.rs | 117 ++---- libherokubuildpack/src/output/interface.rs | 1 - libherokubuildpack/src/output/mod.rs | 3 +- libherokubuildpack/src/output/section_log.rs | 5 - libherokubuildpack/src/output/warn_later.rs | 347 ------------------ 7 files changed, 244 insertions(+), 599 deletions(-) create mode 100644 libherokubuildpack/src/output/background.rs delete mode 100644 libherokubuildpack/src/output/background_timer.rs delete mode 100644 libherokubuildpack/src/output/warn_later.rs diff --git a/libherokubuildpack/src/output/background.rs b/libherokubuildpack/src/output/background.rs new file mode 100644 index 00000000..f3769cb5 --- /dev/null +++ b/libherokubuildpack/src/output/background.rs @@ -0,0 +1,213 @@ +use std::fmt::Debug; +use std::io::Write; +use std::sync::mpsc::channel; +use std::time::Duration; + +/// This module is responsible for the logic involved in the printing to output while +/// other work is being performed. Such as printing dots while a download is being performed + +/// Print dots to the given buffer at the given interval +/// +/// Returns a struct that allows for manually stopping the timer or will automatically stop +/// the timer if the guard is dropped. This functionality allows for errors that trigger +/// an exit of the function to not accidentally have a timer printing in the background +/// forever. +#[must_use] +pub(crate) fn print_interval( + mut buffer: W, + interval: Duration, + start: String, + tick: String, + end: String, +) -> state::PrintGuard +where + W: Write + Send + Debug + 'static, +{ + let (sender, receiver) = channel::<()>(); + + let join_handle = std::thread::spawn(move || { + write!(buffer, "{start}").expect("Internal error"); + buffer.flush().expect("Internal error"); + + loop { + write!(buffer, "{tick}").expect("Internal error"); + buffer.flush().expect("Internal error"); + + if receiver.recv_timeout(interval).is_ok() { + break; + } + } + + write!(buffer, "{end}").expect("Internal error"); + buffer.flush().expect("Internal error"); + + buffer + }); + + state::PrintGuard::new(join_handle, sender) +} + +pub(crate) mod state { + use std::fmt::Debug; + use std::io::Write; + use std::sync::mpsc::Sender; + use std::thread::JoinHandle; + + /// Holds the reference to the background printer + /// + /// Ensures that the dot printer is stopped in the event of an error. By signaling + /// it and joining when this struct is dropped. + /// + /// Gives access to the original io/buffer struct passed to the background writer + /// when the thread is manually stopped. + /// + /// # Panics + /// + /// Updates to this code need to take care to not introduce a panic. See + /// documentation in `PrintGuard::stop` below for more details. + #[derive(Debug)] + pub(crate) struct PrintGuard + where + W: Write + Debug, + { + /// Holds the handle to the thread printing dots + /// + /// Structs that implement `Drop` must ensure a valid internal state at + /// all times due to E0509. The handle is wrapped in an option to allow the + /// inner value to be removed while preserving internal state. + join_handle: Option>, + + /// Holds the signaling method to tell the background printer + /// to stop emitting. + stop_signal: Sender<()>, + } + + impl Drop for PrintGuard + where + W: Write + Debug, + { + fn drop(&mut self) { + // A note on correctness. It might seem that it's enough to signal the thread to + // stop, that we don't also have to join and wait for it to finish, but that's not + // the case. The printer can still emit a value after it's told to stop. + // + // When that happens the output can appear in the middle of another output, such + // as an error message if a global writer is being used such as stdout. + // As a result we have to signal AND ensure the thread is stoped before + // continuing. + if let Some(join_handle) = self.join_handle.take() { + let _ = self.stop_signal.send(()); + let _ = join_handle.join(); + } + } + } + + impl PrintGuard + where + W: Write + Debug, + { + /// Preserve internal state by ensuring the `Option` is always populated + pub(crate) fn new(join_handle: JoinHandle, sender: Sender<()>) -> Self { + let guard = PrintGuard { + join_handle: Some(join_handle), + stop_signal: sender, + }; + debug_assert!(guard.join_handle.is_some()); + + guard + } + + /// The only thing a consumer can do is stop the dots and receive + /// the original buffer. + /// + /// # Panics + /// + /// This code can panic if it encounters an unexpedcted internal state. + /// If that happens it means there is an internal bug in this logic. + /// To avoid a panic, developers modifying this file must obey the following + /// rules: + /// + /// - Always consume the struct when accessing the Option value outside of Drop + /// - Never construct this struct with a `None` option value. + /// + /// This `Option` wrapping is needed to support a implementing Drop to + /// ensure the printing is stopped. When a struct implements drop it cannot + /// remove it's internal state due to E0509: + /// + /// + /// + /// The workaround is to never allow invalid internal state by replacing the + /// inner value with a `None` when removing it. We don't want to expose this + /// implementation detail to the user, so instead we accept the panic, ensure + /// the code is exercised under test, and exhaustively document why this panic + /// exists and how developers working with this code can maintain safety. + #[allow(clippy::panic_in_result_fn)] + pub(crate) fn stop(mut self) -> std::thread::Result { + // Ignore if the channel is closed, likely means the thread died which + // we want in this case. + match self.join_handle.take() { + Some(join_handle) => { + let _ = self.stop_signal.send(()); + join_handle.join() + } + None => panic!("Internal error: Dot print internal state should never be None"), + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::{File, OpenOptions}; + use tempfile::NamedTempFile; + + #[test] + fn does_stop_does_not_panic() { + let mut buffer: Vec = vec![]; + write!(buffer, "before").unwrap(); + + let dot = print_interval( + buffer, + Duration::from_millis(1), + String::from(" ."), + String::from("."), + String::from(". "), + ); + let mut writer = dot.stop().unwrap(); + + write!(writer, "after").unwrap(); + writer.flush().unwrap(); + + assert_eq!("before ... after", String::from_utf8_lossy(&writer)); + } + + #[test] + fn test_drop_stops_timer() { + let tempfile = NamedTempFile::new().unwrap(); + let mut log = File::create(tempfile.path()).unwrap(); + write!(log, "before").unwrap(); + + let dot = print_interval( + log, + Duration::from_millis(1), + String::from(" ."), + String::from("."), + String::from(". "), + ); + drop(dot); + + let mut log = OpenOptions::new() + .append(true) + .write(true) + .open(tempfile.path()) + .unwrap(); + write!(log, "after").unwrap(); + log.flush().unwrap(); + + assert_eq!( + String::from("before ... after"), + std::fs::read_to_string(tempfile.path()).unwrap() + ); + } +} diff --git a/libherokubuildpack/src/output/background_timer.rs b/libherokubuildpack/src/output/background_timer.rs deleted file mode 100644 index f3c7f187..00000000 --- a/libherokubuildpack/src/output/background_timer.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::io::Write; -use std::sync::mpsc::Sender; -use std::sync::{mpsc, Arc, Mutex}; -use std::thread::JoinHandle; -use std::time::{Duration, Instant}; - -/// This module is responsible for the logic involved in the printing to output while -/// other work is being performed. - -/// Prints a start, then a tick every second, and an end to the given `Write` value. -/// -/// Returns a struct that allows for manually stopping the timer or will automatically stop -/// the timer if the guard is dropped. This functionality allows for errors that trigger -/// an exit of the function to not accidentally have a timer printing in the background -/// forever. -pub(crate) fn start_timer( - arc_io: &Arc>, - tick_duration: Duration, - start: impl AsRef, - tick: impl AsRef, - end: impl AsRef, -) -> StopJoinGuard -where - // The 'static lifetime means as long as something holds a reference to it, nothing it references - // will go away. - // - // From https://users.rust-lang.org/t/why-does-thread-spawn-need-static-lifetime-for-generic-bounds/4541 - // - // [lifetimes] refer to the minimum possible lifetime of any borrowed references that the object contains. - T: Write + Send + Sync + 'static, -{ - let instant = Instant::now(); - let (sender, receiver) = mpsc::channel::<()>(); - let start = start.as_ref().to_string(); - let tick = tick.as_ref().to_string(); - let end = end.as_ref().to_string(); - - let arc_io = arc_io.clone(); - let handle = std::thread::spawn(move || { - let mut io = arc_io.lock().expect("Logging mutex poisoned"); - write!(&mut io, "{start}").expect("Internal error"); - io.flush().expect("Internal error"); - loop { - write!(&mut io, "{tick}").expect("Internal error"); - io.flush().expect("Internal error"); - - if receiver.recv_timeout(tick_duration).is_ok() { - write!(&mut io, "{end}").expect("Internal error"); - io.flush().expect("Internal error"); - break; - } - } - }); - - StopJoinGuard { - inner: Some(StopTimer { - handle: Some(handle), - sender: Some(sender), - instant, - }), - } -} - -/// Responsible for stopping a running timer thread -#[derive(Debug)] -pub(crate) struct StopTimer { - instant: Instant, - handle: Option>, - sender: Option>, -} - -impl StopTimer { - pub(crate) fn elapsed(&self) -> Duration { - self.instant.elapsed() - } -} - -pub(crate) trait StopJoin: std::fmt::Debug { - fn stop_join(self) -> Self; -} - -impl StopJoin for StopTimer { - fn stop_join(mut self) -> Self { - if let Some(inner) = self.sender.take() { - inner.send(()).expect("Internal error"); - } - - if let Some(inner) = self.handle.take() { - inner.join().expect("Internal error"); - } - - self - } -} - -// Guarantees that stop is called on the inner -#[derive(Debug)] -pub(crate) struct StopJoinGuard { - inner: Option, -} - -impl StopJoinGuard { - /// Since this consumes self and `stop_join` consumes - /// the inner, the option will never be empty unless - /// it was created with a None inner. - /// - /// Since inner is private we guarantee it's always Some - /// until this struct is consumed. - pub(crate) fn stop(mut self) -> T { - self.inner - .take() - .map(StopJoin::stop_join) - .expect("Internal error: Should never panic, codepath tested") - } -} - -impl Drop for StopJoinGuard { - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { - inner.stop_join(); - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::util::ReadYourWrite; - use libcnb_test::assert_contains; - use std::thread::sleep; - - #[test] - fn does_stop_does_not_panic() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let done = start_timer(&writer.arc_io(), Duration::from_millis(1), " .", ".", ". "); - - let _ = done.stop(); - - assert_contains!(String::from_utf8_lossy(&reader.lock().unwrap()), " ... "); - } - - #[test] - fn test_drop_stops_timer() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let done = start_timer(&writer.arc_io(), Duration::from_millis(1), " .", ".", ". "); - - drop(done); - sleep(Duration::from_millis(2)); - - let before = String::from_utf8_lossy(&reader.lock().unwrap()).to_string(); - sleep(Duration::from_millis(100)); - let after = String::from_utf8_lossy(&reader.lock().unwrap()).to_string(); - assert_eq!(before, after); - } -} diff --git a/libherokubuildpack/src/output/build_log.rs b/libherokubuildpack/src/output/build_log.rs index c7564aa1..966c2de4 100644 --- a/libherokubuildpack/src/output/build_log.rs +++ b/libherokubuildpack/src/output/build_log.rs @@ -1,4 +1,4 @@ -use crate::output::background_timer::{start_timer, StopJoinGuard, StopTimer}; +use crate::output::background::{print_interval, state::PrintGuard}; #[allow(clippy::wildcard_imports)] pub use crate::output::interface::*; use crate::output::style; @@ -156,17 +156,23 @@ where } fn step_timed(self: Box, s: &str) -> Box { - let start = style::step(format!("{s}{}", style::background_timer_start())); - let tick = style::background_timer_tick(); - let end = style::background_timer_end(); + let mut io = self.io; + let data = self.data; + let timer = Instant::now(); - let arc_io = Arc::new(Mutex::new(self.io)); - let background = start_timer(&arc_io, Duration::from_secs(1), start, tick, end); + write_now(&mut io, style::step(s)); + let dot_printer = print_interval( + io, + Duration::from_secs(1), + style::background_timer_start(), + style::background_timer_tick(), + style::background_timer_end(), + ); Box::new(FinishTimedStep { - arc_io, - background, - data: self.data, + data, + timer, + dot_printer, }) } @@ -236,19 +242,6 @@ where writeln_now(&mut self.io, style::important(s.trim())); writeln_now(&mut self.io, ""); } - - fn log_warn_later_shared(&mut self, s: &str) { - let mut formatted = style::warning(s.trim()); - formatted.push('\n'); - - match crate::output::warn_later::try_push(formatted) { - Ok(()) => {} - Err(error) => { - eprintln!("[Buildpack Warning]: Cannot use the delayed warning feature due to error: {error}"); - self.log_warning_shared(s); - } - }; - } } impl ErrorLogger for AnnounceBuildLog @@ -277,15 +270,6 @@ where self } - fn warn_later( - mut self: Box, - s: &str, - ) -> Box> { - self.log_warn_later_shared(s); - - self - } - fn important( mut self: Box, s: &str, @@ -315,14 +299,6 @@ where self } - fn warn_later( - mut self: Box, - s: &str, - ) -> Box> { - self.log_warn_later_shared(s); - self - } - fn important( mut self: Box, s: &str, @@ -448,10 +424,13 @@ where /// /// Used to end a background inline timer i.e. Installing ...... (<0.1s) #[derive(Debug)] -struct FinishTimedStep { +struct FinishTimedStep +where + W: Write + Debug, +{ data: BuildData, - arc_io: Arc>, - background: StopJoinGuard, + timer: Instant, + dot_printer: PrintGuard, } impl TimedStepLogger for FinishTimedStep @@ -460,14 +439,20 @@ where { fn finish_timed_step(self: Box) -> Box { // Must stop background writing thread before retrieving IO - let duration = self.background.stop().elapsed(); - let mut io = try_unwrap_arc_io(self.arc_io); + let data = self.data; + let timer = self.timer; + + let mut io = match self.dot_printer.stop() { + Ok(io) => io, + Err(e) => std::panic::resume_unwind(e), + }; + let duration = timer.elapsed(); writeln_now(&mut io, style::details(style::time::human(&duration))); Box::new(BuildLog { io, - data: self.data, + data, state: PhantomData::, }) } @@ -499,7 +484,6 @@ mod test { use crate::command::CommandExt; use crate::output::style::{self, strip_control_codes}; use crate::output::util::{strip_trailing_whitespace, ReadYourWrite}; - use crate::output::warn_later::WarnGuard; use indoc::formatdoc; use libcnb_test::assert_contains; use pretty_assertions::assert_eq; @@ -641,47 +625,6 @@ mod test { assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); } - #[test] - fn warn_later_doesnt_output_newline() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - - let warn_later = WarnGuard::new(writer.clone()); - BuildLog::new(writer) - .buildpack_name("Walkin' on the Sun") - .section("So don't delay, act now, supplies are running out") - .step("Allow if you're still alive, six to eight years to arrive") - .step("And if you follow, there may be a tomorrow") - .announce() - .warn_later("And all that glitters is gold") - .warn_later("Only shooting stars break the mold") - .end_announce() - .step("But if the offer's shunned") - .step("You might as well be walking on the Sun") - .end_section() - .finish_logging(); - - drop(warn_later); - - let expected = formatdoc! {" - - # Walkin' on the Sun - - - So don't delay, act now, supplies are running out - - Allow if you're still alive, six to eight years to arrive - - And if you follow, there may be a tomorrow - - But if the offer's shunned - - You might as well be walking on the Sun - - Done (finished in < 0.1s) - - ! And all that glitters is gold - - ! Only shooting stars break the mold - "}; - - assert_eq!(expected, strip_control_codes(reader.read_lossy().unwrap())); - } - #[test] fn announce_and_exit_makes_no_whitespace() { let writer = ReadYourWrite::writer(Vec::new()); diff --git a/libherokubuildpack/src/output/interface.rs b/libherokubuildpack/src/output/interface.rs index 46dc615b..52aa1da5 100644 --- a/libherokubuildpack/src/output/interface.rs +++ b/libherokubuildpack/src/output/interface.rs @@ -34,7 +34,6 @@ pub trait AnnounceLogger: ErrorLogger + Debug { type ReturnTo; fn warning(self: Box, s: &str) -> Box>; - fn warn_later(self: Box, s: &str) -> Box>; fn important(self: Box, s: &str) -> Box>; fn end_announce(self: Box) -> Self::ReturnTo; diff --git a/libherokubuildpack/src/output/mod.rs b/libherokubuildpack/src/output/mod.rs index a862a9c9..b3b211ef 100644 --- a/libherokubuildpack/src/output/mod.rs +++ b/libherokubuildpack/src/output/mod.rs @@ -1,7 +1,6 @@ -mod background_timer; +pub(crate) mod background; pub mod build_log; pub mod interface; pub mod section_log; pub mod style; mod util; -pub mod warn_later; diff --git a/libherokubuildpack/src/output/section_log.rs b/libherokubuildpack/src/output/section_log.rs index c5781081..ac1d4e30 100644 --- a/libherokubuildpack/src/output/section_log.rs +++ b/libherokubuildpack/src/output/section_log.rs @@ -108,11 +108,6 @@ pub fn log_warning(s: impl AsRef) { logger().announce().warning(s.as_ref()); } -/// Print an warning block to the output at a later time -pub fn log_warning_later(s: impl AsRef) { - logger().announce().warn_later(s.as_ref()); -} - /// Print an important block to the output pub fn log_important(s: impl AsRef) { logger().announce().important(s.as_ref()); diff --git a/libherokubuildpack/src/output/warn_later.rs b/libherokubuildpack/src/output/warn_later.rs deleted file mode 100644 index 87395ac6..00000000 --- a/libherokubuildpack/src/output/warn_later.rs +++ /dev/null @@ -1,347 +0,0 @@ -use std::cell::RefCell; -use std::fmt::{Debug, Display}; -use std::io::Write; -use std::marker::PhantomData; -use std::rc::Rc; -use std::thread::ThreadId; - -pub type PhantomUnsync = PhantomData>; - -thread_local!(static WARN_LATER: RefCell>> = RefCell::new(None)); - -/// Queue a warning for later -/// -/// Build logs can be quite large and people don't always scroll back up to read every line. Delaying -/// a warning and emitting it right before the end of the build can increase the chances the app -/// developer will read it. -/// -/// ## Use - Setup a `WarnGuard` in your buildpack -/// -/// To ensure warnings are printed, even in the event of errors, you must create a `WarnGuard` -/// in your buildpack that will print any delayed warnings when dropped: -/// -/// ```no_run -/// // src/main.rs -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// // fn build(&self, context: BuildContext) -> libcnb::Result { -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// // ... -/// -/// // Warnings will be emitted when the warn guard is dropped -/// drop(warn_later); -/// // } -/// ``` -/// -/// Alternatively you can manually print delayed warnings: -/// -/// ```no_run -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// // fn build(&self, context: BuildContext) -> libcnb::Result { -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// // ... -/// -/// // Consumes the guard, prints and clears any delayed warnings. -/// warn_later.warn_now(); -/// // } -/// ``` -/// -/// ## Use - Issue a delayed warning -/// -/// Once a warn guard is in place you can queue a warning using `section_log::log_warning_later` or `build_log::*`: -/// -/// ``` -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// use libherokubuildpack::output::build_log::*; -/// -/// // src/main.rs -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// -/// BuildLog::new(std::io::stdout()) -/// .buildpack_name("Julius Caesar") -/// .announce() -/// .warn_later("Beware the ides of march"); -/// ``` -/// -/// ``` -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// use libherokubuildpack::output::section_log::log_warning_later; -/// -/// // src/main.rs -/// let warn_later = WarnGuard::new(std::io::stdout()); -/// -/// // src/layers/greenday.rs -/// log_warning_later("WARNING: Live without warning"); -/// ``` - -/// Pushes a string to a thread local warning vec for to be emitted later -/// -/// # Errors -/// -/// If the internal `WARN_LATER` option is `None` this will emit a `WarnLaterError` because -/// the function call might not be visible to the application owner using the buildpack. -/// -/// This state can happen if no `WarnGuard` is created in the thread where the delayed warning -/// message is trying to be pushed. It can also happen if multiple `WarnGuard`-s are created in the -/// same thread and one of them "takes" the contents before the others go out of scope. -/// -/// For best practice create one and only one `WarnGuard` per thread at a time to avoid this error -/// state. -pub(crate) fn try_push(s: impl AsRef) -> Result<(), WarnLaterError> { - WARN_LATER.with(|cell| match &mut *cell.borrow_mut() { - Some(warnings) => { - warnings.push(s.as_ref().to_string()); - Ok(()) - } - None => Err(WarnLaterError::MissingGuardForThread( - std::thread::current().id(), - )), - }) -} - -/// Ensures a warning vec is present and pushes to it -/// -/// Should only ever be used within a `WarnGuard`. -/// -/// The state where the warnings are taken, but a warn guard is still present -/// can happen when more than one warn guard is created in the same thread -fn force_push(s: impl AsRef) { - WARN_LATER.with(|cell| { - let option = &mut *cell.borrow_mut(); - option - .get_or_insert(Vec::new()) - .push(s.as_ref().to_string()); - }); -} - -/// Removes all delayed warnings from current thread -/// -/// Should only execute from within a `WarnGuard` -fn take() -> Option> { - WARN_LATER.with(|cell| cell.replace(None)) -} - -#[allow(clippy::module_name_repetitions)] -#[derive(Debug)] -pub enum WarnLaterError { - MissingGuardForThread(ThreadId), -} - -impl Display for WarnLaterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WarnLaterError::MissingGuardForThread(id) => { - writeln!( - f, - "Cannot use warn_later unless a WarnGuard has been created\n and not yet dropped in the current thread: {id:?}", - ) - } - } - } -} - -/// Delayed Warning emitter -/// -/// To use the delayed warnings feature you'll need to first register a guard. -/// This guard will emit warnings when it goes out of scope or when you manually force it -/// to emit warnings. -/// -/// This struct allows delayed warnings to be emitted even in the even there's an error. -/// -/// See the `warn_later` module docs for usage instructions. -/// -/// The internal design of this features relies on state tied to the current thread. -/// As a result, this struct is not send or sync: -/// -/// ```compile_fail -/// // Fails to compile -/// # // Do not remove this test, it is the only thing that asserts this is not sync -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// fn is_sync(t: impl Sync) {} -/// -/// is_sync(WarnGuard::new(std::io::stdout())) -/// ``` -/// -/// ```compile_fail -/// // Fails to compile -/// # // Do not remove this test, it is the only thing that asserts this is not send -/// use libherokubuildpack::output::warn_later::WarnGuard; -/// -/// fn is_send(t: impl Send) {} -/// -/// is_send(WarnGuard::new(std::io::stdout())) -/// ``` -/// -/// If you are warning in multiple threads you can pass queued warnings from one thread to another. -/// -/// ```rust -/// use libherokubuildpack::output::warn_later::{WarnGuard, DelayedWarnings}; -/// -/// let main_guard = WarnGuard::new(std::io::stdout()); -/// -/// let (delayed_send, delayed_recv) = std::sync::mpsc::channel::(); -/// -/// std::thread::spawn(move || { -/// let sub_guard = WarnGuard::new(std::io::stdout()); -/// // ... -/// delayed_send -/// .send(sub_guard.consume_quiet()) -/// .unwrap(); -/// }) -/// .join(); -/// -/// main_guard -/// .extend_warnings(delayed_recv.recv().unwrap()); -/// ``` -#[derive(Debug)] -pub struct WarnGuard -where - W: Write + Debug, -{ - // Private inner to force public construction through `new()` which tracks creation state per thread. - io: W, - /// The use of WarnGuard is directly tied to the thread where it was created - /// This forces the struct to not be send or sync - /// - /// To move warn later data between threads, drain quietly, send the data to another - /// thread, and re-apply those warnings to a WarnGuard in the other thread. - unsync: PhantomUnsync, -} - -impl WarnGuard -where - W: Write + Debug, -{ - #[must_use] - #[allow(clippy::new_without_default)] - pub fn new(io: W) -> Self { - WARN_LATER.with(|cell| { - let maybe_warnings = &mut *cell.borrow_mut(); - if let Some(warnings) = maybe_warnings.take() { - let _ = maybe_warnings.insert(warnings); - eprintln!("[Buildpack warning]: Multiple `WarnGuard`-s in thread {id:?}, this may cause unexpected delayed warning output", id = std::thread::current().id()); - } else { - let _ = maybe_warnings.insert(Vec::new()); - } - }); - - Self { - io, - unsync: PhantomData, - } - } - - /// Use to move warnings from a different thread into this one - pub fn extend_warnings(&self, warnings: DelayedWarnings) { - for warning in warnings.inner { - force_push(warning.clone()); - } - } - - /// Use to move warnings out of the current thread without emitting to the UI. - pub fn consume_quiet(self) -> DelayedWarnings { - DelayedWarnings { - inner: take().unwrap_or_default(), - } - } - - /// Consumes self, prints and drains all existing delayed warnings - pub fn warn_now(self) { - drop(self); - } -} - -impl Drop for WarnGuard -where - W: Write + Debug, -{ - fn drop(&mut self) { - if let Some(warnings) = take() { - if !warnings.is_empty() { - for warning in &warnings { - writeln!(&mut self.io).expect("warn guard IO is writeable"); - write!(&mut self.io, "{warning}").expect("warn guard IO is writeable"); - } - } - } - } -} - -/// Holds warnings from a consumed `WarnGuard` -/// -/// The intended use of this struct is to pass warnings from one `WarnGuard` to another. -#[derive(Debug)] -pub struct DelayedWarnings { - // Private inner, must be constructed within a WarnGuard - inner: Vec, -} - -#[cfg(test)] -mod test { - use super::*; - use crate::output::util::ReadYourWrite; - use libcnb_test::assert_contains; - - #[test] - fn test_warn_guard_registers_itself() { - // Err when a guard is not yet created - assert!(try_push("lol").is_err()); - - // Don't err when a guard is created - let _warn_guard = WarnGuard::new(Vec::new()); - try_push("lol").unwrap(); - } - - #[test] - fn test_logging_a_warning() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let warn_guard = WarnGuard::new(writer); - drop(warn_guard); - - assert_eq!(String::new(), reader.read_lossy().unwrap()); - - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let warn_guard = WarnGuard::new(writer); - let message = - "Possessing knowledge and performing an action are two entirely different processes"; - - try_push(message).unwrap(); - drop(warn_guard); - - assert_contains!(reader.read_lossy().unwrap(), message); - - // Assert empty after calling drain - assert!(take().is_none()); - } - - #[test] - fn test_delayed_warnings_on_drop() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let guard = WarnGuard::new(writer); - - let message = "You don't have to have a reason to be tired. You don't have to earn rest or comfort. You're allowed to just be."; - try_push(message).unwrap(); - drop(guard); - - assert_contains!(reader.read_lossy().unwrap(), message); - } - - #[test] - fn does_not_double_whitespace() { - let writer = ReadYourWrite::writer(Vec::new()); - let reader = writer.reader(); - let guard = WarnGuard::new(writer); - - let message = "Caution: This test is hot\n"; - try_push(message).unwrap(); - drop(guard); - - let expected = "\nCaution: This test is hot\n".to_string(); - assert_eq!(expected, reader.read_lossy().unwrap()); - } -}