1use std::ffi::{CStr, CString};
7use std::os::raw::{c_char, c_double, c_int};
8use std::ptr::NonNull;
9
10use crate::{Error, ErrorKind, Result};
11
12#[allow(non_camel_case_types)]
13mod ffi {
14 use super::{c_char, c_double, c_int};
15
16 #[repr(C)]
18 pub struct iperf_test {
19 _private: [u8; 0],
20 }
21
22 pub type MetricsCallback = unsafe extern "C" fn(
23 *mut iperf_test,
24 c_double,
25 c_double,
26 c_double,
27 c_double,
28 c_double,
29 c_double,
30 c_double,
31 c_double,
32 c_double,
33 c_double,
34 c_double,
35 c_double,
36 c_double,
37 c_double,
38 c_double,
39 c_int,
40 c_int,
41 c_int,
42 c_int,
43 c_int,
44 c_int,
45 c_int,
46 c_int,
47 c_int,
48 c_int,
49 c_int,
50 c_int,
51 c_int,
52 c_int,
53 );
54
55 unsafe extern "C" {
56 pub fn iperf_new_test() -> *mut iperf_test;
57 pub fn iperf_defaults(test: *mut iperf_test) -> c_int;
58 pub fn iperf_free_test(test: *mut iperf_test);
59 pub fn iperf_parse_arguments(
60 test: *mut iperf_test,
61 argc: c_int,
62 argv: *mut *mut c_char,
63 ) -> c_int;
64 pub fn iperf_run_client(test: *mut iperf_test) -> c_int;
65 pub fn iperf_reset_test(test: *mut iperf_test);
66 pub fn iperf_get_test_role(test: *mut iperf_test) -> c_char;
67 pub fn iperf_get_test_one_off(test: *mut iperf_test) -> c_int;
68 pub fn iperf_get_test_json_output_string(test: *mut iperf_test) -> *const c_char;
69 pub fn iperf_get_iperf_version() -> *const c_char;
70
71 pub fn iperf3rs_enable_interval_metrics(
72 test: *mut iperf_test,
73 callback: Option<MetricsCallback>,
74 );
75 pub fn iperf3rs_run_server_once(test: *mut iperf_test) -> c_int;
76 pub fn iperf3rs_suppress_output(test: *mut iperf_test) -> c_int;
77 pub fn iperf3rs_current_errno() -> c_int;
78 pub fn iperf3rs_is_auth_test_error() -> c_int;
79 pub fn iperf3rs_current_error() -> *const c_char;
80 pub fn iperf3rs_ignore_sigpipe();
81 pub fn iperf3rs_usage_long() -> *mut c_char;
82 pub fn iperf3rs_free_string(value: *mut c_char);
83 }
84}
85
86pub(crate) use ffi::iperf_test as RawIperfTest;
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize))]
91#[non_exhaustive]
92pub enum Role {
93 Client,
95 Server,
97 Unknown(i8),
99}
100
101impl Default for Role {
102 fn default() -> Self {
103 Self::Unknown(0)
104 }
105}
106
107pub struct IperfTest {
108 ptr: NonNull<ffi::iperf_test>,
109}
110
111impl IperfTest {
112 pub fn new() -> Result<Self> {
113 let ptr = NonNull::new(unsafe { ffi::iperf_new_test() })
114 .ok_or_else(|| Error::internal("iperf_new_test returned null"))?;
115 let test = Self { ptr };
116 let rc = unsafe { ffi::iperf_defaults(test.as_ptr()) };
117 if rc < 0 {
118 return Err(Error::libiperf(format!(
119 "iperf_defaults failed: {}",
120 current_error()
121 )));
122 }
123 Ok(test)
124 }
125
126 pub(crate) fn as_ptr(&self) -> *mut RawIperfTest {
127 self.ptr.as_ptr()
128 }
129
130 pub fn parse_arguments(&mut self, args: &[String]) -> Result<()> {
131 let cstrings = args
134 .iter()
135 .map(|arg| {
136 CString::new(arg.as_str())
137 .map_err(|_| Error::invalid_argument(format!("argument contains NUL: {arg:?}")))
138 })
139 .collect::<Result<Vec<_>>>()?;
140 let mut argv = cstrings
141 .iter()
142 .map(|arg| arg.as_ptr() as *mut c_char)
143 .collect::<Vec<_>>();
144
145 let rc = unsafe {
146 ffi::iperf_parse_arguments(self.as_ptr(), argv.len() as c_int, argv.as_mut_ptr())
147 };
148 if rc < 0 {
149 return Err(Error::libiperf(format!(
150 "failed to parse iperf options: {}",
151 current_error()
152 )));
153 }
154 Ok(())
155 }
156
157 pub(crate) fn enable_interval_metrics(&mut self, callback: ffi::MetricsCallback) {
158 unsafe { ffi::iperf3rs_enable_interval_metrics(self.as_ptr(), Some(callback)) };
159 }
160
161 pub(crate) fn suppress_output(&mut self) -> Result<()> {
162 let rc = unsafe { ffi::iperf3rs_suppress_output(self.as_ptr()) };
163 if rc < 0 {
164 return Err(Error::internal("failed to suppress libiperf output"));
165 }
166 Ok(())
167 }
168
169 pub fn role(&self) -> Role {
170 match unsafe { ffi::iperf_get_test_role(self.as_ptr()) } as u8 as char {
171 'c' => Role::Client,
172 's' => Role::Server,
173 other => Role::Unknown(other as i8),
174 }
175 }
176
177 pub(crate) fn one_off(&self) -> bool {
178 (unsafe { ffi::iperf_get_test_one_off(self.as_ptr()) }) != 0
179 }
180
181 pub fn json_output(&self) -> Option<String> {
183 let ptr = unsafe { ffi::iperf_get_test_json_output_string(self.as_ptr()) };
184 if ptr.is_null() {
185 return None;
186 }
187 Some(
188 unsafe { CStr::from_ptr(ptr) }
189 .to_string_lossy()
190 .into_owned(),
191 )
192 }
193
194 pub fn run(&mut self) -> Result<()> {
195 unsafe { ffi::iperf3rs_ignore_sigpipe() };
196 match self.role() {
197 Role::Client => self.run_client(),
198 Role::Server => self.run_server(),
199 Role::Unknown(role) => Err(Error::invalid_argument(format!(
200 "iperf role was not set by arguments: {role}"
201 ))),
202 }
203 }
204
205 fn run_client(&mut self) -> Result<()> {
206 let rc = unsafe { ffi::iperf_run_client(self.as_ptr()) };
207 if rc < 0 {
208 return Err(Error::libiperf(format!(
209 "iperf client exited with error: {}",
210 current_error()
211 )));
212 }
213 Ok(())
214 }
215
216 fn run_server(&mut self) -> Result<()> {
217 loop {
218 let rc = unsafe { ffi::iperf3rs_run_server_once(self.as_ptr()) };
221 if rc < 0 {
222 let error = current_error();
223 if rc < -1 {
224 return Err(Error::libiperf(format!(
225 "iperf server exited with error: {error}"
226 )));
227 }
228 eprintln!("iperf server recovered from error: {error}");
229 }
230
231 unsafe { ffi::iperf_reset_test(self.as_ptr()) };
232
233 let auth_error = unsafe { ffi::iperf3rs_is_auth_test_error() } != 0;
234 if self.one_off() && rc != 2 {
235 if rc < 0 && auth_error {
238 continue;
239 }
240 return Ok(());
241 }
242 }
243 }
244}
245
246impl Drop for IperfTest {
247 fn drop(&mut self) {
248 unsafe { ffi::iperf_free_test(self.as_ptr()) };
249 }
250}
251
252pub(crate) fn current_error() -> String {
253 let ptr = unsafe { ffi::iperf3rs_current_error() };
254 if ptr.is_null() {
255 let errno = unsafe { ffi::iperf3rs_current_errno() };
256 return format!("unknown libiperf error ({errno})");
257 }
258 unsafe { CStr::from_ptr(ptr) }
259 .to_string_lossy()
260 .into_owned()
261}
262
263pub fn libiperf_version() -> String {
265 let ptr = unsafe { ffi::iperf_get_iperf_version() };
266 if ptr.is_null() {
267 return "unknown".to_owned();
268 }
269 unsafe { CStr::from_ptr(ptr) }
270 .to_string_lossy()
271 .into_owned()
272}
273
274pub fn usage_long() -> Result<String> {
279 let ptr = unsafe { ffi::iperf3rs_usage_long() };
280 if ptr.is_null() {
281 return Err(Error::new(
282 ErrorKind::Libiperf,
283 "failed to render iperf usage text",
284 ));
285 }
286 let text = unsafe { CStr::from_ptr(ptr) }
287 .to_string_lossy()
288 .into_owned();
289 unsafe { ffi::iperf3rs_free_string(ptr) };
290 Ok(text)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use std::sync::Mutex;
297
298 static IPERF_TEST_LOCK: Mutex<()> = Mutex::new(());
299
300 #[test]
301 fn parser_sets_server_role() {
302 let _guard = IPERF_TEST_LOCK.lock().unwrap();
303 let mut test = IperfTest::new().unwrap();
304 test.parse_arguments(&["iperf3-rs".to_owned(), "-s".to_owned(), "-1".to_owned()])
305 .unwrap();
306
307 assert_eq!(test.role(), Role::Server);
308 assert!(test.json_output().is_none());
309 }
310
311 #[test]
312 fn parser_sets_client_role() {
313 let _guard = IPERF_TEST_LOCK.lock().unwrap();
314 let mut test = IperfTest::new().unwrap();
315 test.parse_arguments(&[
316 "iperf3-rs".to_owned(),
317 "-c".to_owned(),
318 "127.0.0.1".to_owned(),
319 "-t".to_owned(),
320 "1".to_owned(),
321 ])
322 .unwrap();
323
324 assert_eq!(test.role(), Role::Client);
325 }
326}