use crate::core::{apply_filters, flatten_targets, resolve_target, NameMatch, Process, Resolved};
use crate::error::{ExitCode, ProcError, Result};
use crate::ui::{colorize_status, format_duration, format_memory, Printer};
use clap::Args;
use colored::*;
use serde::Serialize;
use std::fmt::Write;
#[derive(Args, Debug)]
pub struct InfoCommand {
#[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
targets: Vec<String>,
#[arg(long, short = 'j')]
json: bool,
#[arg(long, short = 'v')]
verbose: bool,
#[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
pub in_dir: Option<String>,
#[arg(long = "by", short = 'b')]
pub by_name: Option<String>,
#[arg(long)]
pub cmd: bool,
}
impl InfoCommand {
pub fn execute(&self) -> Result<()> {
let printer = Printer::from_flags(self.json, self.verbose);
let all_targets = flatten_targets(&self.targets);
let mode = NameMatch::from_flag(self.cmd);
let resolved = resolve_all(&all_targets, |target| resolve_target(target, mode));
let warning = resolved.warning();
let Resolved {
processes: mut found,
not_found,
..
} = resolved;
apply_filters(&mut found, &self.in_dir, &self.by_name, mode);
Process::measure_cpu(&mut found);
if self.json {
printer.print_json(&InfoOutput {
action: "info",
success: !found.is_empty(),
found_count: found.len(),
not_found_count: not_found.len(),
processes: &found,
not_found: ¬_found,
});
} else {
for proc in &found {
self.print_process_info(proc);
}
if let Some(warning) = warning {
printer.warning(&warning);
}
}
if found.is_empty() {
return Err(ProcError::reported(
ExitCode::NotFound,
format!("No process found matching '{}'", self.targets.join(" ")),
));
}
Ok(())
}
fn print_process_info(&self, proc: &Process) {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
print!("{}", render_process_info(proc, self.verbose, now_secs));
}
}
fn render_process_info(proc: &Process, verbose: bool, now_secs: u64) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"{} Process {}",
"✓".green().bold(),
proc.pid.to_string().cyan().bold()
);
let _ = writeln!(out);
let _ = writeln!(
out,
" {} {}",
"Name:".bright_black(),
proc.name.white().bold()
);
let _ = writeln!(
out,
" {} {}",
"PID:".bright_black(),
proc.pid.to_string().cyan()
);
if let Some(ref cwd) = proc.cwd {
let _ = writeln!(out, " {} {}", "Directory:".bright_black(), cwd);
}
if let Some(ref path) = proc.exe_path {
let _ = writeln!(out, " {} {}", "Path:".bright_black(), path);
}
if let Some(ref user) = proc.user {
let _ = writeln!(out, " {} {}", "User:".bright_black(), user);
}
if let Some(ppid) = proc.parent_pid {
let _ = writeln!(
out,
" {} {}",
"Parent PID:".bright_black(),
ppid.to_string().cyan()
);
}
let status_str = format!("{:?}", proc.status);
let status_colored = colorize_status(&proc.status, &status_str);
let _ = writeln!(out, " {} {}", "Status:".bright_black(), status_colored);
let _ = writeln!(out, " {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
let _ = writeln!(
out,
" {} {}",
"Memory:".bright_black(),
format_memory(proc.memory_mb)
);
if let Some(start_time) = proc.start_time {
let uptime = format_duration(now_secs.saturating_sub(start_time));
let _ = writeln!(out, " {} {}", "Uptime:".bright_black(), uptime);
}
if verbose {
if let Some(ref cmd) = proc.command {
let _ = writeln!(
out,
" {} {}",
"Command:".bright_black(),
cmd.bright_black()
);
}
}
let _ = writeln!(out);
out
}
fn resolve_all(targets: &[String], resolve: impl Fn(&str) -> Result<Vec<Process>>) -> Resolved {
let mut found = Vec::new();
let mut not_found = Vec::new();
let mut hints = Vec::new();
let mut seen_pids = std::collections::HashSet::new();
for target in targets {
match resolve(target) {
Ok(processes) => {
if processes.is_empty() {
not_found.push(target.clone());
} else {
for proc in processes {
if seen_pids.insert(proc.pid) {
found.push(proc);
}
}
}
}
Err(error) => {
not_found.push(target.clone());
hints.extend(error.command_line_hint());
}
}
}
Resolved {
processes: found,
not_found,
hints,
}
}
#[derive(Serialize)]
struct InfoOutput<'a> {
action: &'static str,
success: bool,
found_count: usize,
not_found_count: usize,
processes: &'a [Process],
not_found: &'a [String],
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ProcessStatus;
fn process(pid: u32) -> Process {
Process {
pid,
name: format!("p{pid}"),
exe_path: None,
cwd: None,
command: None,
argv0: None,
raw: None,
cpu_percent: 0.0,
memory_mb: 0.0,
status: ProcessStatus::Sleeping,
user: None,
parent_pid: None,
start_time: None,
uptime_secs: None,
}
}
fn targets(list: &[&str]) -> Vec<String> {
list.iter().map(|t| t.to_string()).collect()
}
#[test]
fn a_target_that_resolves_to_nothing_is_not_found() {
let resolved = resolve_all(&targets(&["ghost"]), |_| Ok(Vec::new()));
assert!(resolved.processes.is_empty());
assert_eq!(resolved.not_found, vec!["ghost"]);
}
#[test]
fn a_target_that_fails_to_resolve_is_not_found() {
let resolved = resolve_all(&targets(&["a", "b"]), |t| match t {
"a" => Ok(vec![process(1)]),
_ => Err(ProcError::ProcessNotFound(t.to_string())),
});
assert_eq!(resolved.processes.len(), 1);
assert_eq!(resolved.not_found, vec!["b"]);
assert!(resolved.hints.is_empty());
assert_eq!(resolved.warning().unwrap(), "Not found: b");
}
#[test]
fn a_name_in_command_lines_only_is_not_found_with_a_hint() {
let resolved = resolve_all(&targets(&["serve"]), |t| {
Err(ProcError::NameNotFound {
name: t.to_string(),
in_command_line: 2,
})
});
assert_eq!(resolved.not_found, vec!["serve"]);
assert_eq!(
resolved.warning().unwrap(),
"Not found: serve\n 2 processes have 'serve' in their command line (add --cmd to match those)"
);
}
#[test]
fn a_process_two_targets_name_is_found_once() {
let resolved = resolve_all(&targets(&["a", "b", "c"]), |t| match t {
"a" => Ok(vec![process(1), process(2)]),
"b" => Ok(vec![process(2), process(3)]),
_ => Ok(vec![process(1)]),
});
let pids: Vec<u32> = resolved.processes.iter().map(|p| p.pid).collect();
assert_eq!(pids, vec![1, 2, 3]);
assert!(resolved.not_found.is_empty());
assert_eq!(resolved.warning(), None);
}
fn bare(pid: u32) -> Process {
Process {
name: "worker".to_string(),
..process(pid)
}
}
fn render(proc: &Process, verbose: bool, now_secs: u64) -> String {
colored::control::set_override(false);
render_process_info(proc, verbose, now_secs)
}
#[test]
fn a_process_with_nothing_known_shows_only_what_always_is() {
let out = render(&bare(7), true, 1_000);
assert_eq!(
out,
"✓ Process 7\n\n Name: worker\n PID: 7\n Status: Sleeping\n CPU: 0.0%\n Memory: 0KB\n\n"
);
}
#[test]
fn every_known_fact_gets_a_line() {
let proc = Process {
cwd: Some("/work".to_string()),
exe_path: Some("/bin/worker".to_string()),
user: Some("zee".to_string()),
parent_pid: Some(1),
command: Some("/bin/worker --flag".to_string()),
start_time: Some(940),
..bare(7)
};
let out = render(&proc, false, 1_000);
for line in [
" Directory: /work\n",
" Path: /bin/worker\n",
" User: zee\n",
" Parent PID: 1\n",
" Uptime: 1m 0s\n",
] {
assert!(out.contains(line), "{line:?} missing from\n{out}");
}
assert!(!out.contains("Command:"), "{out}");
let verbose = render(&proc, true, 1_000);
assert!(
verbose.contains(" Command: /bin/worker --flag\n"),
"{verbose}"
);
}
#[test]
fn a_start_time_in_the_future_reads_as_no_time_at_all() {
let proc = Process {
start_time: Some(5_000),
..bare(7)
};
let out = render(&proc, false, 1_000);
assert!(out.contains(" Uptime: 0s\n"), "{out}");
}
}