feat(client): adopt an asynchronous Tokio runtime for the client frontend
This commit is contained in:
8 files changed
+354
-330
No files matched your search
@@ -42,7 +42,7 @@ doc = false
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["client", "server", "tui"]
|
default = ["client", "server", "tui"]
|
||||||
client = ["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats"]
|
client = ["dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats"]
|
||||||
server = ["dep:irtt-server", "dep:tokio"]
|
server = ["dep:irtt-server", "dep:tokio"]
|
||||||
tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"]
|
tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"]
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ irtt-proto = { workspace = true, optional = true }
|
|||||||
irtt-server = { workspace = true, optional = true }
|
irtt-server = { workspace = true, optional = true }
|
||||||
irtt-stats = { workspace = true, optional = true }
|
irtt-stats = { workspace = true, optional = true }
|
||||||
ratatui = { version = "0.30.2", optional = true }
|
ratatui = { version = "0.30.2", optional = true }
|
||||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "time"], optional = true }
|
tokio = { version = "1.53.2", default-features = false, features = ["rt", "time", "sync", "macros"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
irtt-server.workspace = true
|
irtt-server.workspace = true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod args;
|
|||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
mod worker;
|
||||||
|
|
||||||
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
||||||
pub use run::run_stream;
|
pub use run::run_stream;
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
|
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
|
||||||
io::{self, BufRead, Write},
|
io::{self, BufRead, Write},
|
||||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
sync::Arc,
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use irtt_client::{
|
use irtt_client::{
|
||||||
managed::{
|
managed::{
|
||||||
BlockingManagedClient, ManagedClientHandle, ManagedCommandApplyError, ManagedEndReason,
|
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent,
|
||||||
ManagedEvent, ManagedEventSubscription, ManagedEventTryRecvError, ManagedStatus,
|
ManagedEventSubscription, ManagedEventTryRecvError, ManagedStatus, ManagedTargetConfig,
|
||||||
ManagedTargetConfig, ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
||||||
},
|
},
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
};
|
};
|
||||||
@@ -18,13 +17,14 @@ use irtt_client::{
|
|||||||
use super::{
|
use super::{
|
||||||
args::ClientArgs,
|
args::ClientArgs,
|
||||||
output::{EventRenderStats, OutputConfig},
|
output::{EventRenderStats, OutputConfig},
|
||||||
|
worker::ManagedWorker,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::shared::client::{
|
use crate::shared::client::{
|
||||||
expected_probe_count, is_shutdown_requested, parse_stdin_target_set,
|
expected_probe_count, parse_stdin_target_set,
|
||||||
session::{
|
session::{
|
||||||
drain_managed_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
peer_close_run_error, request_managed_stop_for_peer_close, request_managed_stop_once,
|
||||||
request_managed_stop_once, should_print_final_summary, ManagedDrainState,
|
should_print_final_summary,
|
||||||
},
|
},
|
||||||
ManagedRunSetup, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
ManagedRunSetup, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
||||||
};
|
};
|
||||||
@@ -40,12 +40,11 @@ const FINITE_STATS_MEMORY_WARNING_BYTES: u64 = 128 * MIB;
|
|||||||
const FINITE_STATS_MEMORY_STRONG_WARNING_BYTES: u64 = 512 * MIB;
|
const FINITE_STATS_MEMORY_STRONG_WARNING_BYTES: u64 = 512 * MIB;
|
||||||
|
|
||||||
const FINITE_STATS_MEMORY_VERY_STRONG_WARNING_BYTES: u64 = GIB;
|
const FINITE_STATS_MEMORY_VERY_STRONG_WARNING_BYTES: u64 = GIB;
|
||||||
const MANAGED_EVENT_WAIT_SLICE: Duration = Duration::from_millis(20);
|
|
||||||
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
pub fn run_stream(
|
pub async fn run_stream(
|
||||||
args: ClientArgs,
|
args: ClientArgs,
|
||||||
shutdown_requested: &AtomicBool,
|
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if args.list_columns {
|
if args.list_columns {
|
||||||
print!("{}", OutputConfig::list_columns());
|
print!("{}", OutputConfig::list_columns());
|
||||||
@@ -63,7 +62,7 @@ pub fn run_stream(
|
|||||||
)
|
)
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||||
if setup.stdin_controlled {
|
if setup.stdin_controlled {
|
||||||
return run_stdin_stream(setup, output_config, shutdown_requested);
|
return run_stdin_stream(setup, output_config, shutdown).await;
|
||||||
}
|
}
|
||||||
let continuous = args.is_continuous();
|
let continuous = args.is_continuous();
|
||||||
let target_count = setup.target_count();
|
let target_count = setup.target_count();
|
||||||
@@ -71,14 +70,13 @@ pub fn run_stream(
|
|||||||
if let Some(warning) = finite_stats_memory_warning(&args, target_count) {
|
if let Some(warning) = finite_stats_memory_warning(&args, target_count) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
}
|
}
|
||||||
if is_shutdown_requested(shutdown_requested) {
|
if *shutdown.borrow() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let (owner, mut events) = BlockingManagedClient::start_with_subscription(
|
let (task, handle) = ManagedClient::task(setup.managed_config(), setup.managed_targets())?;
|
||||||
setup.managed_config(),
|
let mut events = handle.subscribe()?;
|
||||||
setup.managed_targets(),
|
let mut status = handle.subscribe_status();
|
||||||
)?;
|
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||||
let handle = owner.handle();
|
|
||||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
config: output_config,
|
config: output_config,
|
||||||
@@ -102,9 +100,9 @@ pub fn run_stream(
|
|||||||
let mut dropped_events = 0_u64;
|
let mut dropped_events = 0_u64;
|
||||||
let mut interrupted = false;
|
let mut interrupted = false;
|
||||||
let mut stop_requested = false;
|
let mut stop_requested = false;
|
||||||
let mut subscription_closed = false;
|
|
||||||
loop {
|
loop {
|
||||||
if is_shutdown_requested(shutdown_requested) {
|
let snapshot = status.borrow_and_update().clone();
|
||||||
|
if *shutdown.borrow() {
|
||||||
interrupted = true;
|
interrupted = true;
|
||||||
if request_managed_stop_once(&mut stop_requested) {
|
if request_managed_stop_once(&mut stop_requested) {
|
||||||
drop(handle.stop());
|
drop(handle.stop());
|
||||||
@@ -113,38 +111,33 @@ pub fn run_stream(
|
|||||||
if request_managed_stop_for_peer_close(
|
if request_managed_stop_for_peer_close(
|
||||||
continuous,
|
continuous,
|
||||||
interrupted,
|
interrupted,
|
||||||
handle.status().peer_closed_target_outcomes,
|
snapshot.peer_closed_target_outcomes,
|
||||||
&mut stop_requested,
|
&mut stop_requested,
|
||||||
) {
|
) {
|
||||||
drop(handle.stop());
|
drop(handle.stop());
|
||||||
}
|
}
|
||||||
let drain_state = drain_events(
|
if snapshot.final_outcome.is_some() {
|
||||||
&mut events,
|
|
||||||
&mut stream_output,
|
|
||||||
&mut stats,
|
|
||||||
&mut terminal_targets,
|
|
||||||
&mut dropped_events,
|
|
||||||
)?;
|
|
||||||
match drain_state {
|
|
||||||
ManagedDrainState::Empty => thread::sleep(MANAGED_EVENT_WAIT_SLICE),
|
|
||||||
ManagedDrainState::Closed => subscription_closed = true,
|
|
||||||
ManagedDrainState::BudgetExhausted => {}
|
|
||||||
}
|
|
||||||
if handle.status().final_outcome.is_some() || subscription_closed {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
tokio::select! {
|
||||||
|
_ = shutdown.changed(), if !interrupted => {}
|
||||||
|
changed = status.changed() => { if changed.is_err() { break; } }
|
||||||
|
event = events.recv() => match event {
|
||||||
|
Ok(event) => process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?,
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
|
dropped_events = dropped_events.saturating_add(count);
|
||||||
|
}
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if interrupted {
|
if interrupted {
|
||||||
eprintln!("interrupted, closing managed run...");
|
eprintln!("interrupted, closing managed run...");
|
||||||
}
|
}
|
||||||
let outcome = owner.join()?;
|
let outcome = owner.join().await?;
|
||||||
drain_final_events(
|
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||||
&mut events,
|
process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
||||||
&mut stream_output,
|
})?;
|
||||||
&mut stats,
|
|
||||||
&mut terminal_targets,
|
|
||||||
&mut dropped_events,
|
|
||||||
)?;
|
|
||||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
}
|
}
|
||||||
@@ -159,7 +152,7 @@ pub fn run_stream(
|
|||||||
report_target_failure(target);
|
report_target_failure(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interrupted |= is_shutdown_requested(shutdown_requested);
|
interrupted |= *shutdown.borrow();
|
||||||
let terminal_error = match &outcome.end_reason {
|
let terminal_error = match &outcome.end_reason {
|
||||||
ManagedEndReason::DriverFailed(failure) => {
|
ManagedEndReason::DriverFailed(failure) => {
|
||||||
Some(format!("managed driver failed: {failure}"))
|
Some(format!("managed driver failed: {failure}"))
|
||||||
@@ -201,98 +194,9 @@ pub fn run_stream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct StdinTargetSet {
|
enum StdinUpdate {
|
||||||
revision: u64,
|
Targets(Vec<ManagedTargetConfig>),
|
||||||
targets: Vec<ManagedTargetConfig>,
|
Stop(StdinStop),
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct StdinMailboxState {
|
|
||||||
next_revision: u64,
|
|
||||||
latest: Option<StdinTargetSet>,
|
|
||||||
eof: bool,
|
|
||||||
fatal: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct StdinMailbox {
|
|
||||||
state: Mutex<StdinMailboxState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StdinMailbox {
|
|
||||||
fn publish(&self, targets: Vec<ManagedTargetConfig>) {
|
|
||||||
let mut state = self
|
|
||||||
.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
state.next_revision = state.next_revision.saturating_add(1);
|
|
||||||
state.latest = Some(StdinTargetSet {
|
|
||||||
revision: state.next_revision,
|
|
||||||
targets,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(&self) {
|
|
||||||
self.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
||||||
.eof = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fail(&self, error: String) {
|
|
||||||
let mut state = self
|
|
||||||
.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
state.fatal = Some(error);
|
|
||||||
state.latest = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn latest(&self) -> Option<StdinTargetSet> {
|
|
||||||
self.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
||||||
.latest
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_if_current(&self, revision: u64) {
|
|
||||||
let mut state = self
|
|
||||||
.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
||||||
if state
|
|
||||||
.latest
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|set| set.revision == revision)
|
|
||||||
{
|
|
||||||
state.latest = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_current(&self, revision: u64) -> bool {
|
|
||||||
self.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
||||||
.latest
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|set| set.revision == revision)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fatal(&self) -> Option<String> {
|
|
||||||
self.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
||||||
.fatal
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_finished(&self) -> bool {
|
|
||||||
self.state
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
||||||
.eof
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_stdin_record<R: BufRead>(
|
fn read_stdin_record<R: BufRead>(
|
||||||
@@ -371,7 +275,10 @@ fn read_stdin_record<R: BufRead>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_stdin_target_sets<R: BufRead>(reader: &mut R, mailbox: &StdinMailbox) {
|
fn read_stdin_target_sets<R: BufRead>(
|
||||||
|
reader: &mut R,
|
||||||
|
updates: &tokio::sync::watch::Sender<StdinUpdate>,
|
||||||
|
) {
|
||||||
let mut line = 0_u64;
|
let mut line = 0_u64;
|
||||||
let mut record = Vec::with_capacity(MAX_STDIN_RECORD_BYTES + 1);
|
let mut record = Vec::with_capacity(MAX_STDIN_RECORD_BYTES + 1);
|
||||||
loop {
|
loop {
|
||||||
@@ -383,101 +290,43 @@ fn read_stdin_target_sets<R: BufRead>(reader: &mut R, mailbox: &StdinMailbox) {
|
|||||||
}
|
}
|
||||||
match parse_stdin_target_set(&record, STDIN_MAX_DESIRED_TARGETS) {
|
match parse_stdin_target_set(&record, STDIN_MAX_DESIRED_TARGETS) {
|
||||||
Ok(targets) => {
|
Ok(targets) => {
|
||||||
mailbox.publish(targets.into_iter().map(|target| target.managed).collect())
|
updates.send_replace(StdinUpdate::Targets(
|
||||||
|
targets.into_iter().map(|target| target.managed).collect(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
mailbox.fail(format!("invalid --targets-stdin line {line}: {error}"));
|
updates.send_replace(StdinUpdate::Stop(StdinStop::Fatal(format!(
|
||||||
|
"invalid --targets-stdin line {line}: {error}"
|
||||||
|
))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
mailbox.finish();
|
updates.send_replace(StdinUpdate::Stop(StdinStop::Eof));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
mailbox.fail(format!("failed to read --targets-stdin line {}", line + 1));
|
updates.send_replace(StdinUpdate::Stop(StdinStop::Fatal(format!(
|
||||||
|
"failed to read --targets-stdin line {}",
|
||||||
|
line + 1
|
||||||
|
))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_stdin_target_reader(mailbox: Arc<StdinMailbox>) -> io::Result<()> {
|
fn spawn_stdin_target_reader(updates: tokio::sync::watch::Sender<StdinUpdate>) -> io::Result<()> {
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("irtt-targets-stdin".to_owned())
|
.name("irtt-targets-stdin".to_owned())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
read_stdin_target_sets(&mut stdin.lock(), &mailbox);
|
read_stdin_target_sets(&mut stdin.lock(), &updates);
|
||||||
})
|
})
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct RetryAfterStatus {
|
|
||||||
revision: u64,
|
|
||||||
status: Arc<ManagedStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retry_waits_for_status_change(
|
|
||||||
retry_after: &mut Option<RetryAfterStatus>,
|
|
||||||
target_set: &StdinTargetSet,
|
|
||||||
status: &Arc<ManagedStatus>,
|
|
||||||
) -> bool {
|
|
||||||
let Some(retry) = retry_after else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if retry.revision != target_set.revision {
|
|
||||||
*retry_after = None;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Arc::ptr_eq(&retry.status, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retry_after_capacity_rejection(
|
|
||||||
mailbox: &StdinMailbox,
|
|
||||||
target_set: &StdinTargetSet,
|
|
||||||
status: Arc<ManagedStatus>,
|
|
||||||
) -> Option<RetryAfterStatus> {
|
|
||||||
mailbox
|
|
||||||
.is_current(target_set.revision)
|
|
||||||
.then_some(RetryAfterStatus {
|
|
||||||
revision: target_set.revision,
|
|
||||||
status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_latest_stdin_target_set(
|
|
||||||
handle: &ManagedClientHandle,
|
|
||||||
mailbox: &StdinMailbox,
|
|
||||||
retry_after: &mut Option<RetryAfterStatus>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let Some(target_set) = mailbox.latest() else {
|
|
||||||
*retry_after = None;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let status = handle.status();
|
|
||||||
if retry_waits_for_status_change(retry_after, &target_set, &status) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let receipt = handle
|
|
||||||
.update_targets(target_set.targets.clone())
|
|
||||||
.map_err(|_| "failed to submit --targets-stdin update".to_owned())?;
|
|
||||||
match receipt.blocking_wait() {
|
|
||||||
Ok(_) => {
|
|
||||||
mailbox.clear_if_current(target_set.revision);
|
|
||||||
*retry_after = None;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(ManagedCommandApplyError::LiveGenerationLimitExceeded { .. }) => {
|
|
||||||
*retry_after = retry_after_capacity_rejection(mailbox, &target_set, status);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(_) => Err("--targets-stdin update was rejected".to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BoundedTargetSet {
|
struct BoundedTargetSet {
|
||||||
limit: usize,
|
limit: usize,
|
||||||
order: VecDeque<TargetInstance>,
|
order: VecDeque<TargetInstance>,
|
||||||
@@ -542,37 +391,6 @@ fn process_stdin_event<W: Write>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain_stdin_events<W: Write>(
|
|
||||||
events: &mut ManagedEventSubscription,
|
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
|
||||||
terminal_targets: &mut BoundedTargetSet,
|
|
||||||
dropped_events: &mut u64,
|
|
||||||
) -> io::Result<ManagedDrainState> {
|
|
||||||
drain_managed_events(events, dropped_events, |event| {
|
|
||||||
process_stdin_event(event, stream_output, stats, terminal_targets)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drain_final_stdin_events<W: Write>(
|
|
||||||
events: &mut ManagedEventSubscription,
|
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
|
||||||
terminal_targets: &mut BoundedTargetSet,
|
|
||||||
dropped_events: &mut u64,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
loop {
|
|
||||||
match events.try_recv() {
|
|
||||||
Ok(event) => process_stdin_event(event, stream_output, stats, terminal_targets)?,
|
|
||||||
Err(ManagedEventTryRecvError::Empty | ManagedEventTryRecvError::Closed) => break,
|
|
||||||
Err(ManagedEventTryRecvError::Lagged(count)) => {
|
|
||||||
*dropped_events = dropped_events.saturating_add(count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reconcile_stdin_stats(
|
fn reconcile_stdin_stats(
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
status: &ManagedStatus,
|
status: &ManagedStatus,
|
||||||
@@ -613,36 +431,20 @@ enum StdinStop {
|
|||||||
Fatal(String),
|
Fatal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stdin_stop_request(
|
async fn run_stdin_stream(
|
||||||
shutdown_requested: &AtomicBool,
|
|
||||||
mailbox: &StdinMailbox,
|
|
||||||
) -> Option<StdinStop> {
|
|
||||||
if is_shutdown_requested(shutdown_requested) {
|
|
||||||
Some(StdinStop::Interrupted)
|
|
||||||
} else if let Some(error) = mailbox.fatal() {
|
|
||||||
Some(StdinStop::Fatal(error))
|
|
||||||
} else if mailbox.is_finished() {
|
|
||||||
Some(StdinStop::Eof)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_stdin_stream(
|
|
||||||
setup: ManagedRunSetup,
|
setup: ManagedRunSetup,
|
||||||
output_config: OutputConfig,
|
output_config: OutputConfig,
|
||||||
shutdown_requested: &AtomicBool,
|
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if is_shutdown_requested(shutdown_requested) {
|
if *shutdown.borrow() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let (owner, mut events) = BlockingManagedClient::start_with_subscription(
|
let (task, handle) = ManagedClient::task(setup.managed_config(), setup.managed_targets())?;
|
||||||
setup.managed_config(),
|
let mut events = handle.subscribe()?;
|
||||||
setup.managed_targets(),
|
let mut status = handle.subscribe_status();
|
||||||
)?;
|
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||||
let handle = owner.handle();
|
let (updates, mut stdin) = tokio::sync::watch::channel(StdinUpdate::Targets(Vec::new()));
|
||||||
let mailbox = Arc::new(StdinMailbox::default());
|
spawn_stdin_target_reader(updates)?;
|
||||||
spawn_stdin_target_reader(Arc::clone(&mailbox))?;
|
|
||||||
|
|
||||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
@@ -654,59 +456,103 @@ fn run_stdin_stream(
|
|||||||
};
|
};
|
||||||
let mut stats = BTreeMap::new();
|
let mut stats = BTreeMap::new();
|
||||||
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
||||||
|
let mut stdin_changed = false;
|
||||||
|
let mut desired = None;
|
||||||
|
let mut pending = None;
|
||||||
|
let mut submitted = Vec::new();
|
||||||
let mut retry_after = None;
|
let mut retry_after = None;
|
||||||
let mut dropped_events = 0_u64;
|
let mut dropped_events = 0_u64;
|
||||||
let mut stop_requested = false;
|
let mut stop_requested = false;
|
||||||
let mut stop = None;
|
let mut stop = None;
|
||||||
let mut summary_targets = None;
|
let mut summary_targets = None;
|
||||||
let mut subscription_closed = false;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let status = handle.status();
|
let snapshot = status.borrow_and_update().clone();
|
||||||
if stop.is_none() {
|
if stop.is_none() {
|
||||||
if let Some(requested_stop) = stdin_stop_request(shutdown_requested, &mailbox) {
|
if *shutdown.borrow() {
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status));
|
stop = Some(StdinStop::Interrupted);
|
||||||
stop = Some(requested_stop);
|
} else if stdin_changed || stdin.has_changed().unwrap_or(true) {
|
||||||
if request_managed_stop_once(&mut stop_requested) {
|
stdin_changed = false;
|
||||||
drop(handle.stop());
|
match stdin.borrow_and_update().clone() {
|
||||||
|
StdinUpdate::Targets(targets) => {
|
||||||
|
desired = Some(targets);
|
||||||
|
retry_after = None;
|
||||||
}
|
}
|
||||||
} else if let Err(error) =
|
StdinUpdate::Stop(reason) => stop = Some(reason),
|
||||||
apply_latest_stdin_target_set(&handle, &mailbox, &mut retry_after)
|
}
|
||||||
|
}
|
||||||
|
if stop.is_none()
|
||||||
|
&& pending.is_none()
|
||||||
|
&& !retry_after
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|previous| Arc::ptr_eq(previous, &snapshot))
|
||||||
{
|
{
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status));
|
if let Some(targets) = desired.take() {
|
||||||
stop = Some(StdinStop::Fatal(error));
|
submitted = targets;
|
||||||
|
match handle.update_targets(submitted.clone()) {
|
||||||
|
Ok(receipt) => {
|
||||||
|
pending = Some(receipt);
|
||||||
|
retry_after = Some(snapshot.clone());
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
stop = Some(StdinStop::Fatal(
|
||||||
|
"failed to submit --targets-stdin update".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stop.is_some() {
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &snapshot));
|
||||||
if request_managed_stop_once(&mut stop_requested) {
|
if request_managed_stop_once(&mut stop_requested) {
|
||||||
drop(handle.stop());
|
drop(handle.stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reconcile_stdin_stats(&mut stats, &snapshot, summary_targets.as_ref());
|
||||||
let drain_state = drain_stdin_events(
|
if snapshot.final_outcome.is_some() {
|
||||||
&mut events,
|
|
||||||
&mut stream_output,
|
|
||||||
&mut stats,
|
|
||||||
&mut terminal_targets,
|
|
||||||
&mut dropped_events,
|
|
||||||
)?;
|
|
||||||
reconcile_stdin_stats(&mut stats, &handle.status(), summary_targets.as_ref());
|
|
||||||
match drain_state {
|
|
||||||
ManagedDrainState::Empty => thread::sleep(MANAGED_EVENT_WAIT_SLICE),
|
|
||||||
ManagedDrainState::Closed => subscription_closed = true,
|
|
||||||
ManagedDrainState::BudgetExhausted => {}
|
|
||||||
}
|
|
||||||
if handle.status().final_outcome.is_some() || subscription_closed {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
tokio::select! {
|
||||||
|
_ = shutdown.changed(), if stop.is_none() => {}
|
||||||
|
_ = stdin.changed(), if stop.is_none() => { stdin_changed = true; }
|
||||||
|
changed = status.changed() => { if changed.is_err() { break; } }
|
||||||
|
result = async { pending.as_mut().expect("guarded pending receipt").await }, if pending.is_some() && stop.is_none() => {
|
||||||
|
pending = None;
|
||||||
|
match result {
|
||||||
|
Ok(_) => retry_after = None,
|
||||||
|
Err(ManagedCommandApplyError::LiveGenerationLimitExceeded { .. }) => {
|
||||||
|
// A newer desired set supersedes this rejected set. Otherwise,
|
||||||
|
// retry only after the driver's status advances from submission.
|
||||||
|
if desired.is_none() {
|
||||||
|
desired = Some(std::mem::take(&mut submitted));
|
||||||
|
} else {
|
||||||
|
retry_after = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
stop = Some(StdinStop::Fatal("--targets-stdin update was rejected".to_owned()));
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow()));
|
||||||
|
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event = events.recv() => match event {
|
||||||
|
Ok(event) => {
|
||||||
|
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?;
|
||||||
|
reconcile_stdin_stats(&mut stats, &status.borrow(), summary_targets.as_ref());
|
||||||
|
}
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
|
dropped_events = dropped_events.saturating_add(count);
|
||||||
|
}
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let outcome = owner.join()?;
|
let outcome = owner.join().await?;
|
||||||
drain_final_stdin_events(
|
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||||
&mut events,
|
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
||||||
&mut stream_output,
|
})?;
|
||||||
&mut stats,
|
|
||||||
&mut terminal_targets,
|
|
||||||
&mut dropped_events,
|
|
||||||
)?;
|
|
||||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
}
|
}
|
||||||
@@ -782,28 +628,14 @@ fn process_event<W: Write>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain_events<W: Write>(
|
fn drain_final_events<E>(
|
||||||
events: &mut ManagedEventSubscription,
|
events: &mut ManagedEventSubscription,
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
|
||||||
stats: &mut BTreeMap<String, StatsCollector>,
|
|
||||||
terminal_targets: &mut HashSet<TargetInstance>,
|
|
||||||
dropped_events: &mut u64,
|
dropped_events: &mut u64,
|
||||||
) -> io::Result<ManagedDrainState> {
|
mut process: impl FnMut(ManagedEvent) -> Result<(), E>,
|
||||||
drain_managed_events(events, dropped_events, |event| {
|
) -> Result<(), E> {
|
||||||
process_event(event, stream_output, stats, terminal_targets)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drain_final_events<W: Write>(
|
|
||||||
events: &mut ManagedEventSubscription,
|
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
|
||||||
stats: &mut BTreeMap<String, StatsCollector>,
|
|
||||||
terminal_targets: &mut HashSet<TargetInstance>,
|
|
||||||
dropped_events: &mut u64,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
match events.try_recv() {
|
match events.try_recv() {
|
||||||
Ok(event) => process_event(event, stream_output, stats, terminal_targets)?,
|
Ok(event) => process(event)?,
|
||||||
Err(ManagedEventTryRecvError::Empty | ManagedEventTryRecvError::Closed) => break,
|
Err(ManagedEventTryRecvError::Empty | ManagedEventTryRecvError::Closed) => break,
|
||||||
Err(ManagedEventTryRecvError::Lagged(count)) => {
|
Err(ManagedEventTryRecvError::Lagged(count)) => {
|
||||||
*dropped_events = dropped_events.saturating_add(count);
|
*dropped_events = dropped_events.saturating_add(count);
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
use std::{io, thread, time::Duration};
|
||||||
|
|
||||||
|
use irtt_client::managed::{ManagedClientHandle, ManagedClientTask, ManagedOutcome};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
/// Keeps measurement scheduling separate from frontend output and statistics.
|
||||||
|
/// Drop also stops and joins on frontend errors or cancellation.
|
||||||
|
pub(super) struct ManagedWorker {
|
||||||
|
handle: ManagedClientHandle,
|
||||||
|
thread: Option<thread::JoinHandle<()>>,
|
||||||
|
completion: oneshot::Receiver<io::Result<ManagedOutcome>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagedWorker {
|
||||||
|
pub(super) fn start(task: ManagedClientTask, handle: ManagedClientHandle) -> io::Result<Self> {
|
||||||
|
let (completed, completion) = oneshot::channel();
|
||||||
|
let thread = thread::Builder::new()
|
||||||
|
.name("irtt-managed".to_owned())
|
||||||
|
.spawn(move || {
|
||||||
|
// Construct and destroy the runtime on its owning thread, including
|
||||||
|
// startup failures; never drop a runtime inside the frontend executor.
|
||||||
|
let outcome = (|| {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?;
|
||||||
|
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
runtime.block_on(task)
|
||||||
|
}));
|
||||||
|
runtime.shutdown_timeout(Duration::from_millis(100));
|
||||||
|
match outcome {
|
||||||
|
Ok(outcome) => Ok(outcome),
|
||||||
|
Err(panic) => std::panic::resume_unwind(panic),
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
let _ = completed.send(outcome);
|
||||||
|
})?;
|
||||||
|
Ok(Self {
|
||||||
|
handle,
|
||||||
|
thread: Some(thread),
|
||||||
|
completion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn join(mut self) -> Result<ManagedOutcome, Box<dyn std::error::Error>> {
|
||||||
|
let outcome = (&mut self.completion).await;
|
||||||
|
self.join_thread()?;
|
||||||
|
Ok(outcome
|
||||||
|
.map_err(|_| io::Error::other("managed worker completed without an outcome"))??)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_thread(&mut self) -> io::Result<()> {
|
||||||
|
self.thread
|
||||||
|
.take()
|
||||||
|
.expect("managed worker is joined once")
|
||||||
|
.join()
|
||||||
|
.map_err(|_| io::Error::other("managed worker thread panicked"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ManagedWorker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.thread.is_some() {
|
||||||
|
drop(self.handle.stop());
|
||||||
|
let _ = self.join_thread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use irtt_client::managed::{
|
||||||
|
ManagedClient, ManagedClientConfig, ManagedCompletionPolicy, ManagedEndReason,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The frontend executor must be able to stop a worker while awaiting its
|
||||||
|
// completion; a blocking join here would deadlock this current-thread runtime.
|
||||||
|
#[test]
|
||||||
|
fn completion_leaves_frontend_free_to_stop_and_preserves_outcome() {
|
||||||
|
let (task, handle) = ManagedClient::task(
|
||||||
|
ManagedClientConfig {
|
||||||
|
completion: ManagedCompletionPolicy::ExplicitStop,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let worker = ManagedWorker::start(task, handle.clone()).unwrap();
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
let (outcome, ()) = tokio::time::timeout(Duration::from_secs(2), async {
|
||||||
|
tokio::join!(worker.join(), handle.stop())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("worker completion must allow frontend stop");
|
||||||
|
let outcome = outcome.unwrap();
|
||||||
|
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
|
||||||
|
assert_eq!(handle.status().final_outcome.as_deref(), Some(&outcome));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancelling the frontend's completion wait must stop and join the worker,
|
||||||
|
// rather than detaching an ongoing measurement task.
|
||||||
|
#[test]
|
||||||
|
fn cancelling_completion_stops_and_joins_worker() {
|
||||||
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
task::{Context, Waker},
|
||||||
|
};
|
||||||
|
let (task, handle) = ManagedClient::task(
|
||||||
|
ManagedClientConfig {
|
||||||
|
completion: ManagedCompletionPolicy::ExplicitStop,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let worker = ManagedWorker::start(task, handle.clone()).unwrap();
|
||||||
|
let mut completion = Box::pin(worker.join());
|
||||||
|
assert!(completion
|
||||||
|
.as_mut()
|
||||||
|
.poll(&mut Context::from_waker(Waker::noop()))
|
||||||
|
.is_pending());
|
||||||
|
drop(completion);
|
||||||
|
assert_eq!(
|
||||||
|
handle.status().final_outcome.as_ref().unwrap().end_reason,
|
||||||
|
ManagedEndReason::StopRequested
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#[cfg(any(feature = "client", feature = "server", feature = "tui"))]
|
#[cfg(any(feature = "server", feature = "tui"))]
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
use std::{env, ffi::OsString, process::ExitCode};
|
use std::{env, ffi::OsString, process::ExitCode};
|
||||||
|
|
||||||
use crate::applet::{dispatch_from_argv, AppletDispatch, RequestedApplet};
|
use crate::applet::{dispatch_from_argv, AppletDispatch, RequestedApplet};
|
||||||
#[cfg(any(feature = "client", feature = "server", feature = "tui"))]
|
#[cfg(any(feature = "server", feature = "tui"))]
|
||||||
use crate::signal::install_signal_handler;
|
use crate::signal::install_signal_handler;
|
||||||
|
|
||||||
/// Entry point for the `irtt-rs` multicall dispatcher binary.
|
/// Entry point for the `irtt-rs` multicall dispatcher binary.
|
||||||
@@ -75,9 +75,9 @@ fn run_dispatcher_from_env() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Installs the shutdown signal handler shared by every applet and hands the
|
/// Installs the shutdown signal handler for synchronous applets and hands the
|
||||||
/// resulting flag to `f`.
|
/// resulting flag to `f`.
|
||||||
#[cfg(any(feature = "client", feature = "server", feature = "tui"))]
|
#[cfg(any(feature = "server", feature = "tui"))]
|
||||||
fn with_shutdown_flag(
|
fn with_shutdown_flag(
|
||||||
f: impl FnOnce(&AtomicBool) -> Result<(), Box<dyn std::error::Error>>,
|
f: impl FnOnce(&AtomicBool) -> Result<(), Box<dyn std::error::Error>>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -91,10 +91,13 @@ fn with_shutdown_flag(
|
|||||||
fn run_client_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_client_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
with_shutdown_flag(|shutdown_requested| {
|
let shutdown = crate::signal::install_async_signal_handler()
|
||||||
|
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
||||||
let args = crate::cmd::client::ClientArgs::parse_from(argv);
|
let args = crate::cmd::client::ClientArgs::parse_from(argv);
|
||||||
crate::cmd::client::run_stream(args, shutdown_requested)
|
tokio::runtime::Builder::new_current_thread()
|
||||||
})
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(crate::cmd::client::run_stream(args, shutdown))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "client"))]
|
#[cfg(not(feature = "client"))]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
#[cfg(any(feature = "client", feature = "tui"))]
|
#[cfg(feature = "tui")]
|
||||||
use irtt_client::managed::{ManagedEvent, ManagedEventSubscription, ManagedEventTryRecvError};
|
use irtt_client::managed::{ManagedEvent, ManagedEventSubscription, ManagedEventTryRecvError};
|
||||||
|
|
||||||
#[cfg(any(feature = "client", feature = "tui"))]
|
#[cfg(feature = "tui")]
|
||||||
pub(crate) const MANAGED_EVENT_WORK_BUDGET: usize = 128;
|
pub(crate) const MANAGED_EVENT_WORK_BUDGET: usize = 128;
|
||||||
|
|
||||||
#[cfg(any(feature = "client", feature = "tui"))]
|
#[cfg(feature = "tui")]
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub(crate) enum ManagedDrainState {
|
pub(crate) enum ManagedDrainState {
|
||||||
Empty,
|
Empty,
|
||||||
@@ -16,7 +16,7 @@ pub(crate) enum ManagedDrainState {
|
|||||||
|
|
||||||
/// Drain a bounded amount of lossy presentation work before returning to the
|
/// Drain a bounded amount of lossy presentation work before returning to the
|
||||||
/// frontend control loop.
|
/// frontend control loop.
|
||||||
#[cfg(any(feature = "client", feature = "tui"))]
|
#[cfg(feature = "tui")]
|
||||||
pub(crate) fn drain_managed_events<E>(
|
pub(crate) fn drain_managed_events<E>(
|
||||||
events: &mut ManagedEventSubscription,
|
events: &mut ManagedEventSubscription,
|
||||||
dropped_events: &mut u64,
|
dropped_events: &mut u64,
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
|
#[cfg(any(feature = "server", feature = "tui"))]
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(any(feature = "server", feature = "tui"))]
|
||||||
pub fn install_signal_handler(shutdown_requested: Arc<AtomicBool>) -> Result<(), ctrlc::Error> {
|
pub fn install_signal_handler(shutdown_requested: Arc<AtomicBool>) -> Result<(), ctrlc::Error> {
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
shutdown_requested.store(true, Ordering::Relaxed);
|
shutdown_requested.store(true, Ordering::Relaxed);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preserve ctrlc's SIGINT/SIGTERM policy while waking the async client.
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
pub fn install_async_signal_handler() -> Result<tokio::sync::watch::Receiver<bool>, ctrlc::Error> {
|
||||||
|
let (shutdown, receiver) = tokio::sync::watch::channel(false);
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
shutdown.send_replace(true);
|
||||||
|
})?;
|
||||||
|
Ok(receiver)
|
||||||
|
}
|
||||||
@@ -442,7 +442,7 @@ fn targets_stdin_supersedes_the_active_set_and_stops_on_eof() {
|
|||||||
let mut stdin = client.0.stdin.take().unwrap();
|
let mut stdin = client.0.stdin.take().unwrap();
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
for target in ["first", "second"] {
|
for target in ["first", "second"] {
|
||||||
writeln!(stdin, "{target}={}", server.addr).unwrap();
|
write!(stdin, "\r\n{target}={}\r\n", server.addr).unwrap();
|
||||||
stdin.flush().unwrap();
|
stdin.flush().unwrap();
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
loop {
|
loop {
|
||||||
@@ -485,3 +485,46 @@ fn targets_stdin_supersedes_the_active_set_and_stops_on_eof() {
|
|||||||
.any(|row| row.contains("\"target\":\"second\"")
|
.any(|row| row.contains("\"target\":\"second\"")
|
||||||
&& row.contains("\"event\":\"session_closed\"")));
|
&& row.contains("\"event\":\"session_closed\"")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record bounds and decoding errors must terminate the real stdin frontend,
|
||||||
|
// even when no managed events are available to wake it.
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
#[test]
|
||||||
|
fn targets_stdin_invalid_records_are_fatal_and_empty_eof_is_normal() {
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
process::Stdio,
|
||||||
|
};
|
||||||
|
for (record, success, diagnostic) in [
|
||||||
|
(Vec::new(), true, ""),
|
||||||
|
(b"\r\n\n".to_vec(), true, ""),
|
||||||
|
(b"\n=127.0.0.1:9\n".to_vec(), false, "line 2"),
|
||||||
|
(vec![0xff, b'\n'], false, "line 1"),
|
||||||
|
(vec![b'x'; 64 * 1024 + 2], false, "line 1"),
|
||||||
|
] {
|
||||||
|
let mut client = ClientProcess(
|
||||||
|
Command::new(env!("CARGO_BIN_EXE_irtt-client"))
|
||||||
|
.env_clear()
|
||||||
|
.args(["--duration", "0", "--targets-stdin"])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let mut stdin = client.0.stdin.take().unwrap();
|
||||||
|
stdin.write_all(&record).unwrap();
|
||||||
|
drop(stdin);
|
||||||
|
let status = client.wait();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
client
|
||||||
|
.0
|
||||||
|
.stderr
|
||||||
|
.take()
|
||||||
|
.unwrap()
|
||||||
|
.read_to_string(&mut stderr)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(status.success(), success, "{stderr}");
|
||||||
|
assert!(stderr.contains(diagnostic), "{stderr}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user