R/cron_add.R

Defines functions cron_add

Documented in cron_add

#' @title Make a simple cron job
#' 
#' @description 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"}. Or any complex cron schedule - see the examples. 
#' @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.
#' @param ask Boolean; show prompt asking for validation
#' @param env Named character; set environment variables for a cron job.
#'   Specify `Sys.getenv()` to inherit the variables from the current R session.
#' @export
#' @examples 
#' \dontshow{if(interactive())
#' \{
#' }
#' f   <- system.file(package = "cronR", "extdata", "helloworld.R")
#' cmd <- cron_rscript(f)
#' cmd
#' 
#' cron_add(command = cmd, frequency = 'minutely', 
#'   id = 'test1', description = 'My process 1', tags = c('lab', 'xyz'))
#' cron_add(command = cmd, frequency = 'daily', at='7AM', id = 'test2')
#' cron_njobs()
#' 
#' cron_ls()
#' cron_clear(ask=TRUE)
#' cron_ls()
#' 
#' cmd <- cron_rscript(f, rscript_args = c("productx", "arg2", "123"))
#' cmd
#' cron_add(cmd, frequency = 'minutely', id = 'job1', description = 'Customers')
#' cron_add(cmd, frequency = 'hourly', id = 'job2', description = 'Weather')
#' cron_add(cmd, frequency = 'hourly', id = 'job3', days_of_week = c(1, 2))
#' cron_add(cmd, frequency = 'hourly', id = 'job4', at = '00:20', days_of_week = c(1, 2))
#' cron_add(cmd, frequency = 'daily', id = 'job5', at = '14:20')
#' cron_add(cmd, frequency = 'daily', id = 'job6', at = '14:20', days_of_week = c(0, 3, 5))
#' cron_add(cmd, frequency = 'daily', id = 'job7', at = '23:59', days_of_month = c(1, 30))
#' cron_add(cmd, frequency = 'monthly', id = 'job8', at = '10:30', 
#'   days_of_month = 'first', days_of_week = '*')
#' cron_add(cmd, frequency = '@reboot', id = 'job9', description = 'Good morning')
#' cron_add(cmd, frequency = '*/15 * * * *', id = 'job10', description = 'Every 15 min')   
#' cron_ls()
#' cron_clear(ask=TRUE)
#' \dontshow{
#' \}
#' }
cron_add <- function(command, frequency="daily", at, days_of_month, days_of_week, months,
  id, tags="", description="", dry_run=FALSE, user="", ask=TRUE, env=character()) {
  
  if (length(env) > 0L && is.null(names(env))) {
    stop("The argument `env` must be a named vector.")
  }
  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
    },
    message(sprintf("At your own risk: will set the cron schedule as is: '%s'", 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="," )
    if(frequency != "hourly"){
      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)
  )
  if (length(env) > 0L)
    header <- paste0(header, "\n", paste(paste(names(env), env, sep = "="), collapse = "\n"))
  if(frequency %in% c("minutely", "hourly", "daily", "monthly", "yearly")){
    job_str <- paste( sep="\n", collapse="\n",
                      header,
                      paste(job, collapse=" ", sep=" ")
    )  
  }else{
    job_str <- paste( sep="\n", collapse="\n",
                      header,
                      paste(frequency, job$command, sep=" ")
    )
  }
  if(ask){
    cat(sep="", "Are you sure you want to add the specified cron job: '", job$command, "'? [y/n]: ")
    input <- tolower(scan(what=character(), n=1, quiet=TRUE))
    if (!input %in% "y") {
      message("No action taken.")
      return(invisible())
    }
  }
  
  
  
  message("Adding cronjob:\n",
    "---------------\n\n",
    job_str
  )
  
  if (!dry_run) {
    
    if(missing(user)){
      old_crontab <- suppressWarnings(
        system("crontab -l", intern=TRUE, ignore.stderr=TRUE)
      )  
    }else{
      old_crontab <- suppressWarnings(
        system(sprintf("crontab -u %s -l", user), 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)
    if(missing(user)){
      system( paste("crontab", tempfile) ) 
    }else{
      system( paste("crontab -u", user, tempfile) )
    }
    
  }
  
  return (invisible(job))
  
}

#' @rdname cron_add
cronjob <- cron_add

Try the cronR package in your browser

Any scripts or data that you put into this service are public.

cronR documentation built on Jan. 9, 2023, 5:10 p.m.