use std::fs::{File, metadata, read_dir};
use std::io::{BufReader, BufRead};
use cmdline_words_parser::StrExt;
use regex::Regex;
use std::borrow::{BorrowMut};
use chrono::{DateTime, Timelike, Datelike, Local};
use std::sync::atomic::{AtomicBool,Ordering};
pub const CRONTAB: &str = "/etc/lxcrontab";
pub const ANY: u8 = 255;
pub const NEVER: u8 = 254;
lazy_static! {
pub static ref VERBOSE: AtomicBool = AtomicBool::new(false);
static ref REGEX_FILE: Regex = Regex::new(r"^(/[^\s]+)\s+([a-z][-a-z0-9]+)\s+(.*)$").unwrap();
static ref REGEX_CRON: Regex = Regex::new(r"^([0-9,\*]+)\s+([0-9,\*]+)\s+([0-9,\*]+)\s+([0-9,\*]+)\s+([0-9,\*]+)\s+([a-z][-a-z0-9]+)\s+(.*)$").unwrap();
static ref REGEX_CRON_ALIAS: Regex = Regex::new(r"^(@[a-z]+)\s+([a-z][-a-z0-9]+)\s+(.*)$").unwrap();
}
pub struct TimeSpec {
minute: Vec<u8>,
hour: Vec<u8>,
day_of_month: Vec<u8>,
month: Vec<u8>,
day_of_week: Vec<u8>,
}
pub struct FileSpec {
is_dir: bool,
path: String,
last_mod: i64,
}
impl FileSpec{
pub fn new(path: String) -> FileSpec {
FileSpec {
is_dir: false,
path,
last_mod: 0
}
}
pub fn path(&self) -> &String {
&self.path
}
pub fn is_dir(&self) -> bool {
self.is_dir
}
}
pub struct Job {
time_spec: Option<TimeSpec>,
file_spec: Option<FileSpec>,
user: String,
command: String,
args: Vec<String>,
pub(crate) watch_descriptor: i32,
}
impl Job {
pub fn new(time_spec: Option<TimeSpec>, file_spec: Option<FileSpec>, user: &str, mut command: String) -> Job {
let mut cmd: Vec<String> = command.parse_cmdline_words().map(|s| String::from(s)).collect();
Job {
time_spec,
file_spec,
user: String::from(user),
command: cmd.remove(0),
args: cmd,
watch_descriptor: 0
}
}
pub fn user(&self) -> &String {
&self.user
}
pub fn command(&self) -> &String {
&self.command
}
pub fn args(&self) -> &Vec<String> {
&self.args
}
pub fn file_spec(&self) -> &FileSpec {
self.file_spec.as_ref().unwrap()
}
pub fn set_watch_descriptor(&mut self, wd: i32) {
self.watch_descriptor = wd;
}
pub fn is_due(&self, now: DateTime<Local>) -> bool {
if self.time_spec.is_some() {
return self.is_time(now);
}
if let Some(file_spec) = &self.file_spec {
return if file_spec.is_dir {
self.dir_has_files()
} else {
false
}
}
panic!("BUG: job must be file or time type");
}
pub fn is_time(&self, now: DateTime<Local>) -> bool {
let time_spec = self.time_spec.as_ref().unwrap();
if (time_spec.minute.contains(&ANY) || time_spec.minute.contains(&(now.minute() as u8))) &&
(time_spec.hour.contains(&ANY) || time_spec.hour.contains( &(now.hour() as u8))) &&
(time_spec.day_of_month.contains(&ANY) || time_spec.day_of_month.contains(&(now.day() as u8))) &&
(time_spec.month.contains(&ANY) || time_spec.month.contains(&(now.month() as u8))) &&
(time_spec.day_of_week.contains(&ANY) || time_spec.day_of_week.contains(&((now.weekday().num_days_from_monday() + 1) as u8) ))
{
return true;
}
false
}
pub fn file_has_changed(&mut self) -> bool {
let mut file_spec = self.file_spec.as_mut().unwrap();
if let Ok(stat) = metadata(&file_spec.path) {
if let Ok(stat) = stat.modified() {
let last_mod = stat.duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64;
if file_spec.last_mod < last_mod {
file_spec.last_mod = last_mod;
return true;
}
}
}
false
}
pub fn dir_has_files(&self) -> bool {
let file_spec = self.file_spec.as_ref().unwrap();
if let Ok(mut paths) = read_dir(&file_spec.path) {
return paths.borrow_mut().any(|f| f.is_ok());
}
false
}
}
pub struct Config {
cron_jobs: Vec<Job>,
file_jobs: Vec<Job>,
}
impl Config {
pub fn new() -> Config {
Config::read_config(CRONTAB)
}
pub fn new_jobs(cron_jobs: Vec<Job>, file_jobs: Vec<Job>) -> Config {
Config { cron_jobs, file_jobs }
}
pub fn len(&self) -> usize {
self.cron_jobs.len() + self.file_jobs.len()
}
pub fn take_cron_jobs(&mut self) -> Vec<Job> {
let mut jobs = vec![];
let mut len = self.cron_jobs.len();
while len > 0 {
jobs.push(self.cron_jobs.remove(len - 1));
len = len - 1;
}
jobs
}
pub fn take_file_jobs(&mut self) -> Vec<Job> {
let mut jobs = vec![];
let mut len = self.file_jobs.len();
while len > 0 {
jobs.push(self.file_jobs.remove(len - 1));
len = len - 1;
}
jobs
}
pub fn read_config(path: &str) -> Config {
let file_data = File::open(path).expect(format!("file not found {}", path).as_str());
let buf = BufReader::new(file_data);
return Config::read_lines(buf.lines().map(|line| line.unwrap()));
}
pub fn read_lines<'a, I>(lines: I) -> Config
where
I: Iterator<Item = String>, {
let mut cfg = Config { cron_jobs: vec![], file_jobs: vec![]};
lines.for_each(|line| {
let line_str = line.trim();
if line_str.is_empty() {
return;
}
match line_str.chars().next().unwrap() {
'#' => {
}
'/' => {
cfg.file_jobs.push(Config::parse_file(line_str));
}
'@' => {
cfg.cron_jobs.push(Config::parse_cron_alias(line_str));
}
'0'..='9' | '*' => {
cfg.cron_jobs.push(Config::parse_cron(line_str));
}
_ => {}
}
});
if VERBOSE.load(Ordering::Relaxed) {
println!("loaded {} jobs", cfg.file_jobs.len() + cfg.cron_jobs.len());
}
cfg
}
fn parse_file(line: &str) -> Job {
let cap = REGEX_FILE.captures_iter(line).next().unwrap();
let file_name = String::from(&cap[1]);
let mut last_mod= 0;
if let Ok(stat) = metadata(file_name) {
if let Ok(stat) = stat.modified() {
last_mod = stat.duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64;
}
}
let mut cmd: Vec<String> = String::from(&cap[3]).parse_cmdline_words().map(|s| String::from(s)).collect();
Job {
time_spec: None,
file_spec: Some(FileSpec {
is_dir: cap[1].ends_with("/"),
path: String::from(&cap[1]),
last_mod,
}),
user: String::from(&cap[2]),
command: cmd.remove(0),
args: cmd,
watch_descriptor: -1
}
}
fn parse_cron(line: &str) -> Job {
let cap = REGEX_CRON.captures_iter(line).next().unwrap();
let mut cmd: Vec<String> = String::from(&cap[7]).parse_cmdline_words().map(|s| String::from(s)).collect();
Job {
time_spec: Some(Config::from_fields(&cap[1],&cap[2],&cap[3],&cap[4],&cap[5])),
file_spec: None,
user: String::from(&cap[6]),
command: cmd.remove(0),
args: cmd,
watch_descriptor: -1
}
}
fn parse_cron_alias(line: &str) -> Job {
let cap = REGEX_CRON_ALIAS.captures_iter(line).next().unwrap();
let mut cmd: Vec<String> = String::from(&cap[3]).parse_cmdline_words().map(|s| String::from(s)).collect();
Job {
time_spec: Some(Config::from_alias(&cap[1])),
file_spec: None,
user: String::from(&cap[2]),
command: cmd.remove(0),
args: cmd,
watch_descriptor: -1
}
}
fn from_fields(minute: &str, hour: &str, day_of_month: &str, month: &str, day_of_week: &str) -> TimeSpec {
TimeSpec {
minute: Config::parse_field(minute),
hour: Config::parse_field(hour),
day_of_month: Config::parse_field(day_of_month),
month: Config::parse_field(month),
day_of_week: Config::parse_field(day_of_week),
}
}
fn from_alias(line: &str) -> TimeSpec {
return match line {
"@always" | "@minutely" | "@1min" => TimeSpec {
minute: vec!(ANY),
hour: vec!(ANY),
day_of_month: vec!(ANY),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@5mins" => TimeSpec {
minute: vec!(0,5,10,15,20,25,30,35,40,45,50,55),
hour: vec!(ANY),
day_of_month: vec!(ANY),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@10mins" => TimeSpec {
minute: vec!(0,10,20,30,40,50),
hour: vec!(ANY),
day_of_month: vec!(ANY),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@15mins" => TimeSpec {
minute: vec!(0,15,30,45),
hour: vec!(ANY),
day_of_month: vec!(ANY),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@hourly" => TimeSpec {
minute: vec!(0),
hour: vec!(ANY),
day_of_month: vec!(ANY),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@daily" | "@midnight" => TimeSpec {
minute: vec!(0),
hour: vec!(0),
day_of_month: vec!(ANY),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@mondays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(1),
},
"@tuesdays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(2),
},
"@wednesdays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(3),
},
"@thursdays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(4),
},
"@fridays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(5),
},
"@saturdays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(6),
},
"@sundays" => TimeSpec {
minute: vec!(0), hour: vec!(0), day_of_month: vec!(ANY), month: vec!(ANY), day_of_week: vec!(7),
},
"@semimonthly" | "@fortnightly" => TimeSpec {
minute: vec!(0),
hour: vec!(0),
day_of_month: vec!(1,15),
month: vec!(1),
day_of_week: vec!(ANY),
},
"@monthly" => TimeSpec {
minute: vec!(0),
hour: vec!(0),
day_of_month: vec!(1),
month: vec!(ANY),
day_of_week: vec!(ANY),
},
"@semiannually" | "@biannually" => TimeSpec {
minute: vec!(0),
hour: vec!(0),
day_of_month: vec!(1),
month: vec!(1,7),
day_of_week: vec!(ANY),
},
"@quarterly" => TimeSpec {
minute: vec!(0),
hour: vec!(0),
day_of_month: vec!(1),
month: vec!(1,4,7,10),
day_of_week: vec!(ANY),
},
"@yearly" | "@annually" => TimeSpec {
minute: vec!(0),
hour: vec!(0),
day_of_month: vec!(1),
month: vec!(1),
day_of_week: vec!(ANY),
},
"@never" | _ => TimeSpec {
minute: vec!(NEVER),
hour: vec!(0),
day_of_month: vec!(0),
month: vec!(0),
day_of_week: vec!(0),
}
};
}
fn parse_field(field: &str) -> Vec<u8> {
field.split(',').map(|f| { f.parse::<u8>().unwrap_or(255) }).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::create_dir_all;
#[test]
fn test_parser_noop() {
let data = "";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(0, cfg.cron_jobs.len());
assert_eq!(0, cfg.file_jobs.len());
}
#[test]
fn test_parser_comments() {
let data = "# this is a comment";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(0, cfg.cron_jobs.len());
assert_eq!(0, cfg.file_jobs.len());
}
#[test]
fn test_take_cron_jobs() {
let data = "# simple\n35 * * * * teknopaul /bin/foo arg";
let mut cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.cron_jobs.len());
assert_eq!(1, cfg.take_cron_jobs().len());
assert_eq!(0, cfg.cron_jobs.len());
}
#[test]
fn test_hours() {
let data = "# simple\n0 2 * * * teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.cron_jobs.len());
}
#[test]
fn test_days() {
let data = "# simple\n0 * 10 * * teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.cron_jobs.len());
}
#[test]
fn test_months() {
let data = "# simple\n0 * * 6 * teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.cron_jobs.len());
}
#[test]
fn test_parser_simple_cron_alias() {
let data = "# simple\n@daily teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.cron_jobs.len());
assert_eq!(String::from("/bin/foo"), cfg.cron_jobs[0].command);
assert_eq!(String::from("arg"), cfg.cron_jobs[0].args[0]);
}
#[test]
fn test_parser_simple_file() {
let data = "# simple\n/etc/nginx.conf teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.file_jobs.len());
assert_eq!(false, cfg.file_jobs[0].file_spec.as_ref().unwrap().is_dir);
assert_eq!(String::from("/bin/foo"), cfg.file_jobs[0].command);
assert_eq!(String::from("arg"), cfg.file_jobs[0].args[0]);
}
#[test]
fn test_parser_simple_dir() {
let data = "# simple\n/etc/nginx/ teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(1, cfg.file_jobs.len());
assert_eq!(true, cfg.file_jobs[0].file_spec.as_ref().unwrap().is_dir);
assert_eq!(String::from("/bin/foo"), cfg.file_jobs[0].command);
assert_eq!(String::from("arg"), cfg.file_jobs[0].args[0]);
}
#[test]
fn test_dir_has_files() {
let data = "# simple\n/etc/ teknopaul /bin/foo arg";
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(true, (&cfg.file_jobs[0]).dir_has_files());
}
#[test]
fn test_dir_has_no_files() {
let data = "# simple\n/tmp/somerandomdirectory teknopaul /bin/foo \"aargh aargh\"";
create_dir_all("/tmp/somerandomdirectory").ok();
let cfg = Config::read_lines(data.split('\n').map(|s| String::from(s)));
assert_eq!(false, (&cfg.file_jobs[0]).dir_has_files());
assert_eq!(String::from("/bin/foo"), cfg.file_jobs[0].command);
assert_eq!(String::from("aargh aargh"), cfg.file_jobs[0].args[0]);
}
}