Skip to main content

netscli_core/
trace.rs

1use serde::Serialize;
2use std::process::Stdio;
3use tokio::io::{AsyncBufReadExt, BufReader};
4use tokio::process::Command;
5use tokio::sync::watch;
6
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone, Serialize)]
10pub struct TraceResult {
11    pub host: String,
12    pub tool: String,
13    pub exit_code: Option<i32>,
14    pub lines: Vec<String>,
15}
16
17pub async fn trace_route(
18    host: &str,
19    max_hops: u32,
20    resolve: bool,
21    progress: Option<watch::Sender<String>>,
22) -> Result<TraceResult> {
23    let max_hops = max_hops.clamp(1, 255);
24
25    #[cfg(windows)]
26    {
27        let args = build_tracert_args(host, max_hops, resolve);
28        // Absolute System32 path rather than a bare name -- see
29        // `common::system_tools`. The Unix branch below keeps its bare names
30        // deliberately: its traceroute -> tracepath fallback works by
31        // spawning each and checking for a not-found error, which needs the
32        // PATH lookup that an absolute path would bypass, and Unix `exec`
33        // does not search the executable's own directory anyway.
34        let tracert = crate::common::system_tool("tracert");
35        let tracert = tracert.to_string_lossy().into_owned();
36        run_command_streaming(&tracert, &args, host, progress).await
37    }
38
39    #[cfg(not(windows))]
40    {
41        let mut last_err: Option<Error> = None;
42        let mut progress = progress;
43        for (tool, args) in [
44            ("traceroute", build_traceroute_args(host, max_hops, resolve)),
45            ("tracepath", build_tracepath_args(host, max_hops, resolve)),
46        ] {
47            match run_command_streaming(tool, &args, host, progress.clone()).await {
48                Ok(res) => return Ok(res),
49                Err(e) => {
50                    if is_not_found(&e) {
51                        last_err = Some(e);
52                        progress = None;
53                        continue;
54                    }
55                    return Err(e);
56                }
57            }
58        }
59
60        Err(last_err.unwrap_or_else(|| Error::unsupported("trace tool unavailable")))
61    }
62}
63
64#[cfg(windows)]
65fn build_tracert_args(host: &str, max_hops: u32, resolve: bool) -> Vec<String> {
66    let mut args: Vec<String> = Vec::new();
67    if !resolve {
68        args.push("-d".to_string());
69    }
70    args.push("-h".to_string());
71    args.push(max_hops.to_string());
72    args.push(host.to_string());
73    args
74}
75
76#[cfg(not(windows))]
77fn build_traceroute_args(host: &str, max_hops: u32, resolve: bool) -> Vec<String> {
78    let mut args: Vec<String> = Vec::new();
79    if !resolve {
80        args.push("-n".to_string());
81    }
82    args.push("-m".to_string());
83    args.push(max_hops.to_string());
84    args.push(host.to_string());
85    args
86}
87
88#[cfg(not(windows))]
89fn build_tracepath_args(host: &str, max_hops: u32, resolve: bool) -> Vec<String> {
90    let mut args: Vec<String> = Vec::new();
91    if !resolve {
92        args.push("-n".to_string());
93    }
94    args.push("-m".to_string());
95    args.push(max_hops.to_string());
96    args.push(host.to_string());
97    args
98}
99
100async fn run_command_streaming(
101    tool: &str,
102    args: &[String],
103    host: &str,
104    progress: Option<watch::Sender<String>>,
105) -> Result<TraceResult> {
106    let max_hops = args_max_hops(args).unwrap_or(0);
107    let mut cmd = Command::new(tool);
108    cmd.args(args)
109        .stdout(Stdio::piped())
110        .stderr(Stdio::piped())
111        .kill_on_drop(true);
112
113    let mut child = cmd
114        .spawn()
115        .map_err(|e| Error::Other(format!("failed to spawn {tool}: {e}")))?;
116    let stdout = child
117        .stdout
118        .take()
119        .ok_or_else(|| Error::Other("failed to capture stdout".to_string()))?;
120    let stderr = child
121        .stderr
122        .take()
123        .ok_or_else(|| Error::Other("failed to capture stderr".to_string()))?;
124
125    let mut out_lines: Vec<String> = Vec::new();
126    let mut stdout_lines = BufReader::new(stdout).lines();
127    let mut stderr_lines = BufReader::new(stderr).lines();
128    let mut stdout_done = false;
129    let mut stderr_done = false;
130    let mut status: Option<std::process::ExitStatus> = None;
131
132    while !(stdout_done && stderr_done && status.is_some()) {
133        tokio::select! {
134            line = stdout_lines.next_line(), if !stdout_done => {
135                match line.map_err(|e| Error::Other(format!("trace stdout read failed: {e}")))? {
136                    Some(line) => {
137                        let trimmed = line.trim_end().to_string();
138                        if let Some(tx) = progress.as_ref() {
139                            if let Some(hop) = trimmed.split_whitespace().next().and_then(|t| t.parse::<u32>().ok()) {
140                                let _ = tx.send(format!(
141                                    "hop {hop}/{max_hops} - {rest}",
142                                    rest = trimmed
143                                ));
144                            }
145                        }
146                        out_lines.push(trimmed);
147                    }
148                    None => stdout_done = true,
149                }
150            }
151            line = stderr_lines.next_line(), if !stderr_done => {
152                match line.map_err(|e| Error::Other(format!("trace stderr read failed: {e}")))? {
153                    Some(line) => {
154                        let trimmed = line.trim_end().to_string();
155                        if !trimmed.is_empty() {
156                            out_lines.push(trimmed);
157                        }
158                    }
159                    None => stderr_done = true,
160                }
161            }
162            s = child.wait(), if status.is_none() => {
163                status = Some(s.map_err(|e| Error::Other(format!("trace process wait failed: {e}")))?);
164            }
165        }
166    }
167
168    Ok(TraceResult {
169        host: host.to_string(),
170        tool: tool.to_string(),
171        exit_code: status.and_then(|s| s.code()),
172        lines: out_lines,
173    })
174}
175
176fn args_max_hops(args: &[String]) -> Option<u32> {
177    for i in 0..args.len().saturating_sub(1) {
178        if (args[i] == "-h" || args[i] == "-m") && args[i + 1].parse::<u32>().is_ok() {
179            return args[i + 1].parse::<u32>().ok();
180        }
181    }
182    None
183}
184
185#[cfg(not(windows))]
186fn is_not_found(err: &Error) -> bool {
187    match err {
188        Error::Other(message) => message.contains("os error 2") || message.contains("not found"),
189        _ => false,
190    }
191}