Skip to main content

lock_guard/
lib.rs

1use std::{
2    ffi::OsStr,
3    fs::File,
4    io::Read,
5    path::PathBuf,
6    process::Command,
7    thread,
8    time::{Duration, Instant},
9};
10
11use log::{debug, error, info, warn};
12use sysinfo::{ProcessRefreshKind, RefreshKind, System};
13
14pub enum PowerState {
15    /// Plugged in
16    Charging,
17    /// Not plugged in
18    Discharging,
19}
20
21impl PowerState {
22    /// Checks the status for a battery. If the battery could not be checked, then it returns PowerState::Discharging.
23    ///
24    /// Supported statuses:
25    ///  - "Charging" - Plugged in.
26    ///  - "Not Charging" - Plugged in, but not charging.
27    ///  - "Discharging" - Not plugged in.
28    fn current(battery_name: &String) -> Self {
29        let battery_path = PathBuf::from(format!("/sys/class/power_supply/{battery_name}/status"));
30
31        let mut battery_status = format!("Failed to read from {battery_path:?}");
32        {
33            let _ = File::open(battery_path).and_then(|mut x| {
34                battery_status.clear();
35                x.read_to_string(&mut battery_status)
36            });
37        }
38
39        let trimmed_status = battery_status.trim().to_lowercase();
40        let trimmed_status = trimmed_status.as_str();
41
42        match trimmed_status {
43            "charging" => PowerState::Charging,
44            "full" => PowerState::Charging,
45            "not charging" => PowerState::Charging,
46            "discharging" => PowerState::Discharging,
47            _ => {
48                warn!("Powerstate: {trimmed_status} unknown. Returning Discharging");
49                PowerState::Discharging
50            }
51        }
52    }
53}
54
55pub struct LockWatcher {
56    pub lock_process_names: Vec<String>,
57    pub battery_id: String,
58    pub polling_rate: Duration,
59    pub time_until_shutdown: Duration,
60}
61
62impl LockWatcher {
63    /// Check if any of the lock processes are running
64    fn system_locked(&self) -> bool {
65        let system = System::new_with_specifics(
66            RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
67        );
68
69        for process_name in &self.lock_process_names {
70            if system
71                .processes_by_name(OsStr::new(process_name))
72                .next()
73                .is_some()
74            {
75                debug!("{process_name} is running");
76                return true;
77            }
78        }
79
80        false
81    }
82
83    /// Get the current power state
84    fn get_power_state(&self) -> PowerState {
85        PowerState::current(&self.battery_id)
86    }
87
88    pub fn loop_forever(&self) -> ! {
89        let mut past_power_state: PowerState = self.get_power_state();
90        let mut started_discharging_at: Option<Instant> = None;
91
92        loop {
93            thread::sleep(self.polling_rate);
94
95            if !self.system_locked() {
96                past_power_state = self.get_power_state();
97                continue;
98            }
99
100            match (&past_power_state, self.get_power_state()) {
101                // NOOP
102                (PowerState::Charging, PowerState::Charging) => {
103                    started_discharging_at = None;
104                }
105                // Device unplugged
106                (PowerState::Charging, PowerState::Discharging) => {
107                    started_discharging_at = Some(Instant::now());
108                    past_power_state = PowerState::Discharging;
109                    info!("Disconnected")
110                }
111                // Device plugged in
112                (PowerState::Discharging, PowerState::Charging) => {
113                    started_discharging_at = None;
114                    past_power_state = PowerState::Charging;
115                    info!("Plugged in")
116                }
117                // Noop
118                (PowerState::Discharging, PowerState::Discharging) => {}
119            }
120
121            if let Some(discharging_since) = started_discharging_at {
122                let elapsed = discharging_since.elapsed();
123                let until_shutdown = self.time_until_shutdown.as_secs_f32() - elapsed.as_secs_f32();
124                info!("Time until shutdown: {:.2} seconds", until_shutdown);
125
126                if elapsed > self.time_until_shutdown {
127                    info!("Shutting down");
128
129                    match Command::new("shutdown").arg("now").output() {
130                        Ok(_) => {}
131                        Err(v) => error!("Could not shut down: {:?}", v),
132                    }
133
134                    thread::sleep(Duration::from_secs_f32(2.0));
135                }
136            }
137        }
138    }
139}