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 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}