Skip to main content

mimick/
runtime_env.rs

1//! Provides best-effort checks of system conditions to determine when uploads should be deferred.
2//!
3//! Reads `/sys/class/power_supply` to detect battery state and parses
4//! NetworkManager D-Bus properties to identify metered connections. All
5//! checks are non-fatal: if a sysfs path is missing or D-Bus is unavailable,
6//! the condition is assumed to be clear.
7
8use std::fs;
9use std::path::Path;
10use std::process::Command;
11
12/// Returns true if the current primary network connection is metered.
13pub fn is_metered_connection() -> bool {
14    let output = match Command::new("nmcli")
15        .args([
16            "-t",
17            "-f",
18            "GENERAL.METERED",
19            "connection",
20            "show",
21            "--active",
22        ])
23        .output()
24    {
25        Ok(output) if output.status.success() => output,
26        _ => return false,
27    };
28
29    is_metered_connection_from_nmcli_output(&String::from_utf8_lossy(&output.stdout))
30}
31
32/// Returns true if the system appears to be running on battery power.
33pub fn is_on_battery_power() -> bool {
34    let power_supply_root = Path::new("/sys/class/power_supply");
35    let entries = match fs::read_dir(power_supply_root) {
36        Ok(entries) => entries,
37        Err(_) => return false,
38    };
39
40    let mut statuses = Vec::new();
41
42    for entry in entries.flatten() {
43        let supply_path = entry.path();
44        let supply_type = fs::read_to_string(supply_path.join("type"))
45            .ok()
46            .map(|value| value.trim().to_string());
47        let online = fs::read_to_string(supply_path.join("online"))
48            .ok()
49            .map(|value| value.trim() == "1");
50        let status = fs::read_to_string(supply_path.join("status"))
51            .ok()
52            .map(|value| value.trim().to_ascii_lowercase());
53        statuses.push((supply_type, online, status));
54    }
55
56    is_on_battery_power_from_statuses(&statuses)
57}
58
59/// Check if the nmcli output string indicates a metered network connection.
60fn is_metered_connection_from_nmcli_output(stdout: &str) -> bool {
61    let stdout = stdout.to_ascii_lowercase();
62    stdout.contains("yes") || stdout.contains("guessed-yes")
63}
64
65/// Determine if running on battery based on parsed power supply types and online statuses.
66fn is_on_battery_power_from_statuses(
67    statuses: &[(Option<String>, Option<bool>, Option<String>)],
68) -> bool {
69    let mut found_battery = false;
70    let mut mains_online = false;
71
72    for (supply_type, online, status) in statuses {
73        match supply_type.as_deref() {
74            Some("Mains") | Some("USB") | Some("USB_C") if online.unwrap_or(false) => {
75                mains_online = true;
76            }
77            Some("Battery") => {
78                found_battery = true;
79                let status = status.as_deref().unwrap_or_default();
80                if status == "charging" || status == "full" {
81                    return false;
82                }
83            }
84            _ => {}
85        }
86    }
87
88    found_battery && !mains_online
89}
90
91#[cfg(test)]
92mod tests {
93    use super::{
94        is_metered_connection, is_metered_connection_from_nmcli_output, is_on_battery_power,
95        is_on_battery_power_from_statuses,
96    };
97
98    #[test]
99    fn runtime_checks_are_safe() {
100        let _ = is_metered_connection();
101        let _ = is_on_battery_power();
102    }
103
104    #[test]
105    fn test_metered_connection_parser() {
106        assert!(is_metered_connection_from_nmcli_output(
107            "GENERAL.METERED:yes\n"
108        ));
109        assert!(is_metered_connection_from_nmcli_output(
110            "GENERAL.METERED:guessed-yes\n"
111        ));
112        assert!(!is_metered_connection_from_nmcli_output(
113            "GENERAL.METERED:no\n"
114        ));
115    }
116
117    #[test]
118    fn test_battery_power_detection_from_statuses() {
119        assert!(is_on_battery_power_from_statuses(&[
120            (Some("Battery".into()), None, Some("discharging".into())),
121            (Some("Mains".into()), Some(false), None),
122        ]));
123
124        assert!(!is_on_battery_power_from_statuses(&[
125            (Some("Battery".into()), None, Some("charging".into())),
126            (Some("Mains".into()), Some(true), None),
127        ]));
128
129        assert!(!is_on_battery_power_from_statuses(&[(
130            Some("Mains".into()),
131            Some(true),
132            None,
133        )]));
134    }
135}