pub fn total_memory_mb() -> Option<u64> {
let content = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in content.lines() {
let Some(rest) = line.strip_prefix("MemTotal:") else {
continue;
};
let kb = rest.trim().trim_end_matches("kB").trim().parse::<u64>().ok()?;
return Some(kb / 1024);
}
None
}
pub fn is_on_battery() -> bool {
let Ok(entries) = std::fs::read_dir("/sys/class/power_supply") else {
return false;
};
for entry in entries.flatten() {
let status_path = entry.path().join("status");
if let Ok(status) = std::fs::read_to_string(&status_path) {
if status.trim() == "Discharging" {
return true;
}
}
}
false
}
pub fn process_memory_utilization() -> Option<f32> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let mut vmrss_kb: u64 = 0;
let mut vmsize_kb: u64 = 0;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
vmrss_kb = rest.trim().trim_end_matches("kB").trim().parse().unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("VmSize:") {
vmsize_kb = rest.trim().trim_end_matches("kB").trim().parse().unwrap_or(0);
}
}
if vmsize_kb == 0 {
return None;
}
Some((vmrss_kb as f32 / vmsize_kb as f32).clamp(0.0, 1.0))
}
pub fn process_cpu_utilization() -> Option<f32> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
let Some(rest) = line.strip_prefix("Threads:") else {
continue;
};
let threads = rest.trim().parse::<f32>().ok()?;
let cores = std::thread::available_parallelism().map(|n| n.get() as f32).unwrap_or(4.0);
return Some((threads / (cores * 2.0)).clamp(0.0, 1.0));
}
None
}
pub fn spawn_print_job(job_file: &std::path::Path) -> Result<(), String> {
let attempts: [&str; 2] = ["lpr", "lp"];
let mut last_error = String::from("no print spooler found");
let mut attempted = false;
for program in attempts {
match run_spooler(program, job_file) {
Ok(()) => return Ok(()),
Err(error) => {
if error.is_rejection() {
attempted = true;
}
last_error = error.to_string();
}
}
}
if attempted {
Err(last_error)
} else {
Err(format!(
"no print spooler found: none of the known spooler commands could be spawned \
({last_error}); install CUPS (`cups-client`) or a compatible `lpr` to print"
))
}
}
#[derive(Debug)]
enum SpoolerFailure {
Unavailable(String),
Rejected(String),
}
impl SpoolerFailure {
fn is_rejection(&self) -> bool {
matches!(self, Self::Rejected(_))
}
}
impl std::fmt::Display for SpoolerFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable(message) | Self::Rejected(message) => f.write_str(message),
}
}
}
fn run_spooler(program: &str, job_file: &std::path::Path) -> Result<(), SpoolerFailure> {
let output = std::process::Command::new(program).arg(job_file).output().map_err(|error| {
SpoolerFailure::Unavailable(format!(
"spooler command '{program}' could not be spawned: {error} (check that it is \
installed and on PATH)"
))
})?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr.trim();
let message = if stderr.is_empty() {
format!("{program}: exited with {}", output.status)
} else {
format!("{program}: failed: {stderr}")
};
Err(SpoolerFailure::Rejected(message))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(unix, windows))]
fn stand_in_spooler(
job: &std::path::Path,
record: &std::path::Path,
) -> (&'static str, &'static str, String) {
#[cfg(unix)]
{
let script = format!(
"sleep 0.2\ncat \"{}\" > \"{}\" 2>/dev/null || exit 1\n",
job.display(),
record.display()
);
("sh", "spool.sh", script)
}
#[cfg(windows)]
{
let script = format!(
"Dim fso, src, dst\n\
WScript.Sleep 200\n\
Set fso = CreateObject(\"Scripting.FileSystemObject\")\n\
Set src = fso.OpenTextFile(\"{}\", 1)\n\
Set dst = fso.CreateTextFile(\"{}\", True)\n\
dst.Write src.ReadAll\n\
dst.Close\n\
src.Close\n\
WScript.Quit 0\n",
job.display(),
record.display()
);
("cscript", "spool.vbs", script)
}
}
#[test]
fn probes_are_total() {
if let Some(mb) = total_memory_mb() {
assert!(mb > 0, "a readable /proc/meminfo must report a positive size");
}
if let Some(ratio) = process_memory_utilization() {
assert!((0.0..=1.0).contains(&ratio), "RSS/VmSize must be a fraction, got {ratio}");
}
if let Some(ratio) = process_cpu_utilization() {
assert!((0.0..=1.0).contains(&ratio), "thread ratio must be clamped, got {ratio}");
}
let _ = is_on_battery();
}
#[cfg(any(unix, windows))]
#[test]
fn print_job_waits_for_the_spooler_before_reading_back() {
use std::io::Write as _;
let dir = std::env::temp_dir().join(format!("rw_spool_probe_{}", std::process::id()));
if std::fs::create_dir_all(&dir).is_err() {
return;
}
let record = dir.join("read_back.txt");
let _ = std::fs::remove_file(&record);
let job = dir.join("job.txt");
if std::fs::write(&job, "page:1\n").is_err() {
return;
}
let (program, script_name, script) = stand_in_spooler(&job, &record);
let fake = dir.join(script_name);
if std::fs::File::create(&fake)
.and_then(|mut file| file.write_all(script.as_bytes()))
.is_err()
{
return;
}
let result = run_spooler(program, &fake);
let removed = std::fs::remove_file(&job);
let read_back = std::fs::read_to_string(&record).unwrap_or_default();
let _ = std::fs::remove_file(&fake);
assert!(result.is_ok(), "the stand-in spooler must report success, got {result:?}");
assert!(
removed.is_ok(),
"the caller must be able to delete the job file straight after submission: \
{removed:?}"
);
assert_eq!(
read_back.trim(),
"page:1",
"the spooler read the job file back as {read_back:?} — an empty value means \
`run_spooler` returned before the spooler had finished reading, so the \
caller deleted the file underneath it and nothing was printed"
);
}
#[test]
fn an_absent_spooler_is_reported_as_unavailable() {
let job = std::path::Path::new("/tmp/rw_probe_absent_job.txt");
let result = run_spooler("rw_definitely_not_a_spooler", job);
match result {
Err(failure) => assert!(
!failure.is_rejection(),
"an uninstalled program must not be reported as rejecting the job: {failure}"
),
Ok(()) => panic!("a non-existent spooler must not report success"),
}
}
#[test]
fn print_job_reports_failure_for_a_missing_spooler_or_file() {
let missing = std::path::Path::new("/nonexistent/rust_widgets_probe_job.txt");
let result = spawn_print_job(missing);
assert!(
result.is_err(),
"submitting a non-existent job file must not report success, got {result:?}"
);
}
}