diff --git a/src/uu/free/src/free.rs b/src/uu/free/src/free.rs index e45875b..ecb2f65 100644 --- a/src/uu/free/src/free.rs +++ b/src/uu/free/src/free.rs @@ -24,14 +24,18 @@ use std::env; use std::fs; #[cfg(target_os = "linux")] use std::io::Error; +use std::ops::Mul; use std::process; +use std::thread::sleep; +use std::time::Duration; +use std::u64; use uucore::{error::UResult, format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("free.md"); const USAGE: &str = help_usage!("free.md"); /// The unit of number is [UnitMultiplier::Bytes] -#[derive(Default)] +#[derive(Default, Clone)] struct MemInfo { total: u64, free: u64, @@ -43,10 +47,17 @@ struct MemInfo { swap_free: u64, swap_used: u64, reclaimable: u64, + low_total: u64, + low_free: u64, + high_total: u64, + high_free: u64, + commit_limit: u64, + committed: u64, } #[cfg(target_os = "linux")] fn parse_meminfo() -> Result { + // kernel docs: https://www.kernel.org/doc/html/latest/filesystems/proc.html#meminfo let contents = fs::read_to_string("/proc/meminfo")?; let mut mem_info = MemInfo::default(); @@ -63,10 +74,27 @@ fn parse_meminfo() -> Result { "SwapTotal" => mem_info.swap_total = parsed_value, "SwapFree" => mem_info.swap_free = parsed_value, "SReclaimable" => mem_info.reclaimable = parsed_value, + "LowTotal" => mem_info.low_total = parsed_value, + "LowFree" => mem_info.low_free = parsed_value, + "HighTotal" => mem_info.high_total = parsed_value, + "HighFree" => mem_info.high_free = parsed_value, + "CommitLimit" => mem_info.commit_limit = parsed_value, + "Committed_AS" => mem_info.committed = parsed_value, _ => {} } } } + // as far as i understand the kernel doc everything that is not highmem (out of all the memory) is lowmem + // from kernel doc: "Highmem is all memory above ~860MB of physical memory." + // it would be better to implement this via optionals, etc. but that would require a refactor so lets not do that right now + + if mem_info.low_total == u64::default() { + mem_info.low_total = mem_info.total - mem_info.high_total; + } + + if mem_info.low_free == u64::default() { + mem_info.low_free = mem_info.free - mem_info.high_free; + } mem_info.swap_used = mem_info.swap_total - mem_info.swap_free; @@ -90,6 +118,12 @@ fn parse_meminfo() -> Result> { swap_free: sys.free_swap(), swap_used: sys.total_swap().saturating_sub(sys.free_swap()), reclaimable: 0, + low_total: 0, + low_free: 0, + high_total: 0, + high_free: 0, + commit_limit: 0, + committed: 0, }; Ok(mem_info) @@ -101,88 +135,160 @@ fn parse_meminfo() -> Result> { Ok(MemInfo::default()) } +// print total - used - free combo that is used for everything except memory for now +// free can be negative if the memory is overcommitted so it has to be signed +fn tuf_combo(name: &str, total: u64, used: u64, free: i128, f: F) +where + F: Fn(u64) -> String, +{ + // ugly hack to convert negative values + let free_str: String = if free < 0 { + "-".to_owned() + &f((-free) as u64) + } else { + f(free as u64) + }; + + println!("{:8}{:>12}{:>12}{:>12}", name, f(total), f(used), free_str); +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let wide = matches.get_flag("wide"); + let wide = matches.get_flag("wide"); let human = matches.get_flag("human"); - let si = matches.get_flag("si"); + let total = matches.get_flag("total"); + let lohi = matches.get_flag("lohi"); + let count_flag = matches.get_one("count"); + let mut count: u64 = count_flag.unwrap_or(&1_u64).to_owned(); + let seconds_flag = matches.get_one("seconds"); + let seconds: f64 = seconds_flag.unwrap_or(&1.0_f64).to_owned(); + let committed = matches.get_flag("committed"); + let one_line = matches.get_flag("line"); + let dur = Duration::from_nanos(seconds.mul(1_000_000_000.0).round() as u64); let convert = detect_unit(&matches); - match parse_meminfo() { - Ok(mem_info) => { - let buff_cache = if wide { - mem_info.buffers - } else { - mem_info.buffers + mem_info.cached - }; - let cache = if wide { mem_info.cached } else { 0 }; - let used = mem_info.total - mem_info.available; - - if wide { - wide_header(); - if human { - println!( - "{:8}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}", - "Mem:", - humanized(mem_info.total, si), - humanized(used, si), - humanized(mem_info.free, si), - humanized(mem_info.shared, si), - humanized(buff_cache, si), - humanized(cache + mem_info.reclaimable, si), - humanized(mem_info.available, si), - ) + let infinite: bool = count_flag.is_none() && seconds_flag.is_some(); + + while count > 0 || infinite { + // prevent underflow + if !infinite { + count -= 1; + } + match parse_meminfo() { + Ok(mem_info) => { + let buff_cache = if wide { + mem_info.buffers } else { + mem_info.buffers + mem_info.cached + }; + let cache = if wide { mem_info.cached } else { 0 }; + let used = mem_info.total - mem_info.available; + + // function that converts the number to the correct string + let n2s = |x| { + if human { + humanized(x, si) + } else { + convert(x).to_string() + } + }; + if one_line { println!( - "{:8}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}", - "Mem:", - convert(mem_info.total), - convert(used), - convert(mem_info.free), - convert(mem_info.shared), - convert(buff_cache), - convert(cache + mem_info.reclaimable), - convert(mem_info.available), - ) - } - } else { - header(); - if human { - println!( - "{:8}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}", - "Mem:", - humanized(mem_info.total, si), - humanized(used, si), - humanized(mem_info.free, si), - humanized(mem_info.shared, si), - humanized(buff_cache + mem_info.reclaimable, si), - humanized(mem_info.available, si), - ) + "{:8}{:>12} {:8}{:>12} {:8}{:>12} {:8}{:>12}", + "SwapUse", + n2s(mem_info.swap_used), + "CacheUse", + n2s(buff_cache + mem_info.reclaimable), + "MemUse", + n2s(used), + "MemFree", + n2s(mem_info.free) + ); } else { - println!( - "{:8}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}", - "Mem:", - convert(mem_info.total), - convert(used), - convert(mem_info.free), - convert(mem_info.shared), - convert(buff_cache + mem_info.reclaimable), - convert(mem_info.available), - ) + if wide { + wide_header(); + println!( + "{:8}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}", + "Mem:", + n2s(mem_info.total), + n2s(used), + n2s(mem_info.free), + n2s(mem_info.shared), + n2s(buff_cache), + n2s(cache + mem_info.reclaimable), + n2s(mem_info.available), + ); + } else { + header(); + println!( + "{:8}{:>12}{:>12}{:>12}{:>12}{:>12}{:>12}", + "Mem:", + n2s(mem_info.total), + n2s(used), + n2s(mem_info.free), + n2s(mem_info.shared), + n2s(buff_cache + mem_info.reclaimable), + n2s(mem_info.available), + ) + } + + if lohi { + tuf_combo( + "Low:", + mem_info.low_total, + mem_info.low_total - mem_info.low_free, + mem_info.low_free.into(), + n2s, + ); + tuf_combo( + "High:", + mem_info.high_total, + mem_info.high_total - mem_info.high_free, + mem_info.high_free.into(), + n2s, + ); + } + + tuf_combo( + "Swap:", + mem_info.swap_total, + mem_info.swap_used, + mem_info.swap_free.into(), + n2s, + ); + if total { + tuf_combo( + "Total:", + mem_info.total + mem_info.swap_total, + used + mem_info.swap_used, + (mem_info.free + mem_info.swap_free).into(), + n2s, + ); + } + + if committed { + tuf_combo( + "Comm:", + mem_info.commit_limit, + mem_info.committed, + (mem_info.commit_limit as i128) - (mem_info.committed as i128), + n2s, + ); + } } } - println!( - "{:8}{:>12}{:>12}{:>12}", - "Swap:", mem_info.swap_total, mem_info.swap_used, mem_info.swap_free - ); + Err(e) => { + eprintln!("free: failed to read memory info: {}", e); + process::exit(1); + } } - Err(e) => { - eprintln!("free: failed to read memory info: {}", e); - process::exit(1); + if count > 0 || infinite { + // the original free prints a newline everytime before waiting for the next round + println!(); + sleep(dur); } } @@ -200,20 +306,33 @@ pub fn uu_app() -> Command { "bytes", "kilo", "mega", "giga", "tera", "peta", "kibi", "mebi", "gibi", "tebi", "pebi", ])) .args([ - arg!(-b --bytes "show output in bytes").action(ArgAction::SetTrue), - arg!( --kilo "show output in kilobytes").action(ArgAction::SetFalse), - arg!( --mega "show output in megabytes").action(ArgAction::SetTrue), - arg!( --giga "show output in gigabytes").action(ArgAction::SetTrue), - arg!( --tera "show output in terabytes").action(ArgAction::SetTrue), - arg!( --peta "show output in petabytes").action(ArgAction::SetTrue), - arg!(-k --kibi "show output in kibibytes").action(ArgAction::SetTrue), - arg!(-m --mebi "show output in mebibytes").action(ArgAction::SetTrue), - arg!(-g --gibi "show output in gibibytes").action(ArgAction::SetTrue), - arg!( --tebi "show output in tebibytes").action(ArgAction::SetTrue), - arg!( --pebi "show output in pebibytes").action(ArgAction::SetTrue), - arg!(-h --human "show human-readable output").action(ArgAction::SetTrue), - arg!( --si "use powers of 1000 not 1024").action(ArgAction::SetFalse), - // arg!(-L --line "show output on a single line"), + arg!(-b --bytes "show output in bytes").action(ArgAction::SetTrue), + arg!( --kilo "show output in kilobytes").action(ArgAction::SetFalse), + arg!( --mega "show output in megabytes").action(ArgAction::SetTrue), + arg!( --giga "show output in gigabytes").action(ArgAction::SetTrue), + arg!( --tera "show output in terabytes").action(ArgAction::SetTrue), + arg!( --peta "show output in petabytes").action(ArgAction::SetTrue), + arg!(-k --kibi "show output in kibibytes").action(ArgAction::SetTrue), + arg!(-m --mebi "show output in mebibytes").action(ArgAction::SetTrue), + arg!(-g --gibi "show output in gibibytes").action(ArgAction::SetTrue), + arg!( --tebi "show output in tebibytes").action(ArgAction::SetTrue), + arg!( --pebi "show output in pebibytes").action(ArgAction::SetTrue), + arg!(-h --human "show human-readable output").action(ArgAction::SetTrue), + arg!( --si "use powers of 1000 not 1024").action(ArgAction::SetFalse), + arg!(-l --lohi "show detailed low and high memory statistics") + .action(ArgAction::SetTrue), + arg!(-t --total "show total for RAM + swap").action(ArgAction::SetTrue), + arg!(-v --committed "show committed memory and commit limit") + .action(ArgAction::SetTrue), + // accept 1 as well as 0.5, 0.55, ... + arg!(-s --seconds "repeat printing every N seconds") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(f64)), + // big int because predecesor accepts them as well (some scripts might have huge values as some sort of infinite) + arg!(-c --count "repeat printing N times, then exit") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(u64)), + arg!(-L --line "show output on a single line").action(ArgAction::SetTrue), ]) .arg( Arg::new("wide") diff --git a/tests/by-util/test_free.rs b/tests/by-util/test_free.rs index 90f5cd1..42ea8d4 100644 --- a/tests/by-util/test_free.rs +++ b/tests/by-util/test_free.rs @@ -9,6 +9,8 @@ use regex::Regex; use crate::common::util::TestScenario; +// TODO: make tests combineable (e.g. test --total --human) + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -27,6 +29,54 @@ fn test_free_wide() { assert!(!result.stdout_str().contains("buff/cache")); } +#[test] +fn test_free_total() { + let result = new_ucmd!().arg("-t").succeeds(); + assert_eq!(result.stdout_str().lines().count(), 4); + assert!(result + .stdout_str() + .lines() + .last() + .unwrap() + .starts_with("Total:")) +} + +#[test] +fn test_free_count() { + let result = new_ucmd!().args(&["-c", "2", "-s", "0"]).succeeds(); + assert_eq!(result.stdout_str().lines().count(), 7); +} + +#[test] +fn test_free_lohi() { + let result = new_ucmd!().arg("--lohi").succeeds(); + assert_eq!(result.stdout_str().lines().count(), 5); + let lines = result.stdout_str().lines().collect::>(); + assert!(lines[2].starts_with("Low:")); + assert!(lines[3].starts_with("High:")); +} + +#[test] +fn test_free_committed() { + let result = new_ucmd!().arg("-v").succeeds(); + assert_eq!(result.stdout_str().lines().count(), 4); + assert!(result + .stdout_str() + .lines() + .last() + .unwrap() + .starts_with("Comm:")) +} + +#[test] +fn test_free_always_one_line() { + // -L should ignore all other parameters and always print one line + let result = new_ucmd!().arg("-hltvwL").succeeds(); + let stdout = result.stdout_str().lines().collect::>(); + assert_eq!(stdout.len(), 1); + assert!(stdout[0].starts_with("SwapUse")); +} + #[test] fn test_free_column_format() { let re_head_str = r"^ {15}total {8}used {8}free {6}shared {2}buff/cache {3}available$"; diff --git a/tests/common/util.rs b/tests/common/util.rs index e2e9dc0..e361ce9 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -993,7 +993,7 @@ impl AtPath { pub fn relative_symlink_file(&self, original: &str, link: &str) { #[cfg(windows)] - let original = original.replace('/', &MAIN_SEPARATOR_STR); + let original = original.replace('/', MAIN_SEPARATOR_STR); log_info( "symlink", format!("{},{}", &original, &self.plus_as_string(link)), @@ -1015,7 +1015,7 @@ impl AtPath { pub fn relative_symlink_dir(&self, original: &str, link: &str) { #[cfg(windows)] - let original = original.replace('/', &MAIN_SEPARATOR_STR); + let original = original.replace('/', MAIN_SEPARATOR_STR); log_info( "symlink", format!("{},{}", &original, &self.plus_as_string(link)),