pax_global_header00006660000000000000000000000064140000627570014513gustar00rootroot0000000000000052 comment=650e2ebd33fe3bc3958fea8b1b4b548c381b41b1 snooze-0.5/000077500000000000000000000000001400006275700126745ustar00rootroot00000000000000snooze-0.5/Makefile000066400000000000000000000005311400006275700143330ustar00rootroot00000000000000ALL=snooze CFLAGS=-g -O2 -Wall -Wextra -Wwrite-strings DESTDIR= PREFIX=/usr/local BINDIR=$(PREFIX)/bin MANDIR=$(PREFIX)/share/man all: $(ALL) clean: FRC rm -f $(ALL) install: FRC all mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 install -m0755 $(ALL) $(DESTDIR)$(BINDIR) install -m0644 $(ALL:=.1) $(DESTDIR)$(MANDIR)/man1 FRC: snooze-0.5/NEWS.md000066400000000000000000000013741400006275700137770ustar00rootroot00000000000000## HEAD ## 0.5 (2021-01-14) * Feature: add `-J` for jitter. * Bugfix: fix verbose output when no command is passed. * Bugfix: change timefile calculations to respect slack. ## 0.4 (2020-02-07) * Feature: include runit service files to run cron drop-in scripts. * Bugfix: force stdout line-buffered. ## 0.3 (2018-05-05) * Bugfix: off-by-one for month and day of month. * Bugfix: weird scheduling across DST changes. ## 0.2 (2017-08-29) * Bugfix: Enforce POSIX option processing (stop argument parsing after first nonoption). * Bugfix: off-by-one during week of year parsing. * Bugfix: in a leap year, finding the next event can take longer than 365 days. * Feature: `-X/` now works like `-X*` for all time fields. ## 0.1 (2016-01-05) * Initial release. snooze-0.5/README.md000066400000000000000000000152001400006275700141510ustar00rootroot00000000000000## snooze: run a command at a particular time `snooze` is a new tool for waiting until a particular time and then running a command. Together with a service supervision system such as runit, this can be used to replace cron(8). `snooze` has been tested on Linux 4.2. It will likely work on other Unix-like systems with C99. ## Benefits Over cron: - mnemonic syntax - no overlapping job runs possible - filtering by ISO week and day of year - due to supervision, no centralized daemon required - due to supervision, can easily disable jobs or force their execution instantly - due to supervision, have custom logs - due to no centralized daemon, no fuzzing with multiple users/permissions - very robust with respect to external time changes - can use a file timestamp to ensure minimum waiting time between two runs, even across reboots - randomized delays (some cron have that) - variable slack (no need for anacron) - ad-hoc usage possible, just run the program from command line Over runwhen: - less confusing usage (I hope) - filtering by ISO week and day of year - zero dependencies Over uschedule: - due to supervision, no centralized daemon required - filtering by ISO week and day of year Over systemd timers: - mnemonic syntax - less complexity for jobs, no need for a `.timer` file distinct from the `.service` file, - can use a file timestamp to ensure minimum waiting time between two runs, even across reboots - very robust with respect to external time changes - randomized delays - variable slack - ad-hoc usage possible, just run the program from command line ## Rosetta stone * run five minutes after midnight, every day: cron: `5 0 * * *` snooze: `-M5` * run at 2:15pm on the first of every month: cron: `15 14 1 * *` snooze: `-d1 -H14 -M15` * run at 10 pm on weekdays: cron: `0 22 * * 1-5` snooze: `-w1-5 -H22` * run 23 minutes after midnight, 2am, 4am ..., everyday: cron: `23 0-23/2 * * *` snooze: `-H/2 -M23` * run every second week: snooze: `-W/2` * run every 10 days: snooze: `-D/10` ## Usage: snooze [-nv] [-t timefile] [-T timewait] [-R randdelay] [-s slack] [-d mday] [-m mon] [-w wday] [-D yday] [-W yweek] [-H hour] [-M min] [-S sec] COMMAND... * `-n`: dry-run, print the next 5 times the command would run. * `-v`: verbose, print scheduled (and rescheduled) times. * `-t`, `-T`: see below timefiles * `-R`: add between 0 and RANDDELAY seconds to the start of the scheduled time. * `-J`: add between 0 and JITTER seconds to scheduled execution time. * `-s`: commands are executed even if they are SLACK (default: 60) seconds late. The durations RANDDELAY and JITTER and SLACK and TIMEWAIT are parsed as seconds, unless a postfix of `m` for minutes, `h` for hours, or `d` for days is used. The remaining arguments are patterns for the time fields: * `-d`: day of month * `-m`: month * `-w`: weekday (0-7, sunday is 0 and 7) * `-D`: day of year * `-W`: ISO week of year (0..53) * `-H`: hour * `-M`: minute * `-S`: second The following syntax is used for these options: * exact match: `-d 3`, run on the 3rd * alternation: `-d 3,10,27`, run on 3rd, 10th, 27th * range: `-d 1-5`, run on 1st, 2nd, 3rd, 4th, 5th * star: `-d '*'`, run every day * repetition: `-d /5`, run on 5th, 10th, 15th, 20th, 25th, 30th day * shifted repetition: `-d 2/5`, run on 7th, 12th, 17th, 22nd, 27th day and combinations of those, e.g. `-d 1-10,15/5,28`. The defaults are `-d* -m* -w* -D* -W* -H0 -M0 -S0`, that is, every midnight. Note that *all* patterns need to match (contrary to cron where either day of month *or* day of week matches), so `-w5 -d13` only runs on Friday the 13th. ## Timefiles Optionally, you can keep track of runs in time files, using `-t` and optionally `-T`. When `-T` is passed, execution will not start earlier than the mtime of TIMEFILE plus TIMEWAIT seconds. When `-T` is *not* passed, snooze will start finding the first matching time starting from the mtime of TIMEFILE, and taking SLACK into account. (E.g. `-H0 -s 1d -t timefile` will start an instant execution when timefile has not been touched today, whereas without `-t` this would always wait until next midnight.) If TIMEFILE does not exist, it will be assumed outdated enough to ensure earliest execution. snooze does not update the timefiles, your job needs to do that! Only mtime is looked at, so touch(1) is good. ## Exact behavior * snooze parses the option flags and computes the first time the date pattern matches, as a symbolic date * if a timefile is specified, the time is upped to timefile + timewait seconds * if a random delay is requested, it is added * snooze computes how far this event is in the future * snooze sleeps that long, but at most 5 minutes * after waking, snooze recomputes how far the event is in the future * if the event is in the past, but fewer than SLACK seconds, snooze execs the command. You need to ensure (by setting up supervision) snooze runs again after that! * if we woke due to a SIGALRM, the command is executed immediately as well * if we notice time moved backwards, recompute the time until the event * if the event is in the future, recompute the time it takes, possibly considering shifting of the system time or timezone changes (timezone reload only tested on glibc) * If no command was given, just return with status 0 * and so on... ## Common usages Run a job like cron, every day at 7am and 7pm: exec snooze -H7,19 rdumpfs / /data/dump/mybox 2>&1 Run a job daily, never twice a day: exec snooze -H0 -s 1d -t timefile \ sh -c 'run-parts /etc/cron.daily; touch timefile' Use snooze inline, run a mirror script every hour at 30 minutes past, but ensure there are at least 20 minutes in between. set -e snooze -H'*' -M30 -t timefile -T 20m touch timefile # remove this if instantly retrying on failure were ok mirrorallthethings touch timefile Use snooze inline, cron-style mail: set -e snooze ... actualjob >output 2>&1 || mail -s "$(hostname): snooze job failed with status $?" root has waived all copyright and related or neighboring rights to this work. http://creativecommons.org/publicdomain/zero/1.0/ snooze-0.5/snooze.1000066400000000000000000000070071400006275700142770ustar00rootroot00000000000000.Dd January 4, 2016 .Dt SNOOZE 1 .Os .Sh NAME .Nm snooze .Nd run a command at a particular time .Sh SYNOPSIS .Nm .Op Fl nv .Op Fl t Ar timefile .Op Fl T Ar timewait .Op Fl R Ar randdelay .Op Fl J Ar jitter .Op Fl s Ar slack .Op Fl d Ar day .Op Fl m Ar mon .Op Fl w Ar wday .Op Fl D Ar yday .Op Fl W Ar yweek .Op Fl H Ar hour .Op Fl M Ar min .Op Fl S Ar sec .Ar command\ ... .Sh DESCRIPTION .Nm waits until a particular time and then runs a command. Together with a service supervision system such as .Xr runsv 8 , this can be used to replace .Xr cron 8 . .Pp The options are as follows: .Bl -tag -width Ds .It Fl n Dry run: print the next 5 times the command would run and exit. .It Fl v Verbose: print scheduled (and rescheduled) times. .It Fl t , Fl T See below, .Sx TIMEFILES . .It Fl R Delay determination of scheduled time randomly up to .Ar randdelay seconds later. .It Fl J Delay execution randomly up to .Ar jitter seconds later than scheduled time. .It Fl s Commands are executed even if they are .Ar slack (default: 60) seconds late. .El .Pp The durations .Ar randdelay and .Ar slack and .Ar timewait are parsed as seconds, unless a postfix of .Cm m for minutes, .Cm h for hours, or .Cm d for days is used. .Pp The remaining arguments are patterns for the time fields: .Pp .Bl -tag -compact -width xxxxxxxxxx .It Fl d day of month .It Fl m month .It Fl w weekday (0-7, sunday is 0 and 7) .It Fl D day of year (1..366) .It Fl W ISO week of year (1..53) .It Fl H hour .It Fl M minute .It Fl S second .El .Pp The following syntax is used for these options: .Bl -tag -width xxxxxxxxxx .It Cm -d 3 exact match: run on the 3rd .It Cm -d 3,10,27 alternation: run on 3rd, 10th, 27th .It Cm -d 1-5 range: run on 1st, 2nd, 3rd, 4th, 5th .It Cm -d * star: run every day .It Cm -d /5 repetition: run on 5th, 10th, 15th, 20th, 25th, 30th day .It Cm -d 2/5 shifted repetition: run on 7th, 12th, 17th, 22nd, 27th day .El .Pp and combinations of those, e.g.\& .Cm -d 1-10,15/5,28 . .Pp The defaults are .Cm -d* -m* -w* -D* -W* -H0 -M0 -S0 , that is, every midnight. .Pp Note that .Em all patterns need to match (contrary to .Xr cron 8 where either day of month .Em or day of week matches), so .Cm -w5 -d13 only runs on Friday the 13th. .Pp If .Nm receives a .Dv SIGALRM signal, the command is immediately executed. .Sh TIMEFILES Optionally, you can keep track of runs in time files, using .Fl t : .Pp When .Fl T is passed, execution will not start earlier than the mtime of .Ar timefile plus .Ar timewait seconds. .Pp When .Fl T is .Em not passed, .Nm will start finding the first matching time starting from the mtime of .Ar timefile , and taking .Ar slack into account. (E.g.\& .Cm -H0 -s 1d -t timefile will start an instant execution when timefile has not been touched today, whereas without .Fl t this would always wait until next midnight.) .Pp If .Ar timefile does not exist, it will be assumed outdated enough to ensure earliest execution. .Pp .Nm does not update the timefiles, your job needs to do that! Only mtime is looked at, so .Xr touch 1 is good. .Sh EXIT STATUS .Ex -std .Pp The .Ar command is run using exec, so its exit status gets propagated to the parent. .Pp If no command was given, .Nm just returns with status 0. .Sh SEE ALSO .Xr runwhen 1 , .Xr sleep 1 , .Xr uschedule 1 , .Xr cron 8 .Sh AUTHORS .An Leah Neukirchen Aq Mt leah@vuxu.org .Sh LICENSE .Nm is in the public domain. .Pp To the extent possible under law, the creator of this work has waived all copyright and related or neighboring rights to this work. .Pp .Lk http://creativecommons.org/publicdomain/zero/1.0/ snooze-0.5/snooze.c000066400000000000000000000206321400006275700143600ustar00rootroot00000000000000/* * snooze - run a command at a particular time * * To the extent possible under law, Leah Neukirchen * has waived all copyright and related or neighboring rights to this work. * http://creativecommons.org/publicdomain/zero/1.0/ */ #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include #endif static long slack = 60; #define SLEEP_PHASE 300 static int nflag, vflag; static int timewait = -1; static int randdelay = 0; static int jitter = 0; static char *timefile; static sig_atomic_t alarm_rang = 0; static void wakeup(int sig) { (void)sig; alarm_rang = 1; } static long parse_int(char **s, size_t minn, size_t maxn) { long n; char *end; errno = 0; n = strtol(*s, &end, 10); if (errno) { perror("strtol"); exit(1); } if (n < (long)minn || n >= (long)maxn) { fprintf(stderr, "number outside %zd <= n < %zd\n", minn, maxn); exit(1); } *s = end; return n; } static long parse_dur(char *s) { long n; char *end; errno = 0; n = strtol(s, &end, 10); if (errno) { perror("strtol"); exit(1); } if (n < 0) { fprintf(stderr, "negative duration\n"); exit(1); } switch (*end) { case 'm': n *= 60; break; case 'h': n *= 60*60; break; case 'd': n *= 24*60*60; break; case 0: break; default: fprintf(stderr, "junk after duration: %s\n", end); exit(1); } return n; } static int parse(char *expr, char *buf, long bufsiz, int offset) { char *s; long i, n = 0, n0 = 0; memset(buf, ' ', bufsiz); s = expr; while (*s) { switch (*s) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': n = parse_int(&s, -offset, bufsiz); buf[n+offset] = '*'; break; case '-': n0 = n; s++; n = parse_int(&s, -offset, bufsiz); for (i = n0; i <= n; i++) buf[i+offset] = '*'; break; case '/': s++; n0 = n; if (*s) n = parse_int(&s, -offset, bufsiz); if (n == 0) // / = * n = 1; for (i = n0; i < bufsiz; i += n) buf[i+offset] = '*'; break; case ',': s++; n = 0; break; case '*': s++; n = 0; for (i = 0; i < bufsiz; i++) buf[i+offset] = '*'; break; default: fprintf(stderr, "can't parse: %s %s\n", expr, s); exit(1); } } return 0; } char weekday[8] = {0}; char dayofmonth[32] = {0}; char month[13] = {0}; char dayofyear[367] = {0}; char weekofyear[54] = {0}; char hour[24] = {0}; char minute[60] = {0}; char second[61] = {0}; int isoweek(struct tm *tm) { /* ugh, but easier than the correct formula... */ char weekstr[3]; char *w = weekstr; strftime(weekstr, sizeof weekstr, "%V", tm); return parse_int(&w, 1, 54); } time_t find_next(time_t from) { time_t t; struct tm *tm; t = from; tm = localtime(&t); next_day: while (!( weekday[tm->tm_wday] == '*' && dayofmonth[tm->tm_mday-1] == '*' && month[tm->tm_mon] == '*' && weekofyear[isoweek(tm)-1] == '*' && dayofyear[tm->tm_yday] == '*')) { if (month[tm->tm_mon] != '*') { // if month is not good, step month tm->tm_mon++; tm->tm_mday = 1; } else { tm->tm_mday++; } tm->tm_isdst = -1; tm->tm_sec = 0; tm->tm_min = 0; tm->tm_hour = 0; t = mktime(tm); tm->tm_isdst = -1; if (t > from+(366*24*60*60)) // no result within a year return -1; } int y = tm->tm_yday; // save yday while (!( hour[tm->tm_hour] == '*' && minute[tm->tm_min] == '*' && second[tm->tm_sec] == '*')) { if (hour[tm->tm_hour] != '*') { tm->tm_hour++; tm->tm_min = 0; tm->tm_sec = 0; } else if (minute[tm->tm_min] != '*') { tm->tm_min++; tm->tm_sec = 0; } else { tm->tm_sec++; } t = mktime(tm); if (tm->tm_yday != y) // hit a different day, retry... goto next_day; } if (jitter && !nflag) { long delay; delay = lrand48() % jitter; if (vflag) printf("adding %lds for jitter.\n", delay); t += delay; } return t; } static char isobuf[25]; char * isotime(const struct tm *tm) { strftime(isobuf, sizeof isobuf, "%FT%T%z", tm); return isobuf; } int main(int argc, char *argv[]) { int c; time_t t; time_t now = time(0); time_t last = 0; /* default: every day at 00:00:00 */ memset(weekday, '*', sizeof weekday); memset(dayofmonth, '*', sizeof dayofmonth); memset(month, '*', sizeof month); memset(dayofyear, '*', sizeof dayofyear); memset(weekofyear, '*', sizeof weekofyear); hour[0] = '*'; minute[0] = '*'; second[0] = '*'; setvbuf(stdout, 0, _IOLBF, 0); while ((c = getopt(argc, argv, "+D:W:H:M:S:T:R:J:d:m:ns:t:vw:")) != -1) switch (c) { case 'D': parse(optarg, dayofyear, sizeof dayofyear, -1); break; case 'W': parse(optarg, weekofyear, sizeof weekofyear, -1); break; case 'H': parse(optarg, hour, sizeof hour, 0); break; case 'M': parse(optarg, minute, sizeof minute, 0); break; case 'S': parse(optarg, second, sizeof second, 0); break; case 'd': parse(optarg, dayofmonth, sizeof dayofmonth, -1); break; case 'm': parse(optarg, month, sizeof month, -1); break; case 'w': parse(optarg, weekday, sizeof weekday, 0); // special case: sunday is both 0 and 7. if (weekday[7] == '*') weekday[0] = '*'; break; case 'n': nflag++; break; case 'v': vflag++; break; case 's': slack = parse_dur(optarg); break; case 'T': timewait = parse_dur(optarg); break; case 't': timefile = optarg; break; case 'R': randdelay = parse_dur(optarg); break; case 'J': jitter = parse_dur(optarg); break; default: fprintf(stderr, "Usage: %s [-nv] [-t timefile] [-T timewait] [-R randdelay] [-J jitter] [-s slack]\n" " [-d mday] [-m mon] [-w wday] [-D yday] [-W yweek] [-H hour] [-M min] [-S sec] COMMAND...\n" "Timespec: exact: 1,3,5\n" " range: 1-7\n" " every n-th: /10\n", argv[0]); exit(2); } time_t start = now + 1; if (timefile) { struct stat st; if (stat(timefile, &st) < 0) { if (errno != ENOENT) perror("stat"); t = start - slack - 1 - timewait; } else { t = st.st_mtime + 1; } if (timewait == -1) { while (t < start - slack) t = find_next(t + 1); start = t; } else { if (t + timewait > start - slack) start = t + timewait; } } srand48(getpid() ^ start); if (randdelay) { long delay; delay = lrand48() % randdelay; if (vflag) printf("randomly delaying by %lds.\n", delay); start += delay; } t = find_next(start); if (t < 0) { fprintf(stderr, "no satisfying date found within a year.\n"); exit(2); } if (nflag) { /* dry-run, just output the next 5 dates. */ int i; for (i = 0; i < 5; i++) { char weekstr[4]; struct tm *tm = localtime(&t); strftime(weekstr, sizeof weekstr, "%a", tm); printf("%s %s %2ldd%3ldh%3ldm%3lds ", isotime(tm), weekstr, ((t - now) / (60*60*24)), ((t - now) / (60*60)) % 24, ((t - now) / 60) % 60, (t - now) % 60); if(jitter) { printf("(plus up to %ds for jitter)\n", jitter); } else { printf("\n"); } t = find_next(t + 1); if (t < 0) { fprintf(stderr, "no satisfying date found within a year.\n"); exit(2); } } exit(0); } struct tm *tm = localtime(&t); if (vflag) printf("Snoozing until %s\n", isotime(tm)); // setup SIGALRM handler to force early execution struct sigaction sa; sa.sa_handler = &wakeup; sa.sa_flags = SA_RESTART; sigfillset(&sa.sa_mask); sigaction(SIGALRM, &sa, NULL); // XXX error handling while (!alarm_rang) { now = time(0); if (now < last) { t = find_next(now); if (vflag) printf("Time moved backwards, rescheduled for %s\n", isotime(tm)); } t = mktime(tm); if (t <= now) { if (now - t <= slack) // still about time break; else { // reschedule to next event if (vflag) printf("Missed execution at %s\n", isobuf); t = find_next(now + 1); tm = localtime(&t); if (vflag) printf("Snoozing until %s\n", isotime(tm)); } } else { // do some sleeping, but not more than SLEEP_PHASE struct timespec ts; ts.tv_nsec = 0; ts.tv_sec = t - now > SLEEP_PHASE ? SLEEP_PHASE : t - now; last = now; nanosleep(&ts, 0); // we just iterate again when this exits early } } if (vflag) { now = time(0); tm = localtime(&now); printf("Starting execution at %s\n", isotime(tm)); } // no command to run, the outside script can go on if (argc == optind) return 0; execvp(argv[optind], argv+optind); perror("execvp"); return 255; } snooze-0.5/sv/000077500000000000000000000000001400006275700133245ustar00rootroot00000000000000snooze-0.5/sv/Makefile000066400000000000000000000003511400006275700147630ustar00rootroot00000000000000DESTDIR= SYSCONFDIR=/etc SVDIR=$(SYSCONFDIR)/sv install: FRC for f in daily hourly weekly monthly; do \ mkdir -p $(DESTDIR)$(SVDIR)/snooze-$$f; \ install -m0755 snooze-$$f/run \ $(DESTDIR)$(SVDIR)/snooze-$$f/; \ done FRC: snooze-0.5/sv/snooze-daily/000077500000000000000000000000001400006275700157415ustar00rootroot00000000000000snooze-0.5/sv/snooze-daily/run000077500000000000000000000002771400006275700165010ustar00rootroot00000000000000#!/bin/sh mkdir -p /var/cache/snooze exec snooze -s 1d -t /var/cache/snooze/daily -- sh -c \ "test -d /etc/cron.daily && run-parts --lsbsysinit /etc/cron.daily; : > /var/cache/snooze/daily" snooze-0.5/sv/snooze-hourly/000077500000000000000000000000001400006275700161615ustar00rootroot00000000000000snooze-0.5/sv/snooze-hourly/run000077500000000000000000000003111400006275700167060ustar00rootroot00000000000000#!/bin/sh mkdir -p /var/cache/snooze exec snooze -H \* -s 1h -t /var/cache/snooze/hourly -- sh -c \ "test -d /etc/cron.hourly && run-parts --lsbsysinit /etc/cron.hourly; : > /var/cache/snooze/hourly" snooze-0.5/sv/snooze-monthly/000077500000000000000000000000001400006275700163315ustar00rootroot00000000000000snooze-0.5/sv/snooze-monthly/run000077500000000000000000000003151400006275700170620ustar00rootroot00000000000000#!/bin/sh mkdir -p /var/cache/snooze exec snooze -d 1 -s 28d -t /var/cache/snooze/monthly -- sh -c \ "test -d /etc/cron.monthly && run-parts --lsbsysinit /etc/cron.monthly; : > /var/cache/snooze/monthly" snooze-0.5/sv/snooze-weekly/000077500000000000000000000000001400006275700161375ustar00rootroot00000000000000snooze-0.5/sv/snooze-weekly/run000077500000000000000000000003101400006275700166630ustar00rootroot00000000000000#!/bin/sh mkdir -p /var/cache/snooze exec snooze -w 0 -s 7d -t /var/cache/snooze/weekly -- sh -c \ "test -d /etc/cron.weekly && run-parts --lsbsysinit /etc/cron.weekly; : > /var/cache/snooze/weekly"