lxcrond 0.2.1

cron and entr/inotify server for lxc containers
Documentation
use crate::config::Job;
use crate::ticker;
use crate::exec::execute_job;
use chrono::{DateTime, Local};
use std::sync::atomic::{AtomicBool, Ordering};

pub struct Cron {
    shutdown: AtomicBool,
    jobs: Vec<Job>,
}

impl Cron {

    pub fn new(jobs: Vec<Job>) -> Cron {
        Cron {
            jobs, shutdown: AtomicBool::new(false)
        }
    }

    fn len(&self) -> usize {
        self.jobs.len()
    }

    pub fn run(&self) {
        if 0 == self.len() {
            return;
        }
        let mut now = ticker::minute_sync();
        while ! self.shutdown.load(Ordering::Relaxed)  {
            self.current_jobs(now).iter().for_each(|j| {
                execute_job(j);
            } );
            now = ticker::sleep_sync();
        }
    }

    pub fn shutdown(&self) {
        self.shutdown.store(true, Ordering::Relaxed);
    }

    pub fn current_jobs(&self, now: DateTime<Local>) -> Vec<&Job> {
        let mut current_jobs = vec![];
        for job in &self.jobs {
            if job.is_time(now) {
                current_jobs.push(job);
            }
        }
        current_jobs
    }

}


#[cfg(test)]
mod tests {
    use super::*;
    use std::process::Command;
    use crate::config::Config;
    use chrono::{Timelike, TimeZone};
    use chrono::format::Fixed::TimezoneOffsetZ;

    /// returns local timezone "+01:00", it does not seem to be possible to write code that is agnostic of the timezone
    /// because chrono dates use <Local> with static dispatch
    // TODO how to do this in pure Rust code
    // https://stackoverflow.com/questions/59603665/how-do-you-find-the-local-timezone-offset-in-rust/59603803#59603803
    fn tz() -> String {
        let output = Command::new("date").arg("+%:z").output().expect("failed to fetch timezone");
        String::from(String::from_utf8_lossy(&output.stdout).trim())
    }

    // this test gives the wrong hour in Daylight savings period
    fn test_time(iso8601: &str) -> DateTime<Local> {
        let mut date_str = String::from(iso8601);
        date_str.push_str(tz().as_str());
        println!("date_str='{}'", date_str);
        let dt = DateTime::parse_from_rfc3339(date_str.as_str()).unwrap().with_timezone(&Local);
        // with_timezone() seems to have a bug https://github.com/chronotope/chrono/issues/410
        println!("dt='{:?}'", dt);
        dt
    }

    #[test]
    fn test_minutes() {
        let data = "# simple\n35 * * * * teknopaul /bin/foo arg";
        let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
        let cron = Cron::new(cfg.take_cron_jobs());
        assert_eq!(1, cron.len());
        assert_eq!(0, cron.current_jobs(test_time("2020-01-05T23:34:30")).len());
        assert_eq!(1, cron.current_jobs(test_time("2020-01-05T23:35:30")).len());
        assert_eq!(0, cron.current_jobs(test_time("2020-01-05T23:36:30")).len());
    }

    // this test fails after daylight savings change with_timezone() seems to have a bug https://github.com/chronotope/chrono/issues/410
//    #[test]
//    fn test_hours() {
//        let data = "# simple\n* 2 * * * teknopaul /bin/foo arg";
//        let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
//        let cron = Cron::new(cfg.take_cron_jobs());
//        assert_eq!(1, cron.len());
//        assert_eq!(0, cron.current_jobs(test_time("2020-01-05T01:00:30")).len());
//        assert_eq!(1, cron.current_jobs(test_time("2020-01-05T02:00:30")).len());
//        assert_eq!(0, cron.current_jobs(test_time("2020-01-05T03:00:30")).len());
//    }
    // this test shows Local::Now() is still working as expected
    // you need to fiddle this test to the time you are running it
    /*
    #[test]
    fn test_hour_now() {
        let data = "# simple\n* 0 * * * teknopaul /bin/foo arg";
        let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
        let cron = Cron::new(cfg.take_cron_jobs());
        assert_eq!(1, cron.len());
        assert_eq!(1, cron.current_jobs(Local::now()).len());
    }
    */

    #[test]
    fn test_days() {
        let data = "# simple\n0 * 10 * * teknopaul /bin/foo arg";
        let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
        let cron = Cron::new(cfg.take_cron_jobs());
        assert_eq!(1, cron.len());
        assert_eq!(0, cron.current_jobs(test_time("2020-06-09T00:00:30")).len());
        assert_eq!(1, cron.current_jobs(test_time("2020-06-10T00:00:30")).len());
        assert_eq!(0, cron.current_jobs(test_time("2020-06-11T00:00:30")).len());
    }

    #[test]
    fn test_months() {
        let data = "# simple\n0 * * 6 * teknopaul /bin/foo arg";
        let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
        let cron = Cron::new(cfg.take_cron_jobs());
        assert_eq!(1, cron.len());
        assert_eq!(0, cron.current_jobs(test_time("2020-05-10T00:00:30")).len());
        assert_eq!(1, cron.current_jobs(test_time("2020-06-10T00:00:30")).len());
        assert_eq!(0, cron.current_jobs(test_time("2020-07-10T00:00:30")).len());
    }

    #[test]
    fn test_parser_simple_cron_alias() {
        let data = "# simple\n@daily teknopaul /bin/foo arg";
        let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
        let cron = Cron::new(cfg.take_cron_jobs());
        assert_eq!(1, cron.len());
        assert_eq!(String::from("/bin/foo"), cron.jobs[0].command().as_str());
        assert_eq!(String::from("arg"), cron.jobs[0].args()[0].as_str());
    }

}