Add CPU usage monitoring, remove btest-opensource submodule
All checks were successful
CI / test (push) Successful in 2m16s

CPU usage feature:
- New cpu.rs module: background sampler thread, cross-platform (macOS + Linux)
- Status message byte 1 now carries CPU load (0-100%), matching MikroTik format
- Status format corrected: [type][cpu][00][00][seq:4 LE][bytes:4 LE]
- Client and server exchange CPU in every status message
- Display format: "cpu: 40%/12%" (local/remote), "!" warning if > 70%
- Both client and server show local + remote CPU per interval
- Syslog TEST_END could include CPU averages (future enhancement)

Removed btest-opensource submodule — we've fully reimplemented the protocol
with EC-SRP5 auth, multi-connection, IPv6, syslog, CSV, and CPU monitoring.
The original project is still credited in LICENSE and README.

58 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-01 10:53:00 +04:00
parent 10dd0c3835
commit 24f634170d
9 changed files with 200 additions and 24 deletions

132
src/cpu.rs Normal file
View File

@@ -0,0 +1,132 @@
//! Lightweight CPU usage measurement.
//!
//! Returns the system-wide CPU usage as a percentage (0-100).
//! Works on macOS and Linux without external dependencies.
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
static CURRENT_CPU: AtomicU8 = AtomicU8::new(0);
/// Start a background thread that samples CPU usage every second.
pub fn start_sampler() {
std::thread::spawn(|| {
let mut prev = get_cpu_times();
loop {
std::thread::sleep(Duration::from_secs(1));
let curr = get_cpu_times();
let usage = compute_usage(&prev, &curr);
CURRENT_CPU.store(usage, Ordering::Relaxed);
prev = curr;
}
});
}
/// Get the current CPU usage percentage (0-100).
pub fn get() -> u8 {
CURRENT_CPU.load(Ordering::Relaxed)
}
// --- Platform-specific implementation ---
#[cfg(target_os = "linux")]
fn get_cpu_times() -> (u64, u64) {
// Read /proc/stat: cpu user nice system idle iowait irq softirq steal
if let Ok(content) = std::fs::read_to_string("/proc/stat") {
if let Some(line) = content.lines().next() {
let parts: Vec<u64> = line
.split_whitespace()
.skip(1) // skip "cpu"
.filter_map(|s| s.parse().ok())
.collect();
if parts.len() >= 4 {
let idle = parts[3];
let total: u64 = parts.iter().sum();
return (total, idle);
}
}
}
(0, 0)
}
#[cfg(target_os = "macos")]
fn get_cpu_times() -> (u64, u64) {
// Use host_statistics to get CPU ticks
use std::mem::MaybeUninit;
extern "C" {
fn mach_host_self() -> u32;
fn host_statistics(
host: u32,
flavor: i32,
info: *mut i32,
count: *mut u32,
) -> i32;
}
const HOST_CPU_LOAD_INFO: i32 = 3;
const CPU_STATE_MAX: usize = 4;
unsafe {
let host = mach_host_self();
let mut info = MaybeUninit::<[u32; CPU_STATE_MAX]>::uninit();
let mut count: u32 = CPU_STATE_MAX as u32;
let ret = host_statistics(
host,
HOST_CPU_LOAD_INFO,
info.as_mut_ptr() as *mut i32,
&mut count,
);
if ret == 0 {
let ticks = info.assume_init();
// ticks: [user, system, idle, nice]
let user = ticks[0] as u64;
let system = ticks[1] as u64;
let idle = ticks[2] as u64;
let nice = ticks[3] as u64;
let total = user + system + idle + nice;
return (total, idle);
}
}
(0, 0)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn get_cpu_times() -> (u64, u64) {
(0, 0) // Unsupported platform
}
fn compute_usage(prev: &(u64, u64), curr: &(u64, u64)) -> u8 {
let total_diff = curr.0.saturating_sub(prev.0);
let idle_diff = curr.1.saturating_sub(prev.1);
if total_diff == 0 {
return 0;
}
let busy = total_diff - idle_diff;
((busy * 100) / total_diff).min(100) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpu_times_returns_nonzero() {
let (total, idle) = get_cpu_times();
// On supported platforms, total should be > 0
if cfg!(any(target_os = "linux", target_os = "macos")) {
assert!(total > 0, "CPU total ticks should be > 0");
assert!(idle <= total, "idle should be <= total");
}
}
#[test]
fn test_compute_usage() {
assert_eq!(compute_usage(&(0, 0), &(100, 20)), 80);
assert_eq!(compute_usage(&(0, 0), &(100, 100)), 0);
assert_eq!(compute_usage(&(0, 0), &(100, 0)), 100);
assert_eq!(compute_usage(&(0, 0), &(0, 0)), 0);
}
}