R/cron_add.R

##' Make a simple cron job
##' 
##' Generate a cron job, and pass it to crontab.
##' 
##' The goal is to be able to translate simple English statements of intent
##' to the actual \code{cron} statement that could execute that intent. For example,
##' 
##' \emph{"I want to run a job daily at 7AM."}
##' 
##' is simply
##' 
##' \code{cron_add(<command>, "daily", at="7AM")}
##' 
##' Another example, \emph{"I want to run a job on the 15th of every month."}
##' 
##' is
##' 
##' \code{cron_add(<command>, "monthly", days_of_month="15th")}
##' 
##' @param command A command to execute.
##' @param frequency A character string equal to one of 
##'   \code{"minutely"}, \code{"hourly"}, \code{"daily"},
##'   \code{"monthly"}, or \code{"yearly"}.
##' @param at The actual time of day at which to execute the command.
##'   When unspecified, we default to \code{"3AM"}, when the command is to
##'   be run less frequently than \code{"hourly"}.
##' @param days_of_month Optional; the day(s) of the month on which we execute the
##'   command.
##' @param days_of_week Optional; the day(s) of the week on which we execute the
##'   command.
##' @param months Optional; the month(s) of the year on which we execute
##'   the command.
##' @param id An id, or name, to give to the cronjob task, for easier
##'   revision in the future.
##' @param tags A set of tags, used for easy listing and retrieval
##'   of cron jobs.
##' @param description A short description of the job, and its purpose.
##' @param dry_run Boolean; if \code{TRUE} we do not submit the cron job; 
##'   instead we return the parsed text that would be submitted as a cron job.
##' @param user The user whose cron jobs we wish to examine.
##' @importFrom digest digest
##' @export
cron_add <- function(command, frequency="daily", at, days_of_month, days_of_week, months,
  id, tags="", description="", dry_run=FALSE, user="") {
  
  crontab <- tryCatch(parse_crontab(user=user),
    error=function(e) {
      return( character() )
    })
  
  ## make sure the id generated / used is unique
  call <- match.call()
  digested <- FALSE
  if (missing(id)) {
    digested <- TRUE
    id <- digest(call)
  }
  
  if (length(crontab) && length(crontab$cronR)) {
    if (id %in% sapply(crontab$cronR, "[[", "id")) {
      if (digested) {
        warning("This id was auto-generated by 'digest'; it is likely that ",
          "you attempted to submit an identical job.")
      }
      stop("Can't add this job: a job with id '", id, 
        "' already exists.")
    }
  }
  
  call_str <- paste( collapse="", 
    gsub(" +$", "", capture.output(call) )
  )
  
  job <- list(
    min=NULL,
    hour=NULL,
    day_of_month=NULL,
    month=NULL,
    day_of_week=NULL,
    command=NULL
  )
  
  frequency <- tolower(frequency)
  switch( frequency,
    minutely={
      job[["min"]] <- "0-59"
      job[["hour"]] <- "*"
      job[["day_of_month"]] <- "*"
      job[["month"]] <- "*"
      job[["day_of_week"]] <- "*"
    },
    hourly={
      job[["min"]] <- 0
      job[["hour"]] <- "*"
      job[["day_of_month"]] <- "*"
      job[["month"]] <- "*"
      job[["day_of_week"]] <- "*"
    },
    daily={
      job[["min"]] <- 0
      job[["hour"]] <- 0
      job[["day_of_month"]] <- "*"
      job[["month"]] <- "*"
      job[["day_of_week"]] <- "*"
    },
    monthly={
      job[["min"]] <- 0
      job[["hour"]] <- 0
      job[["day_of_month"]] <- 1
      job[["month"]] <- "*"
      job[["day_of_week"]] <- 1
    },
    yearly={
      job[["min"]] <- 0
      job[["hour"]] <- 0
      job[["day_of_month"]] <- 1
      job[["month"]] <- 1
      job[["day_of_week"]] <- 1
    },
    stop("Unrecognized string passed to frequency: '", frequency, "'")
  )
  
  if (!missing(days_of_week)) {
    days_of_week <- ordinal_swap(days_of_week, adj=-1)
    job[["day_of_week"]] <- paste( unique( sort(sapply(days_of_week, parse_day_of_week)), collapse=",") )
  }
  
  if (!missing(days_of_month)) {
    days_of_month <- ordinal_swap(days_of_month)
    job[["day_of_month"]] <- paste( unique( sort(sapply(days_of_month, parse_day_of_month)), collapse=",") )
  }
  
  if (!missing(months)) {
    months <- ordinal_swap(months)
    job[["month"]] <- paste( unique( sort(sapply(months, parse_month)), collapse=",") )
  }
  
  if (!missing(at)) {
    at_list <- lapply(at, parse_time)
    job[["min"]] <- paste( sapply(at_list, "[[", "minutes"), collapse="," )
    job[["hour"]] <- paste( sapply(at_list, "[[", "hours"), collapse="," )
  }
  
  job[["command"]] <- command
  
  if (any(is.null(job)))
    stop("NULL commands in 'job!' Job is: ", paste(job, collapse=" ", sep=" "))
  
  description <- unlist(strsplit( wrap(description), "\n" ))
  if (length(description) > 1) {
    description[2:length(description)] <- 
      paste0("##   ", description[2:length(description)])
  }
  description <- paste(description, collapse="\n")
  
  header <- paste( sep="\n", collapse="\n",
    "## cronR job",
    paste0("## id:   ", id),
    paste0("## tags: ", paste(tags, collapse=", ")),
    paste0("## desc: ", description)
  )
  
  job_str <- paste( sep="\n", collapse="\n",
    header,
    paste(job, collapse=" ", sep=" ")
  )
  
  message("Adding cronjob:\n",
    "---------------\n\n",
    job_str
  )
  
  if (!dry_run) {
    
    old_crontab <- suppressWarnings(
      system("crontab -l", intern=TRUE, ignore.stderr=TRUE)
    )
    
    old_crontab[ old_crontab == " " ] <- ""
    
    if (length(old_crontab)) {
      new_crontab <- paste( sep="\n",
        paste(old_crontab, collapse="\n"),
        paste0(job_str, "\n")
      )
    } else {
      new_crontab <- paste0(job_str, "\n")
    }
    
    tempfile <- tempfile()
    on.exit( unlink(tempfile) )
    cat(new_crontab, "\n", file=tempfile)
    system( paste("crontab", tempfile) )
  }
  
  return (invisible(job))
  
}

##' @rdname cron_add
cronjob <- cron_add
kevinushey/cronR documentation built on May 20, 2019, 9:09 a.m.