refactor(cli): reorganize project structure into modular commands
This commit is contained in:
6 files changed
+308
-1523
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
pub mod args;
|
||||||
|
pub mod output;
|
||||||
|
pub mod run;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
pub mod summary;
|
||||||
|
|
||||||
|
pub use args::{ClientArgs, OutputMode};
|
||||||
|
pub use output::{
|
||||||
|
format_event, format_human_event, format_human_event_with_options, HumanEventStats,
|
||||||
|
HumanIpdvPair, HumanOutputOptions,
|
||||||
|
};
|
||||||
|
pub use run::run_stream;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod client;
|
||||||
|
#[cfg(feature = "tui")]
|
||||||
|
pub mod tui;
|
||||||
+21
-1523
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,223 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use irtt_client::{MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH};
|
||||||
|
use irtt_proto::{Clock, ReceivedStats, StampAt};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, clap::Args)]
|
||||||
|
pub struct CommonClientArgs {
|
||||||
|
/// Probe interval.
|
||||||
|
#[arg(long, default_value = "1s", value_parser = parse_duration)]
|
||||||
|
pub interval: Duration,
|
||||||
|
|
||||||
|
/// UDP payload length.
|
||||||
|
#[arg(long, default_value_t = 0, value_parser = parse_length)]
|
||||||
|
pub length: u32,
|
||||||
|
|
||||||
|
/// HMAC key.
|
||||||
|
#[arg(long)]
|
||||||
|
pub hmac: Option<String>,
|
||||||
|
|
||||||
|
/// Clock mode to request.
|
||||||
|
#[arg(long, value_enum, default_value_t = ClockArg::Both)]
|
||||||
|
pub clock: ClockArg,
|
||||||
|
|
||||||
|
/// Timestamp mode to request.
|
||||||
|
#[arg(
|
||||||
|
long = "tstamp",
|
||||||
|
visible_alias = "timestamps",
|
||||||
|
value_enum,
|
||||||
|
value_name = "MODE",
|
||||||
|
default_value_t = TimestampArg::Both
|
||||||
|
)]
|
||||||
|
pub tstamp: TimestampArg,
|
||||||
|
|
||||||
|
/// Received-stats mode to request.
|
||||||
|
#[arg(
|
||||||
|
long = "stats",
|
||||||
|
value_enum,
|
||||||
|
default_value_t = ReceivedStatsArg::Both,
|
||||||
|
help = "Server received-stats negotiation mode"
|
||||||
|
)]
|
||||||
|
pub stats: ReceivedStatsArg,
|
||||||
|
|
||||||
|
/// Server payload fill string to request, up to 32 bytes.
|
||||||
|
#[arg(
|
||||||
|
long = "sfill",
|
||||||
|
visible_alias = "server-fill",
|
||||||
|
value_name = "STRING",
|
||||||
|
value_parser = parse_server_fill
|
||||||
|
)]
|
||||||
|
pub server_fill: Option<String>,
|
||||||
|
|
||||||
|
/// DSCP codepoint to request; this is not a raw TOS or Traffic Class byte.
|
||||||
|
#[arg(long, default_value_t = 0, value_name = "0..=63", value_parser = parse_dscp)]
|
||||||
|
pub dscp: u8,
|
||||||
|
|
||||||
|
/// Local outgoing IPv4 TTL or IPv6 unicast hop limit; not negotiated.
|
||||||
|
#[arg(long, value_name = "1..=255", value_parser = parse_ttl)]
|
||||||
|
pub ttl: Option<u32>,
|
||||||
|
|
||||||
|
/// Accept safe server restrictions during negotiation.
|
||||||
|
#[arg(long)]
|
||||||
|
pub loose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum ClockArg {
|
||||||
|
Wall,
|
||||||
|
Monotonic,
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClockArg> for Clock {
|
||||||
|
fn from(value: ClockArg) -> Self {
|
||||||
|
match value {
|
||||||
|
ClockArg::Wall => Self::Wall,
|
||||||
|
ClockArg::Monotonic => Self::Monotonic,
|
||||||
|
ClockArg::Both => Self::Both,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum TimestampArg {
|
||||||
|
None,
|
||||||
|
Send,
|
||||||
|
Receive,
|
||||||
|
Both,
|
||||||
|
Midpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampArg> for StampAt {
|
||||||
|
fn from(value: TimestampArg) -> Self {
|
||||||
|
match value {
|
||||||
|
TimestampArg::None => Self::None,
|
||||||
|
TimestampArg::Send => Self::Send,
|
||||||
|
TimestampArg::Receive => Self::Receive,
|
||||||
|
TimestampArg::Both => Self::Both,
|
||||||
|
TimestampArg::Midpoint => Self::Midpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum ReceivedStatsArg {
|
||||||
|
None,
|
||||||
|
Count,
|
||||||
|
Window,
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReceivedStatsArg> for ReceivedStats {
|
||||||
|
fn from(value: ReceivedStatsArg) -> Self {
|
||||||
|
match value {
|
||||||
|
ReceivedStatsArg::None => Self::None,
|
||||||
|
ReceivedStatsArg::Count => Self::Count,
|
||||||
|
ReceivedStatsArg::Window => Self::Window,
|
||||||
|
ReceivedStatsArg::Both => Self::Both,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_duration(input: &str) -> Result<Duration, String> {
|
||||||
|
let (number, unit) = split_duration(input)?;
|
||||||
|
let value: u64 = number
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid duration value {input:?}"))?;
|
||||||
|
if value == 0 {
|
||||||
|
return Err("duration must be greater than zero".to_owned());
|
||||||
|
}
|
||||||
|
match unit {
|
||||||
|
"ms" => Ok(Duration::from_millis(value)),
|
||||||
|
"s" => Ok(Duration::from_secs(value)),
|
||||||
|
"m" => value
|
||||||
|
.checked_mul(60)
|
||||||
|
.map(Duration::from_secs)
|
||||||
|
.ok_or_else(|| "duration is too large".to_owned()),
|
||||||
|
_ => Err(format!(
|
||||||
|
"unsupported duration unit {unit:?}; use ms, s, or m"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_test_duration(input: &str) -> Result<Duration, String> {
|
||||||
|
if input == "0" {
|
||||||
|
return Ok(Duration::ZERO);
|
||||||
|
}
|
||||||
|
let (number, unit) = split_duration(input)?;
|
||||||
|
let value: u64 = number
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid duration value {input:?}"))?;
|
||||||
|
if value == 0 {
|
||||||
|
return Ok(Duration::ZERO);
|
||||||
|
}
|
||||||
|
match unit {
|
||||||
|
"ms" => Ok(Duration::from_millis(value)),
|
||||||
|
"s" => Ok(Duration::from_secs(value)),
|
||||||
|
"m" => value
|
||||||
|
.checked_mul(60)
|
||||||
|
.map(Duration::from_secs)
|
||||||
|
.ok_or_else(|| "duration is too large".to_owned()),
|
||||||
|
_ => Err(format!(
|
||||||
|
"unsupported duration unit {unit:?}; use ms, s, or m"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_duration(input: &str) -> Result<(&str, &str), String> {
|
||||||
|
let split = input
|
||||||
|
.find(|ch: char| !ch.is_ascii_digit())
|
||||||
|
.ok_or_else(|| "duration must include a unit: ms, s, or m".to_owned())?;
|
||||||
|
let (number, unit) = input.split_at(split);
|
||||||
|
if number.is_empty() || unit.is_empty() {
|
||||||
|
return Err("duration must include a positive value and unit".to_owned());
|
||||||
|
}
|
||||||
|
if number.starts_with('-') {
|
||||||
|
return Err("duration must be greater than zero".to_owned());
|
||||||
|
}
|
||||||
|
Ok((number, unit))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_length(input: &str) -> Result<u32, String> {
|
||||||
|
let length: u32 = input
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid packet length {input:?}"))?;
|
||||||
|
if length > MAX_UDP_PAYLOAD_LENGTH {
|
||||||
|
return Err(format!("packet length must be <= {MAX_UDP_PAYLOAD_LENGTH}"));
|
||||||
|
}
|
||||||
|
Ok(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_server_fill(input: &str) -> Result<String, String> {
|
||||||
|
if input.is_empty() {
|
||||||
|
return Err("server fill must not be empty".to_owned());
|
||||||
|
}
|
||||||
|
let len = input.len();
|
||||||
|
if len > MAX_SERVER_FILL_BYTES {
|
||||||
|
return Err(format!(
|
||||||
|
"server fill must be <= {MAX_SERVER_FILL_BYTES} bytes, got {len}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(input.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_dscp(input: &str) -> Result<u8, String> {
|
||||||
|
let value: u8 = input
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid DSCP codepoint {input:?}"))?;
|
||||||
|
if value > MAX_DSCP_CODEPOINT {
|
||||||
|
return Err(format!("DSCP codepoint must be <= {MAX_DSCP_CODEPOINT}"));
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_ttl(input: &str) -> Result<u32, String> {
|
||||||
|
let value: u32 = input
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid TTL/hop limit {input:?}"))?;
|
||||||
|
if value == 0 || value > MAX_TTL {
|
||||||
|
return Err(format!("TTL/hop limit must be in range 1..={MAX_TTL}"));
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use irtt_client::{ClientConfig, NegotiationPolicy, SocketConfig};
|
||||||
|
|
||||||
|
use super::args::CommonClientArgs;
|
||||||
|
|
||||||
|
pub const DEFAULT_RECV_TIMEOUT: Duration = Duration::from_millis(20);
|
||||||
|
|
||||||
|
impl CommonClientArgs {
|
||||||
|
pub fn to_client_config(&self, server: &str, duration: Duration) -> ClientConfig {
|
||||||
|
ClientConfig {
|
||||||
|
server_addr: server.to_owned(),
|
||||||
|
duration: (!duration.is_zero()).then_some(duration),
|
||||||
|
interval: self.interval,
|
||||||
|
length: self.length,
|
||||||
|
received_stats: self.stats.into(),
|
||||||
|
stamp_at: self.tstamp.into(),
|
||||||
|
clock: self.clock.into(),
|
||||||
|
dscp: self.dscp,
|
||||||
|
hmac_key: self.hmac.as_ref().map(|key| key.as_bytes().to_vec()),
|
||||||
|
server_fill: self.server_fill.clone(),
|
||||||
|
negotiation_policy: if self.loose {
|
||||||
|
NegotiationPolicy::Loose
|
||||||
|
} else {
|
||||||
|
NegotiationPolicy::Strict
|
||||||
|
},
|
||||||
|
socket_config: SocketConfig {
|
||||||
|
ttl: self.ttl,
|
||||||
|
recv_timeout: Some(DEFAULT_RECV_TIMEOUT),
|
||||||
|
..SocketConfig::default()
|
||||||
|
},
|
||||||
|
..ClientConfig::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expected_probe_count(duration: Duration, interval: Duration) -> u64 {
|
||||||
|
let interval_nanos = interval.as_nanos();
|
||||||
|
if interval_nanos == 0 {
|
||||||
|
return u64::MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected = duration
|
||||||
|
.as_nanos()
|
||||||
|
.saturating_add(interval_nanos.saturating_sub(1))
|
||||||
|
/ interval_nanos;
|
||||||
|
expected.min(u128::from(u64::MAX)) as u64
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod client;
|
||||||
Reference in new issue
Block a user