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