Skip to content

Commit 30d3c34

Browse files
feat: gracefully handle logs from the runner while rolling buffer is active
1 parent 007ebef commit 30d3c34

2 files changed

Lines changed: 79 additions & 11 deletions

File tree

src/local_logger/mod.rs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ lazy_static! {
2828
pub static ref SPINNER: Arc<Mutex<Option<ProgressBar>>> = Arc::new(Mutex::new(None));
2929
pub static ref IS_TTY: bool = std::io::IsTerminal::is_terminal(&std::io::stdout());
3030
static ref CURRENT_GROUP_NAME: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
31+
32+
/// Log records deferred while the rolling buffer owns the terminal.
33+
/// Flushed in `draw_frame` before each redraw.
34+
static ref DEFERRED_LOGS: Mutex<Vec<DeferredLog>> = Mutex::new(Vec::new());
35+
}
36+
37+
/// A snapshot of a log record that can be stored across the rolling-buffer
38+
/// lifetime (the original `log::Record` borrows data and cannot be kept).
39+
struct DeferredLog {
40+
level: log::Level,
41+
message: String,
42+
target: String,
3143
}
3244

3345
/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar.
@@ -125,6 +137,25 @@ impl Log for LocalLogger {
125137
return;
126138
}
127139

140+
// When the rolling buffer is active it owns the terminal region and uses
141+
// cursor manipulation to redraw. Any direct stderr output would corrupt
142+
// the display, so we defer log records and flush them before each redraw.
143+
{
144+
use rolling_buffer::ROLLING_BUFFER;
145+
if let Ok(guard) = ROLLING_BUFFER.try_lock() {
146+
if guard.as_ref().is_some_and(|rb| rb.is_active()) {
147+
if let Ok(mut deferred) = DEFERRED_LOGS.try_lock() {
148+
deferred.push(DeferredLog {
149+
level: record.level(),
150+
message: format!("{}", record.args()),
151+
target: record.target().to_string(),
152+
});
153+
}
154+
return;
155+
}
156+
}
157+
}
158+
128159
suspend_progress_bar(|| print_record(record));
129160
}
130161

@@ -183,36 +214,69 @@ fn indent_lines(s: &str, indent: &str) -> String {
183214

184215
/// Print a log record to the console with the appropriate style
185216
fn print_record(record: &log::Record) {
186-
match record.level() {
217+
eprintln!(
218+
"{}",
219+
format_log(
220+
record.level(),
221+
&format!("{}", record.args()),
222+
record.target(),
223+
)
224+
);
225+
}
226+
227+
/// Format a log entry with the appropriate style for its level.
228+
fn format_log(level: log::Level, message: &str, target: &str) -> String {
229+
match level {
187230
log::Level::Error => {
188231
let prefix = style("\u{f00d}").red().bold();
189-
let msg = indent_lines(&format!("{}", record.args()), " ");
232+
let msg = indent_lines(message, " ");
190233
let msg = Style::new().red().apply_to(msg);
191-
eprintln!(" {prefix} {msg}");
234+
format!(" {prefix} {msg}")
192235
}
193236
log::Level::Warn => {
194237
let prefix = style("\u{f071}").yellow();
195-
let msg = indent_lines(&format!("{}", record.args()), " ");
238+
let msg = indent_lines(message, " ");
196239
let msg = Style::new().yellow().apply_to(msg);
197-
eprintln!(" {prefix} {msg}");
240+
format!(" {prefix} {msg}")
198241
}
199242
log::Level::Info => {
200-
let msg = indent_lines(&format!("{}", record.args()), " ");
243+
let msg = indent_lines(message, " ");
201244
let msg = Style::new().white().apply_to(msg);
202-
eprintln!(" {msg}");
245+
format!(" {msg}")
203246
}
204247
log::Level::Debug => {
205248
let prefix = style("\u{00B7}").dim();
206-
let msg = indent_lines(&format!("{}", record.args()), " ");
249+
let msg = indent_lines(message, " ");
207250
let msg = Style::new().blue().dim().apply_to(msg);
208-
eprintln!(" {prefix} {msg}");
251+
format!(" {prefix} {msg}")
209252
}
210253
log::Level::Trace => {
211-
let raw = format!("[TRACE::{}] {}", record.target(), record.args());
254+
let raw = format!("[TRACE::{target}] {message}");
212255
let msg = indent_lines(&raw, " ");
213256
let msg = Style::new().black().dim().apply_to(msg);
214-
eprintln!(" {msg}");
257+
format!(" {msg}")
258+
}
259+
}
260+
}
261+
262+
/// Flush all log records that were deferred while the rolling buffer was active.
263+
/// Each line is cleared before writing to avoid leftover characters from the
264+
/// rolling buffer frame being overwritten.
265+
pub(crate) fn flush_deferred_logs(term: &console::Term) {
266+
let logs: Vec<DeferredLog> = {
267+
match DEFERRED_LOGS.try_lock() {
268+
Ok(mut deferred) => std::mem::take(&mut *deferred),
269+
Err(_) => return,
215270
}
271+
};
272+
if !logs.is_empty() {
273+
// Clear from cursor to end of screen so that wrapped lines from the
274+
// rolling buffer frame don't leave artifacts behind deferred log output.
275+
term.clear_to_end_of_screen().ok();
276+
}
277+
for log in &logs {
278+
let formatted = format_log(log.level, &log.message, &log.target);
279+
term.write_line(&formatted).ok();
216280
}
217281
}
218282

src/local_logger/rolling_buffer/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ impl RollingBuffer {
210210
self.term.move_cursor_up(self.rendered_count).ok();
211211
}
212212

213+
// Flush deferred logs above the frame so they become permanent output
214+
// and the rolling buffer shifts down naturally.
215+
super::flush_deferred_logs(&self.term);
216+
213217
for line in frame {
214218
self.term.clear_line().ok();
215219
self.term.write_line(line).ok();

0 commit comments

Comments
 (0)