fix(cli): fail continuous runs on peer closure
Keep PeerClosed as a distinct successful library outcome while treating it as an error at the CLI and TUI boundary for uninterrupted continuous runs. Track aggregate peer-close outcomes independently of bounded recent history and cover finite, continuous, grouped, mixed-failure, interruption, and TUI status behavior.
This commit is contained in:
7 files changed
+358
-19
No files matched your search
@@ -23,7 +23,9 @@ use super::{
|
||||
#[cfg(feature = "stats")]
|
||||
use crate::shared::client::expected_probe_count;
|
||||
use crate::shared::client::{
|
||||
is_shutdown_requested, session::should_print_final_summary, ClientSession,
|
||||
is_shutdown_requested,
|
||||
session::{peer_close_run_error, should_print_final_summary},
|
||||
ClientSession,
|
||||
};
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
@@ -152,6 +154,12 @@ pub fn run_stream(
|
||||
print_events_with_stats(&mut stream_output, &events, Some(target_label), &mut stats)?;
|
||||
#[cfg(not(feature = "stats"))]
|
||||
print_events_with_stats(&mut stream_output, &events, Some(target_label))?;
|
||||
interrupted |= is_shutdown_requested(shutdown_requested);
|
||||
let peer_close_error = peer_close_run_error(
|
||||
continuous,
|
||||
interrupted,
|
||||
if session.is_peer_closed() { 1 } else { 0 },
|
||||
);
|
||||
let print_final_summary = should_print_final_summary(continuous, interrupted);
|
||||
stream_output.print_final_summary = print_final_summary;
|
||||
stream_output.show_running_only_summary_note = continuous && interrupted && print_final_summary;
|
||||
@@ -160,6 +168,9 @@ pub fn run_stream(
|
||||
#[cfg(not(feature = "stats"))]
|
||||
stream_output.print_summary()?;
|
||||
stream_output.out.flush()?;
|
||||
if let Some(error) = peer_close_error {
|
||||
return Err(error.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -403,9 +414,7 @@ fn run_group_stream(
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
|
||||
if outcome.end_reason == ManagedGroupEndReason::Cancelled && !interrupted {
|
||||
return Err("managed client group was cancelled".into());
|
||||
}
|
||||
interrupted |= is_shutdown_requested(shutdown_requested);
|
||||
for target in &outcome.targets {
|
||||
if terminal_targets.insert(target.id.as_str().to_owned()) {
|
||||
report_target_failure(target);
|
||||
@@ -413,11 +422,19 @@ fn run_group_stream(
|
||||
}
|
||||
let successful_targets = outcome.successful_target_outcomes;
|
||||
let failed_targets = outcome.failed_target_outcomes;
|
||||
if !interrupted && successful_targets == 0 && failed_targets > 0 {
|
||||
return Err(
|
||||
format!("no managed target completed successfully ({failed_targets} failed)").into(),
|
||||
);
|
||||
}
|
||||
let terminal_error = if outcome.end_reason == ManagedGroupEndReason::Cancelled && !interrupted {
|
||||
Some("managed client group was cancelled".to_owned())
|
||||
} else if let Some(error) =
|
||||
peer_close_run_error(continuous, interrupted, outcome.peer_closed_target_outcomes)
|
||||
{
|
||||
Some(error)
|
||||
} else if !interrupted && successful_targets == 0 && failed_targets > 0 {
|
||||
Some(format!(
|
||||
"no managed target completed successfully ({failed_targets} failed)"
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let print_final_summary = should_print_final_summary(continuous, interrupted);
|
||||
stream_output.print_final_summary = print_final_summary;
|
||||
@@ -437,6 +454,9 @@ fn run_group_stream(
|
||||
stream_output.print_summary()?;
|
||||
}
|
||||
stream_output.out.flush()?;
|
||||
if let Some(error) = terminal_error {
|
||||
return Err(error.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use irtt_client::{
|
||||
|
||||
use crate::{
|
||||
cmd::tui::args::{ResolvedTuiTarget, TuiArgs},
|
||||
shared::client::{is_shutdown_requested, ClientSession},
|
||||
shared::client::{is_shutdown_requested, session::peer_close_run_error, ClientSession},
|
||||
};
|
||||
|
||||
use super::ui::{should_render, TuiConfig, TuiState, TuiStatus, TuiTerminal};
|
||||
@@ -129,6 +129,16 @@ pub fn run_tui(
|
||||
|
||||
let events = session.close()?;
|
||||
state.process_events(&events);
|
||||
interrupted |= is_shutdown_requested(shutdown_requested);
|
||||
if let Some(error) = peer_close_run_error(
|
||||
continuous,
|
||||
interrupted,
|
||||
if session.is_peer_closed() { 1 } else { 0 },
|
||||
) {
|
||||
state.set_run_error(error.clone());
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
return Err(error.into());
|
||||
}
|
||||
state.set_status(TuiStatus::Complete);
|
||||
render_if_due(&mut terminal, &state, &mut next_render, true)?;
|
||||
Ok(())
|
||||
@@ -249,6 +259,7 @@ fn run_group_tui(
|
||||
}
|
||||
}
|
||||
|
||||
interrupted |= is_shutdown_requested(shutdown_requested);
|
||||
if outcome.end_reason == ManagedGroupEndReason::Cancelled && !interrupted {
|
||||
return match exit {
|
||||
GroupLoopExit::IdleGraceElapsed => {
|
||||
@@ -262,6 +273,15 @@ fn run_group_tui(
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(error) = peer_close_run_error(
|
||||
args.is_continuous(),
|
||||
interrupted,
|
||||
outcome.peer_closed_target_outcomes,
|
||||
) {
|
||||
state.set_run_error(error.clone());
|
||||
render_if_due(terminal, state, next_render, true)?;
|
||||
return Err(error.into());
|
||||
}
|
||||
let successful_targets = outcome.successful_target_outcomes;
|
||||
let failed_targets = outcome.failed_target_outcomes;
|
||||
if !interrupted && successful_targets == 0 && failed_targets > 0 {
|
||||
|
||||
@@ -409,6 +409,12 @@ impl TuiState {
|
||||
self.push_event(format!("error {message}"));
|
||||
}
|
||||
|
||||
pub(super) fn set_run_error(&mut self, message: String) {
|
||||
self.status = TuiStatus::Error;
|
||||
self.last_warning = Some(message.clone());
|
||||
self.push_event(format!("error {message}"));
|
||||
}
|
||||
|
||||
pub(super) fn clear_visible_history(&mut self) {
|
||||
for target in &mut self.targets {
|
||||
target.graph_history.clear();
|
||||
@@ -2412,6 +2418,23 @@ mod tests {
|
||||
.is_some_and(|warning| warning.contains("pending probe limit")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_error_preserves_peer_closed_target_status() {
|
||||
let mut state = TuiState::with_target_labels(TuiConfig::default(), ["a".to_owned()]);
|
||||
state.process_target_outcome(&target_outcome("a", ManagedTargetEndReason::PeerClosed));
|
||||
|
||||
state.set_run_error(
|
||||
"continuous run ended because of peer closure (1 target session)".to_owned(),
|
||||
);
|
||||
|
||||
assert_eq!(state.status, TuiStatus::Error);
|
||||
assert_eq!(state.targets[0].status, TargetStatus::Closed);
|
||||
assert!(state
|
||||
.last_warning
|
||||
.as_deref()
|
||||
.is_some_and(|warning| warning.contains("peer closure")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_failed_terminal_outcomes_set_error_status() {
|
||||
let mut state =
|
||||
|
||||
Reference in new issue
Block a user