1use std::net::IpAddr;
2use std::time::Duration;
3
4use clap::error::ErrorKind;
5
6pub mod app;
7pub mod cli;
8pub mod config;
9pub mod engine;
10pub mod net;
11pub mod render;
12pub mod ring_buffer;
13pub mod runtime;
14pub mod stats;
15
16#[cfg(target_os = "windows")]
17pub mod windows_console;
18
19const DEFAULT_INTERVAL: Duration = Duration::from_secs(1);
20const DEFAULT_TIMEOUT: Duration = Duration::from_secs(1);
21const DEFAULT_PAYLOAD_SIZE: usize = 56;
22
23pub fn run() -> Result<(), clap::Error> {
24 let config = cli::parse_config_from_env()?;
25 let target = resolve_target(&config)?;
26 let app_config = map_runtime_config(&config, target)?;
27
28 #[cfg(any(target_os = "linux", target_os = "macos"))]
29 {
30 run_with_unix_backend(&config, app_config)
31 }
32
33 #[cfg(target_os = "windows")]
34 {
35 run_with_windows_backend(&config, app_config)
36 }
37
38 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
39 {
40 let _ = app_config;
41 Err(runtime_error(
42 "runtime backend is not supported on this platform",
43 ))
44 }
45}
46
47#[cfg(test)]
48#[must_use]
49fn selected_runtime_backend() -> &'static str {
50 #[cfg(any(target_os = "linux", target_os = "macos"))]
51 {
52 return "unix_surge";
53 }
54
55 #[cfg(target_os = "windows")]
56 {
57 return "windows_ping_async";
58 }
59
60 #[allow(unreachable_code)]
61 "unsupported"
62}
63
64fn resolve_target(config: &config::Config) -> Result<IpAddr, clap::Error> {
65 let resolved = net::dns::resolve_once(&config.host, config.family).map_err(|err| {
66 runtime_error(format!("failed to resolve target '{}': {err}", config.host))
67 })?;
68
69 resolved
70 .addresses
71 .first()
72 .copied()
73 .ok_or_else(|| runtime_error(format!("no resolved addresses found for '{}'", config.host)))
74}
75
76fn map_runtime_config(
77 config: &config::Config,
78 target: IpAddr,
79) -> Result<app::AppConfig, clap::Error> {
80 let interval =
81 option_secs_to_duration(config.interval_secs, DEFAULT_INTERVAL, "-i/--interval")?;
82 let timeout = option_secs_to_duration(config.timeout_secs, DEFAULT_TIMEOUT, "-W/--timeout")?;
83
84 let payload_size = match config.packet_size {
85 Some(packet_size) => usize::try_from(packet_size)
86 .map_err(|_| runtime_error("-s/--size is too large for this platform pointer width"))?,
87 None => DEFAULT_PAYLOAD_SIZE,
88 };
89
90 let ttl = match config.ttl {
91 Some(ttl) => Some(
92 u8::try_from(ttl).map_err(|_| runtime_error("-t/--ttl must be between 1 and 255"))?,
93 ),
94 None => None,
95 };
96
97 Ok(app::AppConfig {
98 target,
99 interval,
100 timeout,
101 count: config.count.map(u64::from),
102 payload_size,
103 ttl,
104 })
105}
106
107fn option_secs_to_duration(
108 configured: Option<f64>,
109 fallback: Duration,
110 flag_name: &str,
111) -> Result<Duration, clap::Error> {
112 let Some(seconds) = configured else {
113 return Ok(fallback);
114 };
115
116 if !seconds.is_finite() || seconds <= 0.0 {
117 return Err(runtime_error(format!(
118 "{flag_name} must be a finite value greater than 0"
119 )));
120 }
121
122 Duration::try_from_secs_f64(seconds)
123 .map_err(|_| runtime_error(format!("{flag_name} value is out of range")))
124}
125
126fn runtime_error(message: impl Into<String>) -> clap::Error {
127 clap::Error::raw(ErrorKind::Io, message.into())
128}
129
130#[cfg(any(target_os = "linux", target_os = "macos"))]
131fn run_with_unix_backend(
132 config: &config::Config,
133 app_config: app::AppConfig,
134) -> Result<(), clap::Error> {
135 let mut engine =
136 engine::unix_surge::UnixSurgeEngine::new(engine::unix_surge::UnixSurgeEngineOptions {
137 target: app_config.target,
138 timeout: app_config.timeout,
139 ttl: app_config.ttl,
140 })
141 .map_err(|err| runtime_error(err.to_string()))?;
142
143 runtime::run_with_runtime(&mut engine, &app_config, config)
144 .map(|_| ())
145 .map_err(|err| runtime_error(format!("ping runtime failed: {err}")))
146}
147
148#[cfg(target_os = "windows")]
149fn run_with_windows_backend(
150 config: &config::Config,
151 app_config: app::AppConfig,
152) -> Result<(), clap::Error> {
153 let mut engine = engine::windows_ping_async::WindowsPingAsyncEngine::new(
154 engine::windows_ping_async::WindowsPingAsyncEngineOptions {
155 target: app_config.target,
156 timeout: app_config.timeout,
157 ttl: app_config.ttl,
158 },
159 )
160 .map_err(|err| runtime_error(err.to_string()))?;
161
162 runtime::run_with_runtime(&mut engine, &app_config, config)
163 .map(|_| ())
164 .map_err(|err| runtime_error(format!("ping runtime failed: {err}")))
165}
166
167#[cfg(test)]
168mod tests {
169 use std::net::{IpAddr, Ipv4Addr};
170 use std::time::Duration;
171
172 use crate::cli::parse_config_from_args;
173
174 use super::map_runtime_config;
175
176 #[test]
177 fn maps_native_ping_flags_into_runtime_contract() {
178 let config = parse_config_from_args([
179 "prettyping",
180 "-c",
181 "7",
182 "-i",
183 "0.25",
184 "-W",
185 "2.5",
186 "-s",
187 "80",
188 "-t",
189 "64",
190 "example.com",
191 ])
192 .expect("cli parsing should pass");
193
194 let runtime = map_runtime_config(&config, IpAddr::V4(Ipv4Addr::LOCALHOST))
195 .expect("runtime mapping should pass");
196
197 assert_eq!(runtime.count, Some(7));
198 assert_eq!(runtime.interval, Duration::from_millis(250));
199 assert_eq!(runtime.timeout, Duration::from_millis(2_500));
200 assert_eq!(runtime.payload_size, 80);
201 assert_eq!(runtime.ttl, Some(64));
202 }
203
204 #[test]
205 fn uses_runtime_defaults_when_native_ping_flags_are_missing() {
206 let config =
207 parse_config_from_args(["prettyping", "example.com"]).expect("cli parsing should pass");
208
209 let runtime = map_runtime_config(&config, IpAddr::V4(Ipv4Addr::LOCALHOST))
210 .expect("runtime mapping should pass");
211
212 assert_eq!(runtime.count, None);
213 assert_eq!(runtime.interval, Duration::from_secs(1));
214 assert_eq!(runtime.timeout, Duration::from_secs(1));
215 assert_eq!(runtime.payload_size, 56);
216 assert_eq!(runtime.ttl, None);
217 }
218
219 #[test]
220 fn selects_runtime_backend_for_current_platform() {
221 #[cfg(any(target_os = "linux", target_os = "macos"))]
222 assert_eq!(super::selected_runtime_backend(), "unix_surge");
223
224 #[cfg(target_os = "windows")]
225 assert_eq!(super::selected_runtime_backend(), "windows_ping_async");
226
227 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
228 assert_eq!(super::selected_runtime_backend(), "unsupported");
229 }
230}