systemd_wake/
lib.rs

1//! # systemd-wake
2//! 
3//! This is a utility library that uses systemd-run under the hood to schedule any [`Command`] to
4//! run at some future time. Allows for tasks to be scheduled and cancelled using custom systemd
5//! unit names as handles. Note that there are no guarantees about naming collisions from other
6//! programs. Be smart about choosing names.
7//!
8//! Requires the systemd-wake binary to be installed in order to work. Remember to install with
9//! `cargo install systemd-wake`.
10//!
11//! Use [`register()`] to schedule a command with systemd-run to wake at specificed time.
12//!
13//! Use [`deregister()`] to cancel timer.
14//!
15//! ### Example
16//! ```
17//! use systemd_wake::*;
18//!
19//! // one minute in the future
20//! let waketime = chrono::Local::now().naive_local() + chrono::Duration::minutes(1);
21//!
22//! // schedule a short beep
23//! let mut command = std::process::Command::new("play");
24//! command.args(vec!["-q","-n","synth","0.1","sin","880"]);
25//!
26//! // create unit handle
27//! let unit_name = UnitName::new("my-special-unit-name-123").unwrap();
28//!
29//! // register future beep
30//! systemd_wake::register(waketime,unit_name,command).unwrap();
31//!
32//! // check future beep
33//! systemd_wake::query_registration(unit_name).unwrap();
34//!
35//! // cancel future beep and retrieve command and deadline
36//! let (command, waketime) = systemd_wake::deregister(unit_name).unwrap();
37//! ```
38
39#![deny(missing_docs)]
40
41/// Command serialization.
42pub mod command;
43use command::{CommandConfig,CommandConfigError};
44
45use std::fmt::{Display,Formatter};
46use std::process::{Command,Output};
47
48use chrono::NaiveDateTime;
49use thiserror::Error;
50#[allow(unused_imports)]
51use tracing::{info,debug,warn,error,trace,Level};
52
53/// Wrapper struct for the name given to the systemd timer unit.
54#[derive(Copy,Clone)]
55pub struct UnitName<'a> {
56    name: &'a str,
57}
58
59impl<'a> UnitName<'a> {
60    /// Creates new TimerName and verifies that unit name meets constraints of being only
61    /// non-whitespace ASCII.
62    pub fn new(name: &'a str) -> Result<Self,UnitNameError> {
63        if !name.is_ascii() {
64            return Err(UnitNameError::NotAscii);
65        }
66        if name.contains(char::is_whitespace) {
67            return Err(UnitNameError::ContainsWhitespace);
68        }
69        Ok(Self { name })
70    }
71}
72
73impl AsRef<str> for UnitName<'_> {
74    fn as_ref(&self) -> &str {
75        self.name
76    }
77}
78
79impl Display for UnitName<'_> {
80    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
81        self.name.fmt(f)
82    }
83}
84
85/// Error struct for creating [`UnitName`].
86#[derive(Error,Debug)]
87#[allow(missing_docs)]
88pub enum UnitNameError {
89    #[error("UnitName must be ASCII")]
90    NotAscii,
91    #[error("UnitName cannot conatin whitespace")]
92    ContainsWhitespace,
93}
94
95/// Error struct for registration.
96#[derive(Error,Debug)]
97#[allow(missing_docs)]
98pub enum RegistrationError {
99    #[error("error querying timer status")]
100    Query(#[from] QueryError),
101    #[error("unit name is already in use")]
102    Duplicate,
103    #[error("error with registration command")]
104    Command(#[from] CommandError),
105}
106
107/// Calls systemd-run to register command to wake at specified time using provided name.
108pub fn register(event_time: NaiveDateTime, unit_name: UnitName, command: Command) -> Result<(),RegistrationError> {
109    debug!("registering timer");
110
111    if check_loaded(unit_name)? {
112        return Err(RegistrationError::Duplicate);
113    }
114
115    let unit_name = format!("--unit={}",unit_name);
116
117    let on_calendar = event_time.format("--on-calendar=%F %T").to_string();
118    debug!("timer set for {}",on_calendar);
119
120    let encoded_command = CommandConfig::encode(command).unwrap();
121
122    let mut systemd_command = Command::new("systemd-run");
123    systemd_command
124        .arg("--user")
125        .arg(unit_name)
126        .arg(on_calendar)
127        .arg("systemd-wake")
128        .arg(encoded_command);
129
130    debug!("running timer command: {:?}",systemd_command);
131    run_command(systemd_command)?;
132    Ok(())
133}
134
135/// Calls systemctl to deregister specified timer.
136pub fn deregister(unit_name: UnitName) -> Result<(Command,NaiveDateTime),RegistrationError> {
137    let (command, deadline) = query_registration(unit_name)?;
138
139    debug!("deregistering timer");
140
141    let timer_name = {
142        let mut name = unit_name.to_string();
143        name.push_str(".timer");
144        name
145    };
146
147    let mut systemd_command = Command::new("systemctl");
148    systemd_command
149        .arg("--user")
150        .arg("stop")
151        .arg(timer_name);
152
153    debug!("running stop timer command: {:?}",systemd_command);
154    run_command(systemd_command)?;
155    Ok((command,deadline))
156}
157
158/// Convenience function for changing scheduled waketime
159pub fn reschedule(unit_name: UnitName, waketime: NaiveDateTime) -> Result<(),RegistrationError> {
160    let (command, _) = deregister(unit_name)?;
161    register(waketime,unit_name,command)
162}
163
164fn extract_property(unit_name: UnitName, property: &str) -> Result<String,QueryError> {
165    let unit_name = {
166        let mut name = unit_name.to_string();
167        name.push_str(".timer");
168        name
169    };
170
171    let mut systemd_command = Command::new("systemctl");
172    systemd_command
173        .arg("--user")
174        .arg("show")
175        .arg(unit_name)
176        .arg(format!("--property={}",property));
177
178    let output = run_command(systemd_command)?;
179
180    match String::from_utf8(output.stdout) {
181        Ok(string) => {
182            if let Some(value) = string.strip_prefix(&format!("{}=",property)) {
183                return Ok(value.trim_end().to_owned())
184            } else {
185                return Err(QueryError::ParseError);
186            }
187        },
188        Err(_) => return Err(QueryError::ParseError),
189    }
190}
191
192fn check_loaded(unit_name: UnitName) -> Result<bool,QueryError> {
193    Ok(extract_property(unit_name,"LoadState")? == "loaded")
194}
195
196/// Returns registered command and wake up time for unit if it exists.
197pub fn query_registration(unit_name: UnitName) -> Result<(Command,NaiveDateTime),QueryError> {
198    debug!("querying registration");
199    // look for:
200    // LoadState
201    // Description
202    // TimersCalendar
203
204    if !check_loaded(unit_name)? {
205        return Err(QueryError::NotLoaded);
206    }
207
208    let desc = extract_property(unit_name, "Description")?;
209    let command = if let Some(splits) = desc.split_once(" ") {
210        CommandConfig::decode(splits.1)?
211    } else {
212        return Err(QueryError::ParseError);
213    };
214
215    let calendar = extract_property(unit_name, "TimersCalendar")?;
216    let datetime_str = calendar
217        .split_once("OnCalendar=").ok_or(QueryError::ParseError)?.1
218        .split_once(" ;").ok_or(QueryError::ParseError)?.0;
219
220    let datetime = match chrono::NaiveDateTime::parse_from_str(&datetime_str,"%Y-%m-%d %H:%M:%S") {
221        Ok(x) => x,
222        Err(_) => return Err(QueryError::ParseError),
223    };
224
225    Ok((command,datetime))
226
227}
228
229/// Error struct for querying task registration.
230#[derive(Error,Debug)]
231pub enum QueryError {
232    /// Error sending command to systemd
233    #[error("systemd command error")]
234    Command(#[from] CommandError),
235    /// Provided unit name is not loaded
236    #[error("unit with provided name not loaded")]
237    NotLoaded,
238    /// Error parsing systemd output
239    #[error("error parsing systemd output")]
240    ParseError,
241    /// Error decoding command
242    #[error("error decoding command")]
243    DecodeError(#[from] CommandConfigError),
244}
245
246/// Error struct for running a command. Wraps running with a non-success exit status as an error variant.
247#[derive(Error,Debug)]
248pub enum CommandError {
249    /// Error running the command
250    #[error("error running command")]
251    RunCommand(#[from] std::io::Error),
252    /// Command ran, but exited with failure status
253    #[error("command exited with failure status")]
254    CommandFailed(Output),
255}
256
257/// Helper function for running commands.
258pub fn run_command(mut command: Command) -> Result<Output,CommandError> {
259    match command.output() {
260        Ok(output) => {
261            if output.status.success() {
262                Ok(output)
263            } else {
264                Err(CommandError::CommandFailed(output))
265            }
266        },
267        Err(e) => {
268            Err(CommandError::RunCommand(e))
269        }
270    }
271}
272
273#[cfg(test)]
274mod test {
275    use super::*;
276
277    #[test]
278    fn test_beep() {
279        // one minute in the future
280        let waketime = chrono::Local::now().naive_local() + chrono::Duration::minutes(1);
281
282        // schedule a short beep
283        let mut command = std::process::Command::new("play");
284        command.args(vec!["-q","-n","synth","0.1","sin","880"]);
285
286        // create unit handle
287        let unit_name = UnitName::new("my-special-unit-name-123").unwrap();
288
289        // register future beep
290        register(waketime,unit_name,command).unwrap();
291
292        // check future beep
293        let (_command, _datetime) = query_registration(unit_name).unwrap();
294
295        // cancel future beep
296        let (_command, _datetime) = deregister(unit_name).unwrap();
297    }
298}