use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::agent::append_bootstrap_log;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Emitted {
Written,
Logged,
Dropped,
}
static FALLBACK_WORKDIR: Mutex<Option<PathBuf>> = Mutex::new(None);
pub fn set_diagnostic_workdir(workdir: &Path) {
let mut slot = FALLBACK_WORKDIR
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*slot = Some(workdir.to_path_buf());
}
pub fn diagnostic_workdir() -> Option<PathBuf> {
FALLBACK_WORKDIR
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub fn to_stdout(line: &str) -> Emitted {
let fallback = diagnostic_workdir();
emit_to(&mut std::io::stdout().lock(), line, fallback.as_deref())
}
pub fn credential_to_stdout(line: &str) -> Emitted {
emit_to(&mut std::io::stdout().lock(), line, None)
}
pub fn to_stderr(line: &str) -> Emitted {
let fallback = diagnostic_workdir();
emit_to(&mut std::io::stderr().lock(), line, fallback.as_deref())
}
pub fn raw_to_stdout(chunk: &str) -> Emitted {
let fallback = diagnostic_workdir();
emit_raw_to(&mut std::io::stdout().lock(), chunk, fallback.as_deref())
}
pub(crate) fn emit_to<W: Write>(out: &mut W, line: &str, fallback: Option<&Path>) -> Emitted {
let written = out
.write_all(line.as_bytes())
.and_then(|()| out.write_all(b"\n"))
.and_then(|()| out.flush());
match written {
Ok(()) => Emitted::Written,
Err(_) => fall_back(line, fallback),
}
}
pub(crate) fn emit_raw_to<W: Write>(out: &mut W, chunk: &str, fallback: Option<&Path>) -> Emitted {
let written = out.write_all(chunk.as_bytes()).and_then(|()| out.flush());
match written {
Ok(()) => Emitted::Written,
Err(_) => fall_back(chunk.trim_end_matches('\n'), fallback),
}
}
fn fall_back(line: &str, fallback: Option<&Path>) -> Emitted {
match fallback {
Some(workdir) => {
append_bootstrap_log(workdir, line);
Emitted::Logged
}
None => Emitted::Dropped,
}
}
#[macro_export]
macro_rules! runtime_out {
($($arg:tt)*) => {{
$crate::diagnostic::to_stdout(&::std::format!($($arg)*));
}};
}
#[macro_export]
macro_rules! runtime_err {
($($arg:tt)*) => {{
$crate::diagnostic::to_stderr(&::std::format!($($arg)*));
}};
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error, ErrorKind};
struct BrokenPipe;
impl Write for BrokenPipe {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(Error::new(
ErrorKind::BrokenPipe,
"Broken pipe (os error 32)",
))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(Error::new(
ErrorKind::BrokenPipe,
"Broken pipe (os error 32)",
))
}
}
#[test]
fn refused_line_lands_in_the_bootstrap_log() {
let workdir = tempfile::tempdir().expect("temp workdir");
let outcome = emit_to(
&mut BrokenPipe,
"{\"url\":\"http://127.0.0.1:1/\"}",
Some(workdir.path()),
);
assert_eq!(outcome, Emitted::Logged);
let log = workdir.path().join("logs").join("bootstrap.log");
let contents = std::fs::read_to_string(&log).expect("bootstrap.log was written");
assert_eq!(contents, "{\"url\":\"http://127.0.0.1:1/\"}\n");
}
#[test]
fn refused_line_with_no_workdir_is_dropped() {
let outcome = emit_to(&mut BrokenPipe, "a warning raised during staging", None);
assert_eq!(outcome, Emitted::Dropped);
}
#[test]
fn accepted_line_gets_exactly_one_newline() {
let mut sink: Vec<u8> = Vec::new();
let outcome = emit_to(&mut sink, "session: ses_01", None);
assert_eq!(outcome, Emitted::Written);
assert_eq!(String::from_utf8(sink).unwrap(), "session: ses_01\n");
}
#[test]
fn raw_chunk_is_written_verbatim() {
let mut sink: Vec<u8> = Vec::new();
let outcome = emit_raw_to(&mut sink, "child line\n", None);
assert_eq!(outcome, Emitted::Written);
assert_eq!(String::from_utf8(sink).unwrap(), "child line\n");
}
#[test]
fn refused_raw_chunk_is_logged_without_a_doubled_newline() {
let workdir = tempfile::tempdir().expect("temp workdir");
let outcome = emit_raw_to(&mut BrokenPipe, "child line\n", Some(workdir.path()));
assert_eq!(outcome, Emitted::Logged);
let log = workdir.path().join("logs").join("bootstrap.log");
let contents = std::fs::read_to_string(&log).expect("bootstrap.log was written");
assert_eq!(contents, "child line\n");
}
}