1use crate::core::{apply_filters, flatten_targets, resolve_target, NameMatch, Process, Resolved};
14use crate::error::{ExitCode, ProcError, Result};
15use crate::out_print;
16use crate::ui::{colorize_status, format_duration, format_memory, Printer};
17use clap::Args;
18use colored::*;
19use serde::Serialize;
20use std::fmt::Write;
21
22#[derive(Args, Debug)]
24pub struct InfoCommand {
25 #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
27 targets: Vec<String>,
28
29 #[arg(long, short = 'j')]
31 json: bool,
32
33 #[arg(long, short = 'v')]
35 verbose: bool,
36
37 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
40 pub in_dir: Option<String>,
41
42 #[arg(long = "by", short = 'b')]
44 pub by_name: Option<String>,
45
46 #[arg(long)]
48 pub cmd: bool,
49}
50
51impl InfoCommand {
52 pub fn execute(&self) -> Result<()> {
54 let printer = Printer::from_flags(self.json, self.verbose);
55
56 let all_targets = flatten_targets(&self.targets);
58
59 let mode = NameMatch::from_flag(self.cmd);
60 let resolved = resolve_all(&all_targets, |target| resolve_target(target, mode));
61 let warning = resolved.warning();
62 let Resolved {
63 processes: mut found,
64 not_found,
65 ..
66 } = resolved;
67
68 apply_filters(&mut found, &self.in_dir, &self.by_name, mode)?;
70
71 Process::measure_cpu(&mut found);
73
74 if self.json {
75 printer.print_json(&InfoOutput {
76 action: "info",
77 success: !found.is_empty(),
78 found_count: found.len(),
79 not_found_count: not_found.len(),
80 processes: &found,
81 not_found: ¬_found,
82 });
83 } else {
84 for proc in &found {
85 self.print_process_info(proc);
86 }
87
88 if let Some(warning) = warning {
89 printer.warning(&warning);
90 }
91 }
92
93 if found.is_empty() {
94 return Err(ProcError::reported(
95 ExitCode::NotFound,
96 format!("No process found matching '{}'", self.targets.join(" ")),
97 ));
98 }
99
100 Ok(())
101 }
102
103 fn print_process_info(&self, proc: &Process) {
104 let now_secs = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .map(|d| d.as_secs())
107 .unwrap_or(0);
108 out_print!("{}", render_process_info(proc, self.verbose, now_secs));
109 }
110}
111
112fn render_process_info(proc: &Process, verbose: bool, now_secs: u64) -> String {
115 let mut out = String::new();
117 let _ = writeln!(
118 out,
119 "{} Process {}",
120 "✓".green().bold(),
121 proc.pid.to_string().cyan().bold()
122 );
123 let _ = writeln!(out);
124 let _ = writeln!(
125 out,
126 " {} {}",
127 "Name:".bright_black(),
128 proc.name.white().bold()
129 );
130 let _ = writeln!(
131 out,
132 " {} {}",
133 "PID:".bright_black(),
134 proc.pid.to_string().cyan()
135 );
136
137 if let Some(ref cwd) = proc.cwd {
138 let _ = writeln!(out, " {} {}", "Directory:".bright_black(), cwd);
139 }
140
141 if let Some(ref path) = proc.exe_path {
142 let _ = writeln!(out, " {} {}", "Path:".bright_black(), path);
143 }
144
145 if let Some(ref user) = proc.user {
146 let _ = writeln!(out, " {} {}", "User:".bright_black(), user);
147 }
148
149 if let Some(ppid) = proc.parent_pid {
150 let _ = writeln!(
151 out,
152 " {} {}",
153 "Parent PID:".bright_black(),
154 ppid.to_string().cyan()
155 );
156 }
157
158 let status_str = format!("{:?}", proc.status);
159 let status_colored = colorize_status(&proc.status, &status_str);
160 let _ = writeln!(out, " {} {}", "Status:".bright_black(), status_colored);
161
162 let _ = writeln!(out, " {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
163 let _ = writeln!(
164 out,
165 " {} {}",
166 "Memory:".bright_black(),
167 format_memory(proc.memory_mb)
168 );
169
170 if let Some(start_time) = proc.start_time {
171 let uptime = format_duration(now_secs.saturating_sub(start_time));
172 let _ = writeln!(out, " {} {}", "Uptime:".bright_black(), uptime);
173 }
174
175 if verbose {
176 if let Some(ref cmd) = proc.command {
177 let _ = writeln!(
178 out,
179 " {} {}",
180 "Command:".bright_black(),
181 cmd.bright_black()
182 );
183 }
184 }
185
186 let _ = writeln!(out);
187 out
188}
189
190fn resolve_all(targets: &[String], resolve: impl Fn(&str) -> Result<Vec<Process>>) -> Resolved {
193 let mut found = Vec::new();
194 let mut not_found = Vec::new();
195 let mut hints = Vec::new();
196 let mut seen_pids = std::collections::HashSet::new();
197
198 for target in targets {
199 match resolve(target) {
200 Ok(processes) => {
201 if processes.is_empty() {
202 not_found.push(target.clone());
203 } else {
204 for proc in processes {
205 if seen_pids.insert(proc.pid) {
207 found.push(proc);
208 }
209 }
210 }
211 }
212 Err(error) => {
213 not_found.push(target.clone());
214 hints.extend(error.command_line_hint());
215 }
216 }
217 }
218
219 Resolved {
220 processes: found,
221 not_found,
222 hints,
223 }
224}
225
226#[derive(Serialize)]
227struct InfoOutput<'a> {
228 action: &'static str,
229 success: bool,
230 found_count: usize,
231 not_found_count: usize,
232 processes: &'a [Process],
233 not_found: &'a [String],
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::core::ProcessStatus;
240
241 fn process(pid: u32) -> Process {
242 Process {
243 pid,
244 name: format!("p{pid}"),
245 exe_path: None,
246 cwd: None,
247 command: None,
248 argv0: None,
249 raw: None,
250 cpu_percent: 0.0,
251 memory_mb: 0.0,
252 status: ProcessStatus::Sleeping,
253 user: None,
254 parent_pid: None,
255 start_time: None,
256 uptime_secs: None,
257 }
258 }
259
260 fn targets(list: &[&str]) -> Vec<String> {
261 list.iter().map(|t| t.to_string()).collect()
262 }
263
264 #[test]
265 fn a_target_that_resolves_to_nothing_is_not_found() {
266 let resolved = resolve_all(&targets(&["ghost"]), |_| Ok(Vec::new()));
267 assert!(resolved.processes.is_empty());
268 assert_eq!(resolved.not_found, vec!["ghost"]);
269 }
270
271 #[test]
272 fn a_target_that_fails_to_resolve_is_not_found() {
273 let resolved = resolve_all(&targets(&["a", "b"]), |t| match t {
274 "a" => Ok(vec![process(1)]),
275 _ => Err(ProcError::ProcessNotFound(t.to_string())),
276 });
277 assert_eq!(resolved.processes.len(), 1);
278 assert_eq!(resolved.not_found, vec!["b"]);
279 assert!(resolved.hints.is_empty());
280 assert_eq!(resolved.warning().unwrap(), "Not found: b");
281 }
282
283 #[test]
284 fn a_name_in_command_lines_only_is_not_found_with_a_hint() {
285 let resolved = resolve_all(&targets(&["serve"]), |t| {
286 Err(ProcError::NameNotFound {
287 name: t.to_string(),
288 in_command_line: 2,
289 })
290 });
291 assert_eq!(resolved.not_found, vec!["serve"]);
292 assert_eq!(
293 resolved.warning().unwrap(),
294 "Not found: serve\n 2 processes have 'serve' in their command line (add --cmd to match those)"
295 );
296 }
297
298 #[test]
299 fn a_process_two_targets_name_is_found_once() {
300 let resolved = resolve_all(&targets(&["a", "b", "c"]), |t| match t {
301 "a" => Ok(vec![process(1), process(2)]),
302 "b" => Ok(vec![process(2), process(3)]),
303 _ => Ok(vec![process(1)]),
304 });
305 let pids: Vec<u32> = resolved.processes.iter().map(|p| p.pid).collect();
306 assert_eq!(pids, vec![1, 2, 3]);
307 assert!(resolved.not_found.is_empty());
308 assert_eq!(resolved.warning(), None);
309 }
310
311 fn bare(pid: u32) -> Process {
312 Process {
313 name: "worker".to_string(),
314 ..process(pid)
315 }
316 }
317
318 fn render(proc: &Process, verbose: bool, now_secs: u64) -> String {
319 colored::control::set_override(false);
320 render_process_info(proc, verbose, now_secs)
321 }
322
323 #[test]
324 fn a_process_with_nothing_known_shows_only_what_always_is() {
325 let out = render(&bare(7), true, 1_000);
326 assert_eq!(
327 out,
328 "✓ Process 7\n\n Name: worker\n PID: 7\n Status: Sleeping\n CPU: 0.0%\n Memory: 0KB\n\n"
329 );
330 }
331
332 #[test]
333 fn every_known_fact_gets_a_line() {
334 let proc = Process {
335 cwd: Some("/work".to_string()),
336 exe_path: Some("/bin/worker".to_string()),
337 user: Some("zee".to_string()),
338 parent_pid: Some(1),
339 command: Some("/bin/worker --flag".to_string()),
340 start_time: Some(940),
341 ..bare(7)
342 };
343 let out = render(&proc, false, 1_000);
344 for line in [
345 " Directory: /work\n",
346 " Path: /bin/worker\n",
347 " User: zee\n",
348 " Parent PID: 1\n",
349 " Uptime: 1m 0s\n",
350 ] {
351 assert!(out.contains(line), "{line:?} missing from\n{out}");
352 }
353 assert!(!out.contains("Command:"), "{out}");
355 let verbose = render(&proc, true, 1_000);
356 assert!(
357 verbose.contains(" Command: /bin/worker --flag\n"),
358 "{verbose}"
359 );
360 }
361
362 #[test]
363 fn a_start_time_in_the_future_reads_as_no_time_at_all() {
364 let proc = Process {
366 start_time: Some(5_000),
367 ..bare(7)
368 };
369 let out = render(&proc, false, 1_000);
370 assert!(out.contains(" Uptime: 0s\n"), "{out}");
371 }
372}