Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a81a8ab

Browse files
committed
fix: eliminate 5s exit delay and restore post-TUI tracing
- Replace Condvar with sync_channel for worker completion signaling (recv_timeout is race-free; condvar notify was lost if fired before wait) - Add TuiTraceGuard RAII type that reroutes tracing to stderr and drains buffered log events when the TUI exits
1 parent 1fc81ef commit a81a8ab

2 files changed

Lines changed: 119 additions & 30 deletions

File tree

src/tui/mod.rs

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,18 +200,68 @@ impl fmt::Debug for Segment {
200200
}
201201
}
202202

203-
/// A [`MakeWriter`] that sends `tracing_subscriber::fmt`-formatted lines through
204-
/// a channel while the TUI is active. When the channel disconnects (TUI exited),
205-
/// output falls back to stderr so post-TUI tracing isn't lost.
203+
/// Shared routing state between [`TuiMakeWriter`] and [`TuiTraceGuard`].
204+
///
205+
/// While `active` is true, formatted trace output is sent to the TUI channel.
206+
/// Once flipped to false (by dropping the guard), output routes to stderr.
207+
pub struct TuiTraceState {
208+
active: AtomicBool,
209+
}
210+
211+
impl TuiTraceState {
212+
fn new() -> Self {
213+
Self {
214+
active: AtomicBool::new(true),
215+
}
216+
}
217+
218+
fn is_active(&self) -> bool {
219+
self.active.load(Ordering::Acquire)
220+
}
221+
}
222+
223+
/// RAII guard that reroutes tracing from the TUI channel to stderr on drop.
224+
///
225+
/// When dropped:
226+
/// 1. Flips the routing flag so future traces write to stderr.
227+
/// 2. Drains any buffered `TuiEvent::Log` events from the channel to stderr.
228+
pub struct TuiTraceGuard {
229+
state: Arc<TuiTraceState>,
230+
rx: mpsc::Receiver<TuiEvent>,
231+
}
232+
233+
impl TuiTraceGuard {
234+
#[must_use]
235+
pub fn new(state: Arc<TuiTraceState>, rx: mpsc::Receiver<TuiEvent>) -> Self {
236+
Self { state, rx }
237+
}
238+
}
239+
240+
impl Drop for TuiTraceGuard {
241+
fn drop(&mut self) {
242+
self.state.active.store(false, Ordering::Release);
243+
244+
while let Ok(event) = self.rx.try_recv() {
245+
if let TuiEvent::Log(msg) = event {
246+
let _ = io::stderr().write_all(msg.as_bytes());
247+
let _ = io::stderr().write_all(b"\n");
248+
}
249+
}
250+
}
251+
}
252+
253+
/// A [`MakeWriter`] that routes formatted trace lines to the TUI channel
254+
/// while the TUI is active, then to stderr after the [`TuiTraceGuard`] drops.
206255
#[derive(Clone)]
207256
pub struct TuiMakeWriter {
208257
tx: mpsc::SyncSender<TuiEvent>,
258+
state: Arc<TuiTraceState>,
209259
}
210260

211261
impl TuiMakeWriter {
212262
#[must_use]
213-
pub fn new(tx: mpsc::SyncSender<TuiEvent>) -> Self {
214-
Self { tx }
263+
pub fn new(tx: mpsc::SyncSender<TuiEvent>, state: Arc<TuiTraceState>) -> Self {
264+
Self { tx, state }
215265
}
216266
}
217267

@@ -221,15 +271,17 @@ impl<'a> MakeWriter<'a> for TuiMakeWriter {
221271
fn make_writer(&'a self) -> Self::Writer {
222272
TuiWriter {
223273
tx: self.tx.clone(),
274+
state: Arc::clone(&self.state),
224275
buf: Vec::with_capacity(256),
225276
}
226277
}
227278
}
228279

229280
/// Per-event writer that buffers a single formatted log line.
230-
/// On drop, sends the line through the channel or falls back to stderr.
281+
/// On drop, routes to the TUI channel or stderr based on [`TuiTraceState`].
231282
pub struct TuiWriter {
232283
tx: mpsc::SyncSender<TuiEvent>,
284+
state: Arc<TuiTraceState>,
233285
buf: Vec<u8>,
234286
}
235287

@@ -254,9 +306,12 @@ impl Drop for TuiWriter {
254306
if trimmed.is_empty() {
255307
return;
256308
}
257-
// Send to TUI for inline display (best-effort).
258-
// A separate tracing layer handles stderr output independently.
259-
let _ = self.tx.try_send(TuiEvent::Log(trimmed));
309+
if self.state.is_active() {
310+
let _ = self.tx.try_send(TuiEvent::Log(trimmed));
311+
} else {
312+
let _ = io::stderr().write_all(trimmed.as_bytes());
313+
let _ = io::stderr().write_all(b"\n");
314+
}
260315
}
261316
}
262317

@@ -586,11 +641,17 @@ mod tests {
586641
assert!(debug.contains('3'));
587642
}
588643

644+
fn make_active_state() -> Arc<TuiTraceState> {
645+
Arc::new(TuiTraceState::new())
646+
}
647+
589648
#[test]
590649
fn tui_writer_sends_through_channel() {
591650
let (tx, rx) = mpsc::sync_channel::<TuiEvent>(16);
651+
let state = make_active_state();
592652
let mut writer = TuiWriter {
593653
tx,
654+
state,
594655
buf: Vec::new(),
595656
};
596657
writer.write_all(b" INFO ferrite: hello world\n").unwrap();
@@ -607,8 +668,10 @@ mod tests {
607668
#[test]
608669
fn tui_writer_empty_buffer_sends_nothing() {
609670
let (tx, rx) = mpsc::sync_channel::<TuiEvent>(16);
671+
let state = make_active_state();
610672
let writer = TuiWriter {
611673
tx,
674+
state,
612675
buf: Vec::new(),
613676
};
614677
drop(writer);
@@ -618,6 +681,37 @@ mod tests {
618681
);
619682
}
620683

684+
#[test]
685+
fn tui_writer_routes_to_stderr_when_inactive() {
686+
let (tx, rx) = mpsc::sync_channel::<TuiEvent>(16);
687+
let state = make_active_state();
688+
state.active.store(false, Ordering::Release);
689+
let mut writer = TuiWriter {
690+
tx,
691+
state,
692+
buf: Vec::new(),
693+
};
694+
writer.write_all(b"routed to stderr\n").unwrap();
695+
drop(writer);
696+
// Nothing sent to the TUI channel
697+
assert!(rx.try_recv().is_err());
698+
}
699+
700+
#[test]
701+
fn tui_trace_guard_drains_and_deactivates() {
702+
let state = make_active_state();
703+
let (tx, rx) = mpsc::sync_channel::<TuiEvent>(16);
704+
let _ = tx.try_send(TuiEvent::Log("buffered msg".into()));
705+
let _ = tx.try_send(TuiEvent::Tick); // non-Log events are discarded
706+
let _ = tx.try_send(TuiEvent::Log("second msg".into()));
707+
708+
let guard = TuiTraceGuard::new(Arc::clone(&state), rx);
709+
assert!(state.is_active());
710+
drop(guard);
711+
assert!(!state.is_active());
712+
// rx is consumed by the guard — channel fully disconnected
713+
}
714+
621715
mod event_loop {
622716
use std::sync::Arc;
623717
use std::sync::atomic::Ordering;

src/tui/run.rs

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![cfg_attr(coverage_nightly, coverage(off))]
22

33
use std::sync::atomic::Ordering;
4-
use std::sync::{Arc, Condvar, Mutex, mpsc};
4+
use std::sync::{Arc, mpsc};
55
use std::thread;
66
use std::time::Duration;
77

@@ -18,7 +18,7 @@ use crate::shutdown;
1818
use crate::units::{Size, UnitSystem};
1919

2020
use super::bridge::EventBridge;
21-
use super::{Segment, TuiConfig, TuiEvent, TuiMakeWriter};
21+
use super::{Segment, TuiConfig, TuiEvent, TuiMakeWriter, TuiTraceGuard, TuiTraceState};
2222

2323
/// Set up the global tracing subscriber with a layered registry.
2424
///
@@ -104,8 +104,10 @@ pub fn run_tui_mode(
104104
) -> Result<()> {
105105
let (tui_tx, tui_rx) = mpsc::sync_channel::<TuiEvent>(256);
106106

107-
// Set up tracing: human ANSI layer -> TUI channel, stderr layer based on mode
108-
let writer = TuiMakeWriter::new(tui_tx.clone());
107+
// Set up tracing: human ANSI layer -> TUI channel, stderr layer based on mode.
108+
// The TuiTraceState lets us reroute to stderr after the TUI exits.
109+
let trace_state = Arc::new(TuiTraceState::new());
110+
let writer = TuiMakeWriter::new(tui_tx.clone(), Arc::clone(&trace_state));
109111
setup_tracing(json_mode, Some(writer));
110112

111113
// Compute region count
@@ -169,8 +171,7 @@ pub fn run_tui_mode(
169171
let worker_regions: Vec<Arc<Segment>> = regions.iter().map(Arc::clone).collect();
170172
let parallel = !sequential;
171173

172-
let worker_done = Arc::new((Mutex::new(false), Condvar::new()));
173-
let worker_done2 = Arc::clone(&worker_done);
174+
let (done_tx, done_rx) = std::sync::mpsc::sync_channel::<()>(1);
174175
let worker = thread::Builder::new()
175176
.name("test-driver".into())
176177
.spawn(move || {
@@ -189,8 +190,6 @@ pub fn run_tui_mode(
189190
thread::Builder::new()
190191
.name(format!("region-{i}"))
191192
.spawn_scoped(s, move || {
192-
// Wait while paused before each pattern (checked inside run via on_activity isn't ideal,
193-
// but the pause loop was per-pattern-start, so we handle it here at region level)
194193
let on_activity = |pos: f64| {
195194
tui_region.activity.touch(pos);
196195
};
@@ -216,10 +215,7 @@ pub fn run_tui_mode(
216215

217216
// All region threads have finished -- signal completion
218217
let _ = event_tx.send(RunEvent::RunComplete);
219-
220-
let (lock, cvar) = &*worker_done2;
221-
*lock.lock().unwrap() = true;
222-
cvar.notify_one();
218+
let _ = done_tx.send(());
223219
})
224220
.expect("failed to spawn test-driver thread");
225221

@@ -237,15 +233,14 @@ pub fn run_tui_mode(
237233
let config = TuiConfig::default();
238234
crate::tui::run_tui(&config, &regions, &tui_tx, &tui_rx).context("TUI failed")?;
239235

240-
// Wait for the worker with a bounded timeout.
241-
{
242-
let (lock, cvar) = &*worker_done;
243-
let guard = lock.lock().unwrap();
244-
let (done, _) = cvar.wait_timeout(guard, Duration::from_secs(5)).unwrap();
245-
if !*done {
246-
eprintln!("Worker did not exit within 5s, forcing exit");
247-
shutdown::force_exit(2);
248-
}
236+
// TUI exited. Reroute tracing to stderr and drain buffered log events.
237+
drop(TuiTraceGuard::new(trace_state, tui_rx));
238+
239+
// Wait for the worker with a bounded timeout (recv_timeout is race-free
240+
// unlike Condvar::wait_timeout — the message sits in the buffer).
241+
if done_rx.recv_timeout(Duration::from_secs(5)).is_err() {
242+
eprintln!("Worker did not exit within 5s, forcing exit");
243+
shutdown::force_exit(2);
249244
}
250245
let _ = worker.join();
251246
if bridge_handle.join().is_err() {

0 commit comments

Comments
 (0)