#![deny(missing_docs)]
use std::ffi::OsString;
use std::fmt::{Display,Formatter};
use std::path::PathBuf;
use std::process::{Command,Output};
use chrono::{Local,DateTime};
use serde::{Serialize,Deserialize};
#[allow(unused_imports)]
use tracing::{info,debug,warn,error,trace,Level};
#[derive(Copy,Clone)]
pub struct TimerName<'a> {
name: &'a str,
}
impl<'a> TimerName<'a> {
pub fn new(name: &'a str) -> Result<Self,TimerNameError> {
if !name.is_ascii() {
return Err(TimerNameError { kind: TimerNameErrorKind::NotAscii });
}
if name.contains(char::is_whitespace) {
return Err(TimerNameError { kind: TimerNameErrorKind::ContainsWhitespace });
}
Ok(Self { name })
}
}
impl AsRef<str> for TimerName<'_> {
fn as_ref(&self) -> &str {
self.name
}
}
impl Display for TimerName<'_> {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
self.name.fmt(f)
}
}
#[derive(Debug)]
pub struct TimerNameError {
kind: TimerNameErrorKind,
}
#[derive(Debug)]
#[allow(missing_docs)]
pub enum TimerNameErrorKind {
NotAscii,
ContainsWhitespace,
}
impl Display for TimerNameError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self.kind {
TimerNameErrorKind::NotAscii => write!(f,"TimerName must be ASCII"),
TimerNameErrorKind::ContainsWhitespace => write!(f,"TimerName cannot contain whitespace"),
}
}
}
impl std::error::Error for TimerNameError {}
#[derive(Serialize,Deserialize)]
pub struct CommandConfig {
program: OsString,
dir: Option<PathBuf>,
env_vars: Vec<(OsString,Option<OsString>)>,
args: Vec<OsString>,
}
impl From<Command> for CommandConfig {
fn from(command: Command) -> Self {
let program = command.get_program().into();
let dir = command.get_current_dir().map(|path| path.to_path_buf());
let env_vars = command.get_envs().map(|(key, value)| {
(key.to_os_string(), value.map(|value| value.to_os_string()))
}).collect();
let args = command.get_args().map(|value| value.to_os_string()).collect();
CommandConfig {
program,
dir,
env_vars,
args,
}
}
}
impl From<CommandConfig> for Command {
fn from(config: CommandConfig) -> Self {
let mut command = Command::new(config.program);
command.args(config.args);
for (key, value) in config.env_vars {
match value {
Some(value) => {
command.env(key,value);
},
None => {
command.env_remove(key);
}
}
}
match config.dir {
Some(dir) => {
command.current_dir(dir);
}
None => {},
};
command
}
}
#[allow(missing_docs)]
impl CommandConfig {
pub fn encode(command: Command) -> String {
let config: CommandConfig = command.into();
hex::encode(serde_json::to_string(&config).unwrap())
}
pub fn decode(hexcode: impl AsRef<[u8]>) -> Command {
let config: CommandConfig = serde_json::from_str(&String::from_utf8(hex::decode(hexcode).unwrap()).unwrap()).unwrap();
config.into()
}
}
pub fn register(event_time: DateTime<Local>, timer_name: TimerName, command: Command) -> Result<(),CommandError> {
debug!("registering timer");
let unit_name = format!("--unit={}",timer_name);
let on_calendar = event_time.format("--on-calendar=%F %T").to_string();
debug!("timer set for {}",on_calendar);
let encoded_command = CommandConfig::encode(command);
let mut systemd_command = Command::new("systemd-run");
systemd_command
.arg("--user")
.arg(unit_name)
.arg(on_calendar)
.arg("systemd-wake")
.arg(encoded_command);
debug!("running timer command: {:?}",systemd_command);
run_command(systemd_command)
}
pub fn deregister(timer_name: TimerName) -> Result<(),CommandError> {
debug!("deregistering timer");
let unit_name = {
let mut name = timer_name.to_string();
name.push_str(".timer");
name
};
let mut systemd_command = Command::new("systemctl");
systemd_command
.arg("--user")
.arg("stop")
.arg(unit_name);
debug!("running stop timer command: {:?}",systemd_command);
run_command(systemd_command)
}
#[derive(Debug)]
pub struct CommandError {
pub command: Command,
pub kind: CommandErrorKind,
}
#[derive(Debug)]
pub enum CommandErrorKind {
RunCommand(std::io::Error),
CommandFailed(Output),
}
impl Display for CommandError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f,"systemd-run command failed: {:?}", self.command)
}
}
impl std::error::Error for CommandError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
CommandErrorKind::RunCommand(e) => Some(e),
CommandErrorKind::CommandFailed(_) => None,
}
}
}
pub fn run_command(mut command: Command) -> Result<(),CommandError> {
match command.output() {
Ok(output) => {
if output.status.success() {
Ok(())
} else {
Err(CommandError {
command,
kind: CommandErrorKind::CommandFailed(output),
})
}
},
Err(e) => {
Err(CommandError {
command,
kind: CommandErrorKind::RunCommand(e),
})
}
}
}