Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .github/assets/rust-cli-sysinfo_help.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/assets/rust-cli-sysinfo_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/assets/rust-cli-sysinfo_tokens.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/assets/rust-cli-sysinfo_tokens_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ This will result in the following output:

Want to create a new service that doesn't yet exist in RCS?

RCS follows the [Open–Closed Principle (OCP)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) where every service is implemented within its own Rust source file and automatically registers with the utility at compile time. No need to edit or modify existing source files: just add a separate new service file and rebuild the project.
RCS follows the [Open–Closed Principle (OCP)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) where every service is implemented within its own Rust source file and automatically registers with the utility at compile time. No need to edit or modify existing source files: just add a separate new service file and rebuild the project.

To create a new RCS service:

Expand Down Expand Up @@ -170,7 +170,7 @@ Nice!

RCS is designed to remain lean and fast, but some goals for upcoming releases include:

- [x] Streamline modular service generation patterns (Zero-touch compilation)
- [x] Streamline modular service generation patterns (following the [Open–Closed Principle (OCP)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle))

- [-] Additional metric services:
- Network interfaces (IP address added in v0.9.0)
Expand Down
10 changes: 4 additions & 6 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,13 @@ pub const SECS_PER_MIN: u64 = 60;
pub const SECS_PER_HOUR: u64 = 3_600;
pub const SECS_PER_DAY: u64 = 86_400;

// Size conversions for `format_size()`
pub const KB_PER_MB: u64 = 1_024;
// Size conversions
pub const KB_PER_GB: u64 = 1_048_576;
pub const KB_PER_TB: u64 = 1_073_741_824;

// Warning/Critical thresholds percentages
pub const CPU_WARN_PCT: f64 = 70.0;
pub const CPU_CRIT_PCT: f64 = 90.0;
pub const MEM_WARN_PCT: f64 = 75.0;
pub const MEM_WARN_PCT: f64 = 70.0;
pub const MEM_CRIT_PCT: f64 = 90.0;
pub const DISK_WARN_PCT: f64 = 80.0;
pub const DISK_CRIT_PCT: f64 = 95.0;
pub const DISK_WARN_PCT: f64 = 70.0;
pub const DISK_CRIT_PCT: f64 = 90.0;
30 changes: 1 addition & 29 deletions src/presentation/format.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use super::colors::Colors;
use crate::constants::{
INDENT, KB_PER_GB, KB_PER_MB, KB_PER_TB, LABEL_WIDTH, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN,
};
use crate::constants::{INDENT, LABEL_WIDTH, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN};

/// `Threshold` controls value-based color thresholds for utility rows
///
Expand Down Expand Up @@ -59,32 +57,6 @@ pub fn format_uptime(seconds: u64) -> String {
format!("{days:03}d:{hours:02}h:{mins:02}m:{secs:02}s")
}

/// `format_size()` formats a size given in kilobytes as a human-readable string with a T/G/M/K
/// suffix
///
#[must_use]
pub fn format_size(kb: u64) -> String {
// Casting integer size constants to f64 for fractional division (precision loss possible)
#[allow(clippy::cast_precision_loss)]
const UNITS: &[(f64, &str)] = &[
(KB_PER_TB as f64, "T"),
(KB_PER_GB as f64, "G"),
(KB_PER_MB as f64, "M"),
];

#[allow(clippy::cast_precision_loss)]
let k = kb as f64;

// Check for any unit prefix
for &(threshold, suffix) in UNITS {
if k >= threshold {
return format!("{:.1}{suffix}", k / threshold);
}
}

format!("{kb}K")
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
36 changes: 24 additions & 12 deletions src/services/disk.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use std::path::{Path, PathBuf};

use super::prelude::*;
use crate::constants::{DISK_CRIT_PCT, DISK_WARN_PCT};
use crate::presentation::format::format_size;
use crate::constants::{DISK_CRIT_PCT, DISK_WARN_PCT, KB_PER_GB};

/// `DiskInfo` contains disk usage metrics for a single mount point
#[derive(Default, Debug)]
Expand Down Expand Up @@ -108,20 +107,34 @@ impl Service for DiskService {
})
}

/// `render()` renders disk usage as a percentage with used/total sizes
/// `render()` renders disk usage as a percentage with used/total sizes in GiB
///
fn render(&self, disk: &Self::Data) -> Result<RenderedRow, AppError> {
let (value, threshold) = if disk.total_kb == 0 {
// Oh wow! A real disk was resolved, but reporting zero total space which could
// conceivably occur
("n/a".to_string(), Threshold::None)
} else {
// Convert to tenths of GiB using integer arithmetic to avoid casting large
// integers to f64 and triggering potential precision-loss
let used_tenths =
(u128::from(disk.used_kb) * 10 + u128::from(KB_PER_GB) / 2) / u128::from(KB_PER_GB);
let total_tenths = (u128::from(disk.total_kb) * 10 + u128::from(KB_PER_GB) / 2)
/ u128::from(KB_PER_GB);

let used_whole = used_tenths / 10;
let used_frac = used_tenths % 10;
let total_whole = total_tenths / 10;
let total_frac = total_tenths % 10;

// display() handles formatting potential non-UTF-8 characters
let text = format!(
"{:.1}% ({}/{}) of {}",
"{:.1}% ({}.{}/{}.{} GiB) of {}",
disk.pct,
format_size(disk.used_kb),
format_size(disk.total_kb),
used_whole,
used_frac,
total_whole,
total_frac,
self.mount.display()
);
(
Expand Down Expand Up @@ -252,15 +265,14 @@ mod tests {
mount: PathBuf::from("/anywhere"),
};
let data = DiskInfo {
total_kb: 1_000_000,
used_kb: 850_000,
pct: 85.0,
total_kb: 485_709_824,
used_kb: 199_019_724,
pct: 41.0,
};

let row = svc.render(&data).unwrap();
assert!(row.value.contains("85.0%"));
assert!(row.value.contains("/anywhere"));
assert_is_check_threshold_matching(&row.threshold, 85.0, DISK_WARN_PCT, DISK_CRIT_PCT);
assert_eq!(row.value, "41.0% (189.8/463.2 GiB) of /anywhere");
assert_is_check_threshold_matching(&row.threshold, 41.0, DISK_WARN_PCT, DISK_CRIT_PCT);
}

/// `picks_longest_enclosing_mount()` asserts the closest (longest-prefix) mount wins over a
Expand Down
55 changes: 49 additions & 6 deletions src/services/memory.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::prelude::*;
use crate::constants::{MEM_CRIT_PCT, MEM_WARN_PCT};
use crate::constants::{KB_PER_GB, MEM_CRIT_PCT, MEM_WARN_PCT};

/// `MemInfo` contains memory usage metrics
///
Expand Down Expand Up @@ -36,15 +36,27 @@ impl Service for MemoryService {
Ok(MemInfo { total, used, pct })
}

/// `render()` renders memory usage as a percentage with used/total in MB and threshold-based
/// `render()` renders memory usage as a percentage with used/total in GiB and threshold-based
/// color coding
///
fn render(&self, mem: &Self::Data) -> Result<RenderedRow, AppError> {
// Convert to tenths of GiB using integer arithmetic to avoid casting large
// integers to f64 and triggering potential precision-loss
let used_tenths =
(u128::from(mem.used) * 10 + u128::from(KB_PER_GB) / 2) / u128::from(KB_PER_GB);
let total_tenths =
(u128::from(mem.total) * 10 + u128::from(KB_PER_GB) / 2) / u128::from(KB_PER_GB);

// Split into whole and fractional (tenths) parts and format without
// creating f64 values for the sizes.
let used_whole = used_tenths / 10;
let used_frac = used_tenths % 10;
let total_whole = total_tenths / 10;
let total_frac = total_tenths % 10;

let mem_str = format!(
"{:.1}% ({}M/{}M)",
mem.pct,
mem.used / 1024,
mem.total / 1024
"{:.1}% ({}.{}/{}.{} GiB)",
mem.pct, used_whole, used_frac, total_whole, total_frac
);

Ok(RenderedRow {
Expand Down Expand Up @@ -72,3 +84,34 @@ pub fn descriptor(_ctx: &ServiceContext) -> (ServiceMeta, Box<dyn ErasedService>
Box::new(MemoryService),
)
}

#[cfg(test)]
mod tests {
use super::*;

/// `collect_returns_ok_and_nonzero_total()` asserts that memory collection succeeds and
/// returns non-zero total memory on a running system
///
#[test]
fn collect_returns_ok_and_nonzero_total() {
let result = MemoryService.collect();
assert!(result.is_ok());
let mem = result.unwrap();
assert!(mem.total > 0);
}

/// `render_formats_in_gib()` asserts that rendering memory usage formats used and total
/// in GiB with one decimal place
///
#[test]
fn render_formats_in_gib() {
let mem = MemInfo {
total: 28_351_488, // ~27.0 GiB
used: 4_346_880, // ~4.145 GiB -> 4.1 GiB
pct: 15.3,
};

let row = MemoryService.render(&mem).unwrap();
assert_eq!(row.value, "15.3% (4.1/27.0 GiB)");
}
}