From 25beac27807ed9f41ae9ef8e3da6feae19c43ba7 Mon Sep 17 00:00:00 2001 From: Marco Migliori Date: Mon, 27 Jun 2016 14:38:05 +0200 Subject: [PATCH] Added useful utility cronnext to find out time of the next job run. --- man/cronnext.1 | 48 ++++++++ src/Makefile.am | 6 +- src/cronnext.c | 300 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 man/cronnext.1 create mode 100644 src/cronnext.c diff --git a/man/cronnext.1 b/man/cronnext.1 new file mode 100644 index 0000000..3edb0f2 --- /dev/null +++ b/man/cronnext.1 @@ -0,0 +1,48 @@ +.TH CRONNEXT 1 "Nov 30, 2015" +.SH NAME +cronnext \- time of next job cron will execute +.SH SYNOPSIS +.TP 9 +.B cronnext +[-i users] [-e users] [-s] [-t time] [-v] [-h] +.SH DESCRIPTION +Determine the time cron will execute the next job. +Without arguments, it prints that time considering all crontabs, +in number of seconds since the Epoch. +This number can be converted into other formats using date(1), +like 'date --date @43243254' +.SH OPTIONS +.TP +-i user,user,user,... +consider only the crontabs of these users; +.br +use "*system*" for the system crontab +.TP +-e user,user,user,... +do not consider the crontabs of these users +.TP +-s +do not consider the system crontab, usually /etc/crontab; +.br +the system crontab usually contains the hourly, daily, weekly and montly +crontabs, which might be better dealt with anacron(8) +.TP +-t time +determine the next job from this time, instead of now; +.br +the time is expressed in number of seconds since the Epoch, as obtained for +example by 'date +%s --date "now + 2 hours"' +.TP +-v +verbose mode: scanning the crontab is done in verbose mode, and for each user +and job the next execution time is printed both as number of seconds since the +Epoch and in the standard format +.TP +-h +summary of options +.SH AUTHOR +Marco Migliori +.SH SEE ALSO +cron(8), cron(1), crontab(5), crontab(1), anacron(8), anacrontab(5), atq(1), +date(1) + diff --git a/src/Makefile.am b/src/Makefile.am index 71124de..c074145 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,18 +1,22 @@ # Makefile.am - two binaries crond and crontab sbin_PROGRAMS = crond -bin_PROGRAMS = crontab +bin_PROGRAMS = crontab cronnext crond_SOURCES = \ cron.c database.c user.c job.c do_command.c popen.c \ $(common_src) crontab_SOURCES = crontab.c $(common_src) +cronnext_SOURCES = \ + cronnext.c database.c user.c job.c do_command.c popen.c \ + $(common_src) common_src = entry.c env.c misc.c pw_dup.c security.c \ externs.h funcs.h globals.h macros.h pathnames.h structs.h \ bitstring.h common_nodist = cron-paths.h nodist_crond_SOURCES = $(common_nodist) nodist_crontab_SOURCES = $(common_nodist) +nodist_cronnext_SOURCES = $(common_nodist) BUILT_SOURCES = $(common_nodist) AM_CFLAGS = -I$(top_srcdir) diff --git a/src/cronnext.c b/src/cronnext.c new file mode 100644 index 0000000..6d3c373 --- /dev/null +++ b/src/cronnext.c @@ -0,0 +1,300 @@ +/* + cronnext - calculate the time cron will execute the next job + Copyright (C) 2016 Marco Migliori + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + The GNU General Public License can also be found in the file + `COPYING' that comes with the Anacron source distribution. +*/ + +#include "config.h" + +#define MAIN_PROGRAM + +#include +#include +#include + +#include "globals.h" +#include "funcs.h" +#include "cron-paths.h" + +/* + * print entry flags + */ +char *flagname[]= { + [MIN_STAR] = "MIN_STAR", + [HR_STAR] = "HR_STAR", + [DOM_STAR] = "DOM_STAR", + [DOW_STAR] = "DOW_STAR", + [WHEN_REBOOT] = "WHEN_REBOOT", + [DONT_LOG] = "DONT_LOG" +}; + +void printflags(int flags) { + int f; + printf("flags: 0x%d = ", flags); + for (f = 1; f < sizeof(flagname); f = f << 1) + if (flags & f) + printf("%s ", flagname[f]); + printf("\n"); +} + +/* + * print a crontab entry + */ +void printentry(entry *e, int system, time_t next) { + if (system) + printf("entry user: %s\n", e->pwd->pw_name); + printf("cmd: %s\n", e->cmd); + printflags(e->flags); + printf("delay: %d\n", e->delay); + printf("next: %d = ", next); + printf("%s", asctime(localtime(&next))); +} + +/* + * print a crontab data + */ +void printcrontab(user *u) { + printf("==========================\n"); + printf("user: %s\n", u->name); + printf("crontab: %s\n", u->tabname); + printf("system: %d\n", u->system); +} + +/* + * basic algorithm: iterate over time from now to 8 year ahead in default steps + * of 1 minute, checking whether time matches a crontab entry at each step (8 + * years is the largest interval between two crontab matches) + * + * to save iterations, use larger steps if month or day don't match the entry: + * - if the month doesn't match, skip to 00:00 of the first day of next month + * - for the day, avoid the complication of the different length of months: if + * neither the day nor the next day match, increase time of one day + */ + +/* + * check whether time matches day of month and/or day of week; this requires + * checking dom if dow=*, dow if dom=*, either one otherwise; see comment "the + * dom/dow situation is odd..." in cron.c + */ +int matchday(entry *e, time_t time) { + struct tm current; + int d; + + localtime_r(&time, ¤t); + + if (e->flags & DOW_STAR) + return bit_test(e->dom, current.tm_mday - 1); + if (e->flags & DOM_STAR) + return bit_test(e->dow, current.tm_wday); + return bit_test(e->dom, current.tm_mday - 1) || + bit_test(e->dow, current.tm_wday); +} + +/* + * next time matching a crontab entry + */ +time_t nextmatch(entry *e, time_t start) { + time_t time; + time_t nexttime; + struct tm current; + int res; + + /* maximum match interval is 8 years (<102 months of 28 days): + * crontab has '* * 29 2 *' and we are on 1 March 2096: + * next matching time will be 29 February 2104 */ + + for (time = start; time < start + 102 * 28 * 24 * 60 * 60; ) { + localtime_r(&time, ¤t); + + /* month doesn't match: move to 1st of next month */ + if (!bit_test(e->month, current.tm_mon)) { + current.tm_mon++; + if (current.tm_mon >= 12) { + current.tm_year++; + current.tm_mon = 0; + } + current.tm_mday = 1; + current.tm_hour = 0; + current.tm_min = 0; + time = mktime(¤t); + continue; + } + + /* neither time nor time+1day match day: increase 1 day */ + if (!matchday(e, time) && !matchday(e, time + 24 * 60 * 60)) { + time += 24 * 60 * 60; + continue; + } + + /* if time matches, return time; + * check for month is redudant, but check for day is + * necessary because we only know that either time + * or time+1day match */ + if (bit_test(e->month, current.tm_mon) && + matchday(e, time) && + bit_test(e->hour, current.tm_hour) && + bit_test(e->minute, current.tm_min) + ) + return time; + + /* skip to next minute */ + time += 60; + } + + return -1; +} + +/* + * match a user against a list + */ +int matchuser(char *user, char *list) { + char *pos; + int l = strlen(user); + + for (pos = list; pos = strstr(pos, user); pos += l) { + if ((pos != list) && (*(pos - 1) != ',')) + continue; + if ((pos[l] != '\0') && (pos[l] != ',')) + continue; + return 1; + } + return 0; +} + +/* + * find next sheduled job + */ +time_t cronnext(time_t start, + char *include, char *exclude, int system, + int verbose) { + time_t closest, next; + static cron_db database = {NULL, NULL, (time_t) 0}; + user *u; + entry *e; + + /* load crontabs */ + load_database(&database); + + /* find next sheduled time */ + closest = -1; + for (u = database.head; u; u = u->next) { + if (include && !matchuser(u->name, include)) + continue; + if (exclude && matchuser(u->name, exclude)) + continue; + if (!system && u->system) + continue; + + if (verbose) + printcrontab(u); + + for (e = u->crontab; e; e = e->next) { + next = nextmatch(e, start); + if (next < 0) + continue; + if ((closest < 0) || (next < closest)) + closest = next; + if (verbose) + printentry(e, u->system, next); + } + } + + return closest; +} + +void usage() { + printf("find the next time of a scheduled cron job\n"); + printf("usage:\n"); + printf("\tcronnext [-i users] [-e users] [-s] [-t time] [-v] [-h]\n"); + printf("\t\t-i users\tinclude only the crontab of these users\n"); + printf("\t\t-e users\texclude the crontab of these users\n"); + printf("\t\t-s\t\tdo not include the system crontab\n"); + printf("\t\t-t time\t\tstart from this time (seconds since epoch)\n"); + printf("\t\t-v\t\tverbose mode\n"); + printf("\t\t-h\t\tthis help\n"); +} + +/* + * main + */ +int main(int argn, char *argv[]) { + int opt; + char *include, *exclude; + int system, verbose; + time_t start, next; + + include = NULL; + exclude = NULL; + system = 1; + start = time(NULL); + verbose = 0; + + while (-1 != (opt = getopt(argn, argv, "i:e:st:vh"))) { + switch (opt) { + case 'i': + include = optarg; + break; + case 'e': + exclude = optarg; + break; + case 's': + system = 0; + break; + case 't': + start = atoi(optarg); + break; + case 'v': + verbose = 1; + break; + case 'h': + usage(); + return EXIT_SUCCESS; + default: + printf("unrecognized option: %s\n", + argv[optind - 1]); + usage(); + exit(EXIT_FAILURE); + } + } + + /* debug cron */ + if (verbose) { + printf("SPOOL_DIR=%s\n", SPOOL_DIR); + set_debug_flags("load"); + } + /* "load,pars" for debugging loading and parsing, "" for nothing + see globals.h for symbolic names and macros.h for meaning */ + + /* print time of next scheduled command */ + next = cronnext(start, include, exclude, system, verbose); + if (next == -1) { + if (verbose) + printf("no sheduled job\n"); + return EXIT_FAILURE; + } + else + if (verbose) + printf("next of all jobs: %d=%s", + next, asctime(localtime(&next))); + else + printf("%d\n", next); + + return EXIT_SUCCESS; +} + -- 2.50.1