capsule_runtime/
diagnostic.rs1use std::io::Write;
21use std::path::{Path, PathBuf};
22use std::sync::Mutex;
23
24use crate::agent::append_bootstrap_log;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Emitted {
29 Written,
31 Logged,
33 Dropped,
35}
36
37static FALLBACK_WORKDIR: Mutex<Option<PathBuf>> = Mutex::new(None);
42
43pub fn set_diagnostic_workdir(workdir: &Path) {
51 let mut slot = FALLBACK_WORKDIR
52 .lock()
53 .unwrap_or_else(|poisoned| poisoned.into_inner());
54 *slot = Some(workdir.to_path_buf());
55}
56
57pub fn diagnostic_workdir() -> Option<PathBuf> {
59 FALLBACK_WORKDIR
60 .lock()
61 .unwrap_or_else(|poisoned| poisoned.into_inner())
62 .clone()
63}
64
65pub fn to_stdout(line: &str) -> Emitted {
67 let fallback = diagnostic_workdir();
68 emit_to(&mut std::io::stdout().lock(), line, fallback.as_deref())
69}
70
71pub fn credential_to_stdout(line: &str) -> Emitted {
75 emit_to(&mut std::io::stdout().lock(), line, None)
76}
77
78pub fn to_stderr(line: &str) -> Emitted {
80 let fallback = diagnostic_workdir();
81 emit_to(&mut std::io::stderr().lock(), line, fallback.as_deref())
82}
83
84pub fn raw_to_stdout(chunk: &str) -> Emitted {
89 let fallback = diagnostic_workdir();
90 emit_raw_to(&mut std::io::stdout().lock(), chunk, fallback.as_deref())
91}
92
93pub(crate) fn emit_to<W: Write>(out: &mut W, line: &str, fallback: Option<&Path>) -> Emitted {
95 let written = out
96 .write_all(line.as_bytes())
97 .and_then(|()| out.write_all(b"\n"))
98 .and_then(|()| out.flush());
99 match written {
100 Ok(()) => Emitted::Written,
101 Err(_) => fall_back(line, fallback),
102 }
103}
104
105pub(crate) fn emit_raw_to<W: Write>(out: &mut W, chunk: &str, fallback: Option<&Path>) -> Emitted {
108 let written = out.write_all(chunk.as_bytes()).and_then(|()| out.flush());
109 match written {
110 Ok(()) => Emitted::Written,
111 Err(_) => fall_back(chunk.trim_end_matches('\n'), fallback),
112 }
113}
114
115fn fall_back(line: &str, fallback: Option<&Path>) -> Emitted {
116 match fallback {
117 Some(workdir) => {
118 append_bootstrap_log(workdir, line);
119 Emitted::Logged
120 }
121 None => Emitted::Dropped,
122 }
123}
124
125#[macro_export]
131macro_rules! runtime_out {
132 ($($arg:tt)*) => {{
133 $crate::diagnostic::to_stdout(&::std::format!($($arg)*));
134 }};
135}
136
137#[macro_export]
139macro_rules! runtime_err {
140 ($($arg:tt)*) => {{
141 $crate::diagnostic::to_stderr(&::std::format!($($arg)*));
142 }};
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use std::io::{Error, ErrorKind};
149
150 struct BrokenPipe;
152
153 impl Write for BrokenPipe {
154 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
155 Err(Error::new(
156 ErrorKind::BrokenPipe,
157 "Broken pipe (os error 32)",
158 ))
159 }
160
161 fn flush(&mut self) -> std::io::Result<()> {
162 Err(Error::new(
163 ErrorKind::BrokenPipe,
164 "Broken pipe (os error 32)",
165 ))
166 }
167 }
168
169 #[test]
170 fn refused_line_lands_in_the_bootstrap_log() {
171 let workdir = tempfile::tempdir().expect("temp workdir");
172 let outcome = emit_to(
173 &mut BrokenPipe,
174 "{\"url\":\"http://127.0.0.1:1/\"}",
175 Some(workdir.path()),
176 );
177 assert_eq!(outcome, Emitted::Logged);
178 let log = workdir.path().join("logs").join("bootstrap.log");
179 let contents = std::fs::read_to_string(&log).expect("bootstrap.log was written");
180 assert_eq!(contents, "{\"url\":\"http://127.0.0.1:1/\"}\n");
181 }
182
183 #[test]
184 fn refused_line_with_no_workdir_is_dropped() {
185 let outcome = emit_to(&mut BrokenPipe, "a warning raised during staging", None);
186 assert_eq!(outcome, Emitted::Dropped);
187 }
188
189 #[test]
190 fn accepted_line_gets_exactly_one_newline() {
191 let mut sink: Vec<u8> = Vec::new();
192 let outcome = emit_to(&mut sink, "session: ses_01", None);
193 assert_eq!(outcome, Emitted::Written);
194 assert_eq!(String::from_utf8(sink).unwrap(), "session: ses_01\n");
195 }
196
197 #[test]
198 fn raw_chunk_is_written_verbatim() {
199 let mut sink: Vec<u8> = Vec::new();
200 let outcome = emit_raw_to(&mut sink, "child line\n", None);
201 assert_eq!(outcome, Emitted::Written);
202 assert_eq!(String::from_utf8(sink).unwrap(), "child line\n");
203 }
204
205 #[test]
206 fn refused_raw_chunk_is_logged_without_a_doubled_newline() {
207 let workdir = tempfile::tempdir().expect("temp workdir");
208 let outcome = emit_raw_to(&mut BrokenPipe, "child line\n", Some(workdir.path()));
209 assert_eq!(outcome, Emitted::Logged);
210 let log = workdir.path().join("logs").join("bootstrap.log");
211 let contents = std::fs::read_to_string(&log).expect("bootstrap.log was written");
212 assert_eq!(contents, "child line\n");
213 }
214}