use crate::util::ParagraphInspectWrite;
use crate::util::TrailingParagraph;
use crate::util::TrailingParagraphSend;
use std::any::Any;
use std::cell::Cell;
use std::io::Write;
use std::panic::catch_unwind;
use std::panic::resume_unwind;
use std::panic::AssertUnwindSafe;
use std::sync::LazyLock;
use std::sync::Mutex;
static WRITER: LazyLock<Mutex<Box<dyn TrailingParagraphSend>>> =
LazyLock::new(|| Mutex::new(Box::new(ParagraphInspectWrite::new(std::io::stderr()))));
pub struct GlobalWriter;
#[deprecated(since = "0.5.0", note = "_GlobalWriter use GlobalWriter instead")]
pub type _GlobalWriter = GlobalWriter;
impl Write for GlobalWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut w = WRITER.lock().map_err(|_| {
std::io::Error::other("GlobalWriter lock poisoned - cannot guarantee data consistency")
})?;
w.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
let mut w = WRITER.lock().map_err(|_| {
std::io::Error::other("GlobalWriter lock poisoned - cannot guarantee data consistency")
})?;
w.flush()
}
}
impl TrailingParagraph for GlobalWriter {
fn trailing_paragraph(&self) -> bool {
let w = WRITER.lock().unwrap();
w.trailing_paragraph()
}
fn trailing_newline_count(&self) -> usize {
let w = WRITER.lock().unwrap();
w.trailing_newline_count()
}
}
pub fn set_writer<W>(new_writer: W)
where
W: Write + Send + 'static,
{
if std::any::Any::type_id(&new_writer) == std::any::TypeId::of::<GlobalWriter>() {
panic!("Cannot set the global writer to GlobalWriter");
}
let _global_lock = WITH_WRITER_GLOBAL_LOCK
.try_lock()
.expect("Cannot call `set_writer` inside of `with_locked_writer`");
let mut writer = WRITER
.lock()
.expect("Global writer lock poisoned - cannot guarantee data consistency");
*writer = Box::new(ParagraphInspectWrite::new(new_writer));
}
static WITH_WRITER_GLOBAL_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| ().into());
thread_local! {
static WITH_WRITER_REENTRANT_CHECK: Cell<bool> = const { Cell::new(false) };
}
struct ReentrantGuard;
impl ReentrantGuard {
fn new() -> Self {
WITH_WRITER_REENTRANT_CHECK.with(|only_once| {
if only_once.get() {
panic!("Cannot call this function recursively!");
}
only_once.set(true);
});
ReentrantGuard
}
}
impl Drop for ReentrantGuard {
fn drop(&mut self) {
WITH_WRITER_REENTRANT_CHECK.with(|only_once| {
only_once.set(false);
});
}
}
pub fn with_locked_writer<W, F>(new_writer: W, f: F) -> W
where
W: Write + Send + Any + 'static,
F: FnOnce(),
{
if std::any::Any::type_id(&new_writer) == std::any::TypeId::of::<GlobalWriter>() {
panic!("Cannot set the global writer to GlobalWriter");
}
let writer_or_panic = {
let _reentrant_guard = ReentrantGuard::new();
let _global_lock = WITH_WRITER_GLOBAL_LOCK
.lock()
.expect("Global writer coordination lock poisoned - cannot guarantee thread safety");
let old_writer = {
let mut write_lock = WRITER
.lock()
.expect("Global writer lock poisoned - cannot guarantee data consistency");
std::mem::replace(
&mut *write_lock,
Box::new(ParagraphInspectWrite::new(new_writer)),
)
};
let f_panic = catch_unwind(AssertUnwindSafe(f));
let new_writer = {
let mut write_lock = WRITER
.lock()
.expect("Global writer lock poisoned - cannot guarantee data consistency");
std::mem::replace(&mut *write_lock, old_writer)
};
if let Ok(original) = (new_writer as Box<dyn Any>).downcast::<ParagraphInspectWrite<W>>() {
f_panic.map(|_| original.inner)
} else {
panic!("Could not downcast to original type. Writer was mutated unexpectedly. This indicates a bug in with_locked_writer implementation.")
}
};
writer_or_panic.unwrap_or_else(|payload| resume_unwind(payload))
}
#[cfg(feature = "global_functions")]
pub mod print {
use super::*;
use crate::write;
use crate::GlobalTimer;
use std::time::Instant;
pub fn h1(s: impl AsRef<str>) {
write::h1(&mut GlobalWriter, s);
}
pub fn h2(s: impl AsRef<str>) {
write::h2(&mut GlobalWriter, s);
}
pub fn h3(s: impl AsRef<str>) {
write::h3(&mut GlobalWriter, s);
}
pub fn plain(s: impl AsRef<str>) {
write::plain(&mut GlobalWriter, s)
}
pub fn buildpack(s: impl AsRef<str>) -> Instant {
write::h2(&mut GlobalWriter, s);
Instant::now()
}
pub fn header(s: impl AsRef<str>) {
write::h3(&mut GlobalWriter, s);
}
pub fn bullet(s: impl AsRef<str>) {
write::bullet(&mut GlobalWriter, s)
}
pub fn sub_bullet(s: impl AsRef<str>) {
write::sub_bullet(&mut GlobalWriter, s);
}
pub fn sub_stream_with<F, T>(s: impl AsRef<str>, f: F) -> T
where
F: FnMut(Box<dyn Write + Send + Sync>, Box<dyn Write + Send + Sync>) -> T,
T: 'static,
{
write::sub_stream_with(&mut GlobalWriter, s, f)
}
#[cfg(feature = "fun_run")]
pub fn sub_stream_cmd(
command: impl fun_run::CommandWithName,
) -> Result<fun_run::NamedOutput, fun_run::CmdError> {
write::sub_stream_cmd(&mut GlobalWriter, command)
}
pub fn sub_start_timer(s: impl AsRef<str>) -> crate::GlobalTimer {
let started = Instant::now();
let guard = write::sub_start_print_interval(GlobalWriter, s);
GlobalTimer { started, guard }
}
#[cfg(feature = "fun_run")]
pub fn sub_time_cmd(
command: impl fun_run::CommandWithName,
) -> Result<fun_run::NamedOutput, fun_run::CmdError> {
write::sub_time_cmd(ParagraphInspectWrite::new(GlobalWriter), command)
}
pub fn all_done(started: &Option<Instant>) {
write::all_done(&mut GlobalWriter, started);
}
pub fn warning(s: impl AsRef<str>) {
write::warning(&mut GlobalWriter, s);
}
pub fn error(s: impl AsRef<str>) {
write::error(&mut GlobalWriter, s);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::strip_ansi;
use indoc::formatdoc;
use pretty_assertions::assert_eq;
use std::panic;
use std::thread;
#[test]
fn with_locked_writer_handles_panics_across_threads() {
let handle1 = thread::spawn(|| {
panic::catch_unwind(|| {
with_locked_writer(Vec::new(), || {
print::bullet("About to panic");
panic!("Intentional panic for testing");
});
})
});
let result = handle1
.join()
.expect("First thread should complete successfully");
assert!(result.is_err(), "Expected panic to be caught {result:?}");
let handle2 = thread::spawn(|| {
let output = with_locked_writer(Vec::new(), || {
print::bullet("This should work fine");
print::sub_bullet("Even after another thread panicked");
});
let expected = formatdoc! {"
- This should work fine
- Even after another thread panicked
"};
assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&output)));
});
handle2
.join()
.expect("Second thread should complete successfully");
let output = with_locked_writer(Vec::new(), || {
print::bullet("Main thread still works");
});
let expected = "- Main thread still works\n";
assert_eq!(expected, strip_ansi(String::from_utf8_lossy(&output)));
}
}