use std::os::fd::AsFd as _;
pub const TARGET: &str = "stderr";
pub fn is_warning(line: &str) -> bool {
line.contains("FAILED") || line.contains("REFUSED") || line.contains("panicked") || line.to_ascii_lowercase().contains(" error")
}
pub fn say(line: &str) {
let line = line.trim_end_matches(['\r', '\n']);
if line.trim().is_empty() {
return;
}
if is_warning(line) {
tracing::warn!(target: "stderr", "{line}");
} else {
tracing::info!(target: "stderr", "{line}");
}
}
pub fn start() -> std::io::Result<()> {
static STARTED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if STARTED.get().is_some() {
return Ok(());
}
let console: std::fs::File = std::io::stderr().as_fd().try_clone_to_owned()?.into();
let (read, write) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)?;
let reader = std::io::BufReader::new(std::fs::File::from(read));
std::thread::Builder::new().name("stderr-capture".into()).spawn(move || pump(reader))?;
crate::logqueue::set_console(console);
rustix::stdio::dup2_stderr(&write)?;
drop(write);
let _ = STARTED.set(());
Ok(())
}
pub fn pump(mut reader: impl std::io::BufRead) {
let mut buf = Vec::with_capacity(1024);
loop {
buf.clear();
match reader.read_until(b'\n', &mut buf) {
Ok(0) | Err(_) => return,
Ok(_) => say(&String::from_utf8_lossy(&buf)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_that_says_it_failed_is_a_warning() {
assert!(is_warning("monetize-server: grow FAILED: the cloud said no"));
assert!(is_warning("monetize-server: entitlement push REFUSED for gunnar/alice, not retried"));
assert!(is_warning("monetize-server: poller panicked: index out of bounds"));
assert!(!is_warning("monetize-server: listening on 127.0.0.1:50061 (gRPC h2c + POST /callback/<vendor>)"));
}
}