1#![deny(missing_docs)]
40
41pub 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#[derive(Copy,Clone)]
55pub struct UnitName<'a> {
56 name: &'a str,
57}
58
59impl<'a> UnitName<'a> {
60 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#[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#[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
107pub 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
135pub 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
158pub 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
196pub fn query_registration(unit_name: UnitName) -> Result<(Command,NaiveDateTime),QueryError> {
198 debug!("querying registration");
199 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#[derive(Error,Debug)]
231pub enum QueryError {
232 #[error("systemd command error")]
234 Command(#[from] CommandError),
235 #[error("unit with provided name not loaded")]
237 NotLoaded,
238 #[error("error parsing systemd output")]
240 ParseError,
241 #[error("error decoding command")]
243 DecodeError(#[from] CommandConfigError),
244}
245
246#[derive(Error,Debug)]
248pub enum CommandError {
249 #[error("error running command")]
251 RunCommand(#[from] std::io::Error),
252 #[error("command exited with failure status")]
254 CommandFailed(Output),
255}
256
257pub 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 let waketime = chrono::Local::now().naive_local() + chrono::Duration::minutes(1);
281
282 let mut command = std::process::Command::new("play");
284 command.args(vec!["-q","-n","synth","0.1","sin","880"]);
285
286 let unit_name = UnitName::new("my-special-unit-name-123").unwrap();
288
289 register(waketime,unit_name,command).unwrap();
291
292 let (_command, _datetime) = query_registration(unit_name).unwrap();
294
295 let (_command, _datetime) = deregister(unit_name).unwrap();
297 }
298}