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