use gander_config::Config;
use gander_core::row::{Field, Row};
use gander_core::table::{Dir, Filter, Sort};
use gander_core::{format, PLATFORM_SUPPORTED};
use crate::args::Batch;
pub fn run(config: &Config, batch: &Batch) -> std::process::ExitCode {
if !PLATFORM_SUPPORTED {
eprintln!("gander: batch mode requires a live backend (Linux, macOS or FreeBSD)");
return std::process::ExitCode::from(1);
}
let iterations = batch.iterations.unwrap_or(1).max(1);
let sort_field = match batch.sort.as_deref() {
None => Field::PercentCpu,
Some(name) => match Field::from_name(name) {
Some(field) => field,
None => {
let known_elsewhere = is_linux_only_field(name);
if known_elsewhere {
eprintln!(
"gander: --sort {name} is a Linux-only column and this build has no \
data source for it"
);
} else {
eprintln!("gander: unknown --sort field: {name}");
eprintln!("gander: run `gander --help` for the list");
}
return std::process::ExitCode::from(2);
}
},
};
let sort = Sort {
field: sort_field,
dir: if sort_field.default_sort_desc() {
Dir::Desc
} else {
Dir::Asc
},
};
let filter = batch.filter.as_deref().and_then(Filter::parse);
let stdout = std::io::stdout();
let mut out = stdout.lock();
let mut sampler = gander_core::Sampler::default();
gander_core::PlatformSampler::set_scan_options(
&mut sampler,
gander_core::ScanOptions::none()
.with_threads(batch.threads)
.with_smaps(sort.field.needs_smaps_rollup())
.with_gpu(sort.field.needs_gpu_fdinfo())
.with_delayacct(sort.field.needs_delayacct()),
);
for iteration in 0..iterations {
let rows = match sample_once(&mut sampler, sort, filter.as_ref()) {
Ok(rows) => rows,
Err(e) => {
eprintln!("gander: {e}");
return std::process::ExitCode::from(1);
}
};
let result = if batch.json {
write_json(&mut out, &rows, batch.limit, config)
} else {
write_text(&mut out, &rows, batch.limit, sample_clk_tck())
};
if let Err(e) = result {
if e.kind() == std::io::ErrorKind::BrokenPipe {
return std::process::ExitCode::SUCCESS;
}
eprintln!("gander: {e}");
return std::process::ExitCode::from(1);
}
if iteration + 1 < iterations {
std::thread::sleep(config.delay());
}
}
std::process::ExitCode::SUCCESS
}
#[cfg(not(target_os = "linux"))]
fn is_linux_only_field(name: &str) -> bool {
matches!(
name,
"M_PSS"
| "M_EPSS"
| "M_PRIVATE"
| "M_SWAP"
| "GPU%"
| "GPU_TIME"
| "CPUD%"
| "IOD%"
| "SWPD%"
| "CGROUP"
| "CONTAINER"
)
}
#[cfg(target_os = "linux")]
fn is_linux_only_field(_name: &str) -> bool {
false
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
fn sample_clk_tck() -> u64 {
gander_core::CurrentPlatform::clk_tck()
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
fn sample_once(
sampler: &mut gander_core::Sampler,
sort: Sort,
filter: Option<&Filter>,
) -> std::io::Result<Vec<Row>> {
use gander_core::PlatformSampler as _;
let (_snapshot, rows) = sampler.sample();
let mut order: Vec<usize> = if let Some(f) = filter {
(0..rows.len()).filter(|&i| f.matches(&rows[i])).collect()
} else {
(0..rows.len()).collect()
};
gander_core::table::sort_order(&rows, &mut order, sort);
let mut slots: Vec<Option<Row>> = rows.into_iter().map(Some).collect();
Ok(order.into_iter().filter_map(|i| slots[i].take()).collect())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
fn sample_once(
_sampler: &mut gander_core::Sampler,
_sort: Sort,
_filter: Option<&Filter>,
) -> std::io::Result<Vec<Row>> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no backend for this platform",
))
}
fn write_text(
out: &mut impl std::io::Write,
rows: &[Row],
limit: Option<usize>,
clk_tck: u64,
) -> std::io::Result<()> {
let take = limit.unwrap_or(rows.len()).min(rows.len());
writeln!(
out,
"{:>7} {:>6} {:>5} {:>5} {:>8} {:>8} : COMMAND",
"PID", "CPU%", "MEM%", "NI", "RES", "TIME"
)?;
for row in rows.iter().take(take) {
let command = if row.is_thread {
format!("{{{}}} {}", row.comm.as_str(), row.display_command())
} else {
row.display_command().to_string()
};
writeln!(
out,
"{:>7} {:>6} {:>5} {:>5} {:>8} {:>8} : {}",
row.pid,
format::percent_human(row.percent_cpu),
format::percent_human(row.percent_mem),
row.nice,
format::bytes_human(row.res),
format::cpu_time_human(row.time_ticks, clk_tck),
command
)?;
}
Ok(())
}
fn write_json(
out: &mut impl std::io::Write,
rows: &[Row],
limit: Option<usize>,
_config: &Config,
) -> std::io::Result<()> {
let take = limit.unwrap_or(rows.len()).min(rows.len());
writeln!(out, "[")?;
for (i, row) in rows.iter().take(take).enumerate() {
let comma = if i + 1 == take { "" } else { "," };
writeln!(
out,
" {{\"pid\":{},\"ppid\":{},\"uid\":{},\"comm\":{},\"state\":\"{}\",\
\"cpu_percent\":{:.2},\"mem_percent\":{:.2},\"rss\":{},\"vsize\":{},\
\"nice\":{},\"priority\":{},\"threads\":{},\"elapsed\":{},\"is_thread\":{},\
\"identity\":{},\"cmdline\":{},\"memory\":{}}}{}",
row.pid,
row.ppid,
row.uid,
json_string(row.comm.as_str()),
row.state as char,
row.percent_cpu,
row.percent_mem,
row.res,
row.virt,
row.nice,
row.priority,
row.nlwp,
row.elapsed,
row.is_thread,
row.identity(),
json_string(row.display_command()),
memory_json(row),
comma
)?;
}
writeln!(out, "]")?;
Ok(())
}
#[cfg(target_os = "linux")]
fn memory_json(row: &Row) -> String {
let opt = |v: Option<u64>| match v {
Some(n) => n.to_string(),
None => "null".to_string(),
};
let fopt = |v: Option<f32>| match v {
Some(p) => format!("{p:.2}"),
None => "null".to_string(),
};
format!(
concat!(
"{{\"pss\":{},\"epss\":{},\"private\":{},\"swap\":{},\"gpu_percent\":{},",
"\"gpu_ns\":{},\"cpu_delay_percent\":{},\"io_delay_percent\":{},",
"\"swap_delay_percent\":{}}}"
),
opt(row.pss),
opt(row.epss),
opt(row.private),
opt(row.swap_bytes),
fopt(row.gpu_percent),
opt(row.gpu_ns),
fopt(row.cpu_delay_percent),
fopt(row.io_delay_percent),
fopt(row.swap_delay_percent)
)
}
#[cfg(not(target_os = "linux"))]
fn memory_json(_row: &Row) -> String {
"null".to_string()
}
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
use gander_core::row::Row;
fn row(pid: u32, comm: &str, cpu: f32) -> Row {
let mut r = Row::default_for_test(pid);
r.comm.try_push_str(comm).unwrap();
r.percent_cpu = cpu;
r
}
#[cfg(not(target_os = "linux"))]
#[test]
fn linux_only_fields_are_recognised_as_such() {
for name in [
"M_PSS",
"M_EPSS",
"M_PRIVATE",
"M_SWAP",
"GPU%",
"GPU_TIME",
"CPUD%",
"IOD%",
"SWPD%",
] {
assert!(is_linux_only_field(name), "{name} should be Linux-only");
assert_eq!(
Field::from_name(name),
None,
"{name} must not resolve on this platform"
);
}
assert!(!is_linux_only_field("BOGUS"));
assert!(!is_linux_only_field("PERCENT_MEM"));
}
#[cfg(target_os = "linux")]
#[test]
fn no_field_is_linux_only_on_linux() {
for name in [
"M_PSS",
"M_EPSS",
"M_PRIVATE",
"M_SWAP",
"GPU%",
"GPU_TIME",
"CPUD%",
"IOD%",
"SWPD%",
] {
assert!(!is_linux_only_field(name));
assert!(
Field::from_name(name).is_some(),
"{name} must resolve on Linux"
);
}
}
#[test]
fn json_escaping() {
assert_eq!(json_string("plain"), "\"plain\"");
assert_eq!(json_string("a\"b"), "\"a\\\"b\"");
assert_eq!(json_string("a\\b"), "\"a\\\\b\"");
assert_eq!(json_string("a\nb"), "\"a\\nb\"");
assert_eq!(json_string("a\u{1}b"), "\"a\\u0001b\"");
}
#[test]
fn text_output_respects_limit() {
let rows = vec![row(1, "a", 1.0), row(2, "b", 2.0), row(3, "c", 3.0)];
let mut buf = Vec::new();
write_text(&mut buf, &rows, Some(2), 100).unwrap();
let text = String::from_utf8(buf).unwrap();
assert_eq!(text.lines().count(), 3, "header plus two rows");
}
#[test]
fn json_output_is_well_formed() {
let rows = vec![row(1, "init", 0.5), row(2, "bash", 1.5)];
let mut buf = Vec::new();
write_json(&mut buf, &rows, None, &Config::default()).unwrap();
let text = String::from_utf8(buf).unwrap();
assert!(text.starts_with('['));
assert!(text.trim_end().ends_with(']'));
assert_eq!(text.matches("{\"pid\"").count(), 2);
assert_eq!(text.matches("},").count(), 1, "comma only between entries");
}
#[test]
fn empty_output_is_empty_array() {
let mut buf = Vec::new();
write_json(&mut buf, &[], None, &Config::default()).unwrap();
assert_eq!(String::from_utf8(buf).unwrap().trim(), "[\n]");
}
}