├── sv ├── snooze-daily │ └── run ├── snooze-weekly │ └── run ├── snooze-hourly │ └── run ├── snooze-monthly │ └── run └── Makefile ├── Makefile ├── NEWS.md ├── snooze.1 ├── README.md └── snooze.c /sv/snooze-daily/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /var/cache/snooze 3 | exec snooze -s 1d -t /var/cache/snooze/daily -- sh -c \ 4 | "test -d /etc/cron.daily && run-parts --lsbsysinit /etc/cron.daily; touch /var/cache/snooze/daily" 5 | -------------------------------------------------------------------------------- /sv/snooze-weekly/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /var/cache/snooze 3 | exec snooze -w 0 -s 7d -t /var/cache/snooze/weekly -- sh -c \ 4 | "test -d /etc/cron.weekly && run-parts --lsbsysinit /etc/cron.weekly; touch /var/cache/snooze/weekly" 5 | -------------------------------------------------------------------------------- /sv/snooze-hourly/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /var/cache/snooze 3 | exec snooze -H \* -s 1h -t /var/cache/snooze/hourly -- sh -c \ 4 | "test -d /etc/cron.hourly && run-parts --lsbsysinit /etc/cron.hourly; touch /var/cache/snooze/hourly" 5 | -------------------------------------------------------------------------------- /sv/snooze-monthly/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /var/cache/snooze 3 | exec snooze -d 1 -s 28d -t /var/cache/snooze/monthly -- sh -c \ 4 | "test -d /etc/cron.monthly && run-parts --lsbsysinit /etc/cron.monthly; touch /var/cache/snooze/monthly" 5 | -------------------------------------------------------------------------------- /sv/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | SYSCONFDIR=/etc 3 | SVDIR=$(SYSCONFDIR)/sv 4 | 5 | install: FRC 6 | for f in daily hourly weekly monthly; do \ 7 | mkdir -p $(DESTDIR)$(SVDIR)/snooze-$$f; \ 8 | install -m0755 snooze-$$f/run \ 9 | $(DESTDIR)$(SVDIR)/snooze-$$f/; \ 10 | done 11 | 12 | FRC: 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ALL=snooze 2 | 3 | CFLAGS=-g -O2 -Wall -Wextra -Wwrite-strings 4 | 5 | DESTDIR= 6 | PREFIX=/usr/local 7 | BINDIR=$(PREFIX)/bin 8 | MANDIR=$(PREFIX)/share/man 9 | 10 | all: $(ALL) 11 | 12 | clean: FRC 13 | rm -f $(ALL) 14 | 15 | install: FRC all 16 | mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 17 | install -m0755 $(ALL) $(DESTDIR)$(BINDIR) 18 | install -m0644 $(ALL:=.1) $(DESTDIR)$(MANDIR)/man1 19 | 20 | FRC: 21 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | ## HEAD 2 | 3 | ## 0.5.1 (2025-10-03) 4 | 5 | * Minor bug fixes and documentation improvements. 6 | 7 | ## 0.5 (2021-01-14) 8 | 9 | * Feature: add `-J` for jitter. 10 | * Bugfix: fix verbose output when no command is passed. 11 | * Bugfix: change timefile calculations to respect slack. 12 | 13 | ## 0.4 (2020-02-07) 14 | 15 | * Feature: include runit service files to run cron drop-in scripts. 16 | * Bugfix: force stdout line-buffered. 17 | 18 | ## 0.3 (2018-05-05) 19 | 20 | * Bugfix: off-by-one for month and day of month. 21 | * Bugfix: weird scheduling across DST changes. 22 | 23 | ## 0.2 (2017-08-29) 24 | 25 | * Bugfix: Enforce POSIX option processing (stop argument parsing after 26 | first nonoption). 27 | * Bugfix: off-by-one during week of year parsing. 28 | * Bugfix: in a leap year, finding the next event can take longer than 365 days. 29 | * Feature: `-X/` now works like `-X*` for all time fields. 30 | 31 | ## 0.1 (2016-01-05) 32 | 33 | * Initial release. 34 | -------------------------------------------------------------------------------- /snooze.1: -------------------------------------------------------------------------------- 1 | .Dd August 26, 2023 2 | .Dt SNOOZE 1 3 | .Os 4 | .Sh NAME 5 | .Nm snooze 6 | .Nd run a command at a particular time 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl nv 10 | .Op Fl t Ar timefile 11 | .Op Fl T Ar timewait 12 | .Op Fl R Ar randdelay 13 | .Op Fl J Ar jitter 14 | .Op Fl s Ar slack 15 | .Op Fl d Ar day 16 | .Op Fl m Ar mon 17 | .Op Fl w Ar wday 18 | .Op Fl D Ar yday 19 | .Op Fl W Ar yweek 20 | .Op Fl H Ar hour 21 | .Op Fl M Ar min 22 | .Op Fl S Ar sec 23 | .Ar command\ ... 24 | .Sh DESCRIPTION 25 | .Nm 26 | waits until a particular time and then runs a command. 27 | Together with a service supervision system such as 28 | .Xr runsv 8 , 29 | this can be used to replace 30 | .Xr cron 8 . 31 | .Pp 32 | The options are as follows: 33 | .Bl -tag -width Ds 34 | .It Fl n 35 | Dry run: print the next 5 times the command would run and exit. 36 | .It Fl v 37 | Verbose: print scheduled (and rescheduled) times. 38 | .It Fl t , Fl T 39 | See below, 40 | .Sx TIMEFILES . 41 | .It Fl R 42 | Delay determination of scheduled time randomly up to 43 | .Ar randdelay 44 | seconds later. 45 | .It Fl J 46 | Delay execution randomly up to 47 | .Ar jitter 48 | seconds later than scheduled time. 49 | .It Fl s 50 | Commands are executed even if they are 51 | .Ar slack 52 | (default: 60) seconds late. 53 | .Em Caveat: 54 | This will not result in immediate execution unless 55 | .Sx TIMEFILES 56 | are used. 57 | .El 58 | .Pp 59 | The durations 60 | .Ar randdelay 61 | and 62 | .Ar slack 63 | and 64 | .Ar timewait 65 | are parsed as seconds, 66 | unless a postfix of 67 | .Cm m 68 | for minutes, 69 | .Cm h 70 | for hours, or 71 | .Cm d 72 | for days is used. 73 | .Pp 74 | The remaining arguments are patterns for the time fields: 75 | .Pp 76 | .Bl -tag -compact -width xxxxxxxxxx 77 | .It Fl d 78 | day of month 79 | .It Fl m 80 | month 81 | .It Fl w 82 | weekday (0-7, sunday is 0 and 7) 83 | .It Fl D 84 | day of year (1..366) 85 | .It Fl W 86 | ISO week of year (1..53) 87 | .It Fl H 88 | hour 89 | .It Fl M 90 | minute 91 | .It Fl S 92 | second 93 | .El 94 | .Pp 95 | The following syntax is used for these options: 96 | .Bl -tag -width xxxxxxxxxx 97 | .It Cm -d 3 98 | exact match: run on the 3rd 99 | .It Cm -d 3,10,27 100 | alternation: run on 3rd, 10th, 27th 101 | .It Cm -d 1-5 102 | range: run on 1st, 2nd, 3rd, 4th, 5th 103 | .It Cm -d * 104 | star: run every day 105 | .It Cm -d /5 106 | repetition: run on 5th, 10th, 15th, 20th, 25th, 30th day 107 | .It Cm -d 2/5 108 | shifted repetition: run on 7th, 12th, 17th, 22nd, 27th day 109 | .El 110 | .Pp 111 | and combinations of those, e.g.\& 112 | .Cm -d 1-10,15/5,28 . 113 | .Pp 114 | The defaults are 115 | .Cm -d* -m* -w* -D* -W* -H0 -M0 -S0 , 116 | that is, every midnight. 117 | .Pp 118 | Note that 119 | .Em all 120 | patterns need to match 121 | (contrary to 122 | .Xr cron 8 123 | where either day of month 124 | .Em or 125 | day of week matches), so 126 | .Cm -w5 -d13 127 | only runs on Friday the 13th. 128 | .Pp 129 | If 130 | .Nm 131 | receives a 132 | .Dv SIGALRM 133 | signal, the command is immediately executed. 134 | .Sh TIMEFILES 135 | Optionally, you can keep track of runs in time files, using 136 | .Fl t : 137 | .Pp 138 | When 139 | .Fl T 140 | is passed, execution will not start earlier than the mtime 141 | of 142 | .Ar timefile 143 | plus 144 | .Ar timewait 145 | seconds. 146 | .Pp 147 | When 148 | .Fl T 149 | is 150 | .Em not 151 | passed, 152 | .Nm 153 | will start finding the first matching time 154 | starting from the mtime of 155 | .Ar timefile , 156 | and taking 157 | .Ar slack 158 | into account. 159 | (E.g.\& 160 | .Cm -H0 -s 1d -t timefile 161 | will start an instant 162 | execution when timefile has not been touched today, whereas without 163 | .Fl t 164 | this would always wait until next midnight.) 165 | .Pp 166 | If 167 | .Ar timefile 168 | does not exist, it will be assumed outdated enough to 169 | ensure earliest execution. 170 | .Pp 171 | .Nm 172 | does not update the timefiles, your job needs to do that! 173 | Only mtime is looked at, so 174 | .Xr touch 1 175 | is good. 176 | .Sh EXIT STATUS 177 | .Ex -std 178 | .Pp 179 | The 180 | .Ar command 181 | is run using exec, so its exit status gets propagated to the parent. 182 | .Pp 183 | If no command was given, 184 | .Nm 185 | just returns with status 0. 186 | .Sh SEE ALSO 187 | .Xr runwhen 1 , 188 | .Xr sleep 1 , 189 | .Xr uschedule 1 , 190 | .Xr cron 8 191 | .Sh AUTHORS 192 | .An Leah Neukirchen Aq Mt leah@vuxu.org 193 | .Sh LICENSE 194 | .Nm 195 | is in the public domain. 196 | .Pp 197 | To the extent possible under law, 198 | the creator of this work 199 | has waived all copyright and related or 200 | neighboring rights to this work. 201 | .Pp 202 | .Lk http://creativecommons.org/publicdomain/zero/1.0/ 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## snooze: run a command at a particular time 2 | 3 | `snooze` is a new tool for waiting until a particular time and then 4 | running a command. Together with a service supervision system such as 5 | runit, this can be used to replace cron(8). 6 | 7 | `snooze` has been tested on Linux 4.2. 8 | It will likely work on other Unix-like systems with C99. 9 | 10 | ## Benefits 11 | 12 | Over cron: 13 | - mnemonic syntax 14 | - no overlapping job runs possible 15 | - filtering by ISO week and day of year 16 | - due to supervision, no centralized daemon required 17 | - due to supervision, can easily disable jobs or force their 18 | execution instantly 19 | - due to supervision, have custom logs 20 | - due to no centralized daemon, no fuzzing with multiple users/permissions 21 | - very robust with respect to external time changes 22 | - can use a file timestamp to ensure minimum waiting time between two 23 | runs, even across reboots 24 | - randomized delays (some cron have that) 25 | - variable slack (no need for anacron) 26 | - ad-hoc usage possible, just run the program from command line 27 | 28 | Over runwhen: 29 | - less confusing usage (I hope) 30 | - filtering by ISO week and day of year 31 | - zero dependencies 32 | 33 | Over uschedule: 34 | - due to supervision, no centralized daemon required 35 | - filtering by ISO week and day of year 36 | 37 | Over systemd timers: 38 | - mnemonic syntax 39 | - less complexity for jobs, no need for a `.timer` file distinct 40 | from the `.service` file, 41 | - can use a file timestamp to ensure minimum waiting time between two 42 | runs, even across reboots 43 | - very robust with respect to external time changes 44 | - randomized delays 45 | - variable slack 46 | - ad-hoc usage possible, just run the program from command line 47 | 48 | ## Rosetta stone 49 | 50 | * run five minutes after midnight, every day: 51 | cron: `5 0 * * *` 52 | snooze: `-M5` 53 | * run at 2:15pm on the first of every month: 54 | cron: `15 14 1 * *` 55 | snooze: `-d1 -H14 -M15` 56 | * run at 10 pm on weekdays: 57 | cron: `0 22 * * 1-5` 58 | snooze: `-w1-5 -H22` 59 | * run 23 minutes after midnight, 2am, 4am ..., everyday: 60 | cron: `23 0-23/2 * * *` 61 | snooze: `-H/2 -M23` 62 | * run every second week: 63 | snooze: `-W/2` 64 | * run every 10 days: 65 | snooze: `-D/10` 66 | 67 | ## Usage: 68 | 69 | 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... 70 | 71 | * `-n`: dry-run, print the next 5 times the command would run. 72 | * `-v`: verbose, print scheduled (and rescheduled) times. 73 | * `-t`, `-T`: see below timefiles 74 | * `-R`: add between 0 and RANDDELAY seconds to the start of the scheduled time. 75 | * `-J`: add between 0 and JITTER seconds to scheduled execution time. 76 | * `-s`: commands are executed even if they are SLACK (default: 60) seconds late. 77 | 78 | The durations RANDDELAY and JITTER and SLACK and TIMEWAIT are parsed as seconds, 79 | unless a postfix of `m` for minutes, `h` for hours, or `d` for days is used. 80 | 81 | The remaining arguments are patterns for the time fields: 82 | 83 | * `-d`: day of month 84 | * `-m`: month 85 | * `-w`: weekday (0-7, sunday is 0 and 7) 86 | * `-D`: day of year 87 | * `-W`: ISO week of year (0..53) 88 | * `-H`: hour 89 | * `-M`: minute 90 | * `-S`: second 91 | 92 | The following syntax is used for these options: 93 | 94 | * exact match: `-d 3`, run on the 3rd 95 | * alternation: `-d 3,10,27`, run on 3rd, 10th, 27th 96 | * range: `-d 1-5`, run on 1st, 2nd, 3rd, 4th, 5th 97 | * star: `-d '*'`, run every day 98 | * repetition: `-d /5`, run on 5th, 10th, 15th, 20th, 25th, 30th day 99 | * shifted repetition: `-d 2/5`, run on 7th, 12th, 17th, 22nd, 27th day 100 | 101 | and combinations of those, e.g. `-d 1-10,15/5,28`. 102 | 103 | The defaults are `-d* -m* -w* -D* -W* -H0 -M0 -S0`, that is, every midnight. 104 | 105 | Note that *all* patterns need to match (contrary to cron where either 106 | day of month *or* day of week matches), so `-w5 -d13` only runs on 107 | Friday the 13th. 108 | 109 | ## Timefiles 110 | 111 | Optionally, you can keep track of runs in time files, using `-t` and 112 | optionally `-T`. 113 | 114 | When `-T` is passed, execution will not start earlier than the mtime 115 | of TIMEFILE plus TIMEWAIT seconds. 116 | 117 | When `-T` is *not* passed, snooze will start finding the first matching time 118 | starting from the mtime of TIMEFILE, and taking SLACK into account. 119 | (E.g. `-H0 -s 1d -t timefile` will start an instant 120 | execution when timefile has not been touched today, whereas without `-t` 121 | this would always wait until next midnight.) 122 | 123 | If TIMEFILE does not exist, it will be assumed outdated enough to 124 | ensure earliest execution. 125 | 126 | snooze does not update the timefiles, your job needs to do that! 127 | Only mtime is looked at, so touch(1) is good. 128 | 129 | ## Exact behavior 130 | 131 | * snooze parses the option flags and computes the first time the 132 | date pattern matches, as a symbolic date 133 | * if a timefile is specified, the time is upped to timefile + timewait seconds 134 | * if a random delay is requested, it is added 135 | * snooze computes how far this event is in the future 136 | * snooze sleeps that long, but at most 5 minutes 137 | * after waking, snooze recomputes how far the event is in the future 138 | * if the event is in the past, but fewer than SLACK seconds, snooze 139 | execs the command. You need to ensure (by setting up supervision) 140 | snooze runs again after that! 141 | * if we woke due to a SIGALRM, the command is executed immediately as well 142 | * if we notice time moved backwards, recompute the time until the event 143 | * if the event is in the future, recompute the time it takes, possibly 144 | considering shifting of the system time or timezone changes 145 | (timezone reload only tested on glibc) 146 | * If no command was given, just return with status 0 147 | * and so on... 148 | 149 | ## Common usages 150 | 151 | Run a job like cron, every day at 7am and 7pm: 152 | 153 | exec snooze -H7,19 rdumpfs / /data/dump/mybox 2>&1 154 | 155 | Run a job daily, never twice a day: 156 | 157 | exec snooze -H0 -s 1d -t timefile \ 158 | sh -c 'run-parts /etc/cron.daily; touch timefile' 159 | 160 | Use snooze inline, run a mirror script every hour at 30 minutes past, 161 | but ensure there are at least 20 minutes in between. 162 | 163 | set -e 164 | snooze -H'*' -M30 -t timefile -T 20m 165 | touch timefile # remove this if instantly retrying on failure were ok 166 | mirrorallthethings 167 | touch timefile 168 | 169 | Use snooze inline, cron-style mail: 170 | 171 | set -e 172 | snooze ... 173 | actualjob >output 2>&1 || 174 | mail -s "$(hostname): snooze job failed with status $?" root 197 | has waived all copyright and related or neighboring rights to this work. 198 | 199 | http://creativecommons.org/publicdomain/zero/1.0/ 200 | -------------------------------------------------------------------------------- /snooze.c: -------------------------------------------------------------------------------- 1 | /* 2 | * snooze - run a command at a particular time 3 | * 4 | * To the extent possible under law, Leah Neukirchen 5 | * has waived all copyright and related or neighboring rights to this work. 6 | * http://creativecommons.org/publicdomain/zero/1.0/ 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | static long slack = 60; 22 | #define SLEEP_PHASE 300 23 | static int nflag, vflag; 24 | 25 | static int timewait = -1; 26 | static int randdelay = 0; 27 | static int jitter = 0; 28 | static char *timefile; 29 | 30 | static volatile sig_atomic_t alarm_rang = 0; 31 | 32 | static void 33 | wakeup(int sig) 34 | { 35 | (void)sig; 36 | alarm_rang = 1; 37 | } 38 | 39 | static long 40 | parse_int(char **s, size_t minn, size_t maxn) 41 | { 42 | long n; 43 | char *end; 44 | 45 | errno = 0; 46 | n = strtol(*s, &end, 10); 47 | if (errno) { 48 | perror("strtol"); 49 | exit(1); 50 | } 51 | if (n < (long)minn || n >= (long)maxn) { 52 | fprintf(stderr, "number outside %zd <= n < %zd\n", minn, maxn); 53 | exit(1); 54 | } 55 | *s = end; 56 | return n; 57 | } 58 | 59 | static long 60 | parse_dur(char *s) 61 | { 62 | long n; 63 | char *end; 64 | 65 | errno = 0; 66 | n = strtol(s, &end, 10); 67 | if (errno) { 68 | perror("strtol"); 69 | exit(1); 70 | } 71 | if (n < 0) { 72 | fprintf(stderr, "negative duration\n"); 73 | exit(1); 74 | } 75 | switch (*end) { 76 | case 'm': n *= 60; break; 77 | case 'h': n *= 60*60; break; 78 | case 'd': n *= 24*60*60; break; 79 | case 0: break; 80 | default: 81 | fprintf(stderr, "junk after duration: %s\n", end); 82 | exit(1); 83 | } 84 | return n; 85 | } 86 | 87 | static int 88 | parse(char *expr, char *buf, long bufsiz, int offset) 89 | { 90 | char *s; 91 | long i, n = 0, n0 = 0; 92 | 93 | memset(buf, ' ', bufsiz); 94 | 95 | s = expr; 96 | while (*s) { 97 | switch (*s) { 98 | case '0': case '1': case '2': case '3': case '4': 99 | case '5': case '6': case '7': case '8': case '9': 100 | n = parse_int(&s, -offset, bufsiz); 101 | buf[n+offset] = '*'; 102 | break; 103 | case '-': 104 | n0 = n; 105 | s++; 106 | n = parse_int(&s, -offset, bufsiz); 107 | for (i = n0; i <= n; i++) 108 | buf[i+offset] = '*'; 109 | break; 110 | case '/': 111 | s++; 112 | n0 = n; 113 | if (*s) 114 | n = parse_int(&s, -offset, bufsiz); 115 | if (n == 0) // / = * 116 | n = 1; 117 | for (i = n0; i < bufsiz; i += n) 118 | buf[i+offset] = '*'; 119 | break; 120 | case ',': 121 | s++; 122 | n = 0; 123 | break; 124 | case '*': 125 | s++; 126 | n = 0; 127 | for (i = 0; i < bufsiz; i++) 128 | buf[i+offset] = '*'; 129 | break; 130 | default: 131 | fprintf(stderr, "can't parse: %s %s\n", expr, s); 132 | exit(1); 133 | } 134 | } 135 | 136 | return 0; 137 | } 138 | 139 | char weekday[8] = {0}; 140 | char dayofmonth[32] = {0}; 141 | char month[13] = {0}; 142 | char dayofyear[367] = {0}; 143 | char weekofyear[54] = {0}; 144 | char hour[24] = {0}; 145 | char minute[60] = {0}; 146 | char second[61] = {0}; 147 | 148 | int 149 | isoweek(struct tm *tm) 150 | { 151 | /* ugh, but easier than the correct formula... */ 152 | char weekstr[3]; 153 | char *w = weekstr; 154 | strftime(weekstr, sizeof weekstr, "%V", tm); 155 | return parse_int(&w, 1, 54); 156 | } 157 | 158 | time_t 159 | find_next(time_t from) 160 | { 161 | time_t t; 162 | struct tm *tm; 163 | 164 | t = from; 165 | tm = localtime(&t); 166 | 167 | next_day: 168 | while (!( 169 | weekday[tm->tm_wday] == '*' && 170 | dayofmonth[tm->tm_mday-1] == '*' && 171 | month[tm->tm_mon] == '*' && 172 | weekofyear[isoweek(tm)-1] == '*' && 173 | dayofyear[tm->tm_yday] == '*')) { 174 | if (month[tm->tm_mon] != '*') { 175 | // if month is not good, step month 176 | tm->tm_mon++; 177 | tm->tm_mday = 1; 178 | } else { 179 | tm->tm_mday++; 180 | } 181 | 182 | tm->tm_isdst = -1; 183 | tm->tm_sec = 0; 184 | tm->tm_min = 0; 185 | tm->tm_hour = 0; 186 | 187 | t = mktime(tm); 188 | tm->tm_isdst = -1; 189 | 190 | if (t > from+(366*24*60*60)) // no result within a year 191 | return -1; 192 | } 193 | 194 | int y = tm->tm_yday; // save yday 195 | 196 | while (!( 197 | hour[tm->tm_hour] == '*' && 198 | minute[tm->tm_min] == '*' && 199 | second[tm->tm_sec] == '*')) { 200 | if (hour[tm->tm_hour] != '*') { 201 | tm->tm_hour++; 202 | tm->tm_min = 0; 203 | tm->tm_sec = 0; 204 | } else if (minute[tm->tm_min] != '*') { 205 | tm->tm_min++; 206 | tm->tm_sec = 0; 207 | } else { 208 | tm->tm_sec++; 209 | } 210 | t = mktime(tm); 211 | if (tm->tm_yday != y) // hit a different day, retry... 212 | goto next_day; 213 | } 214 | 215 | if (jitter && !nflag) { 216 | long delay; 217 | delay = lrand48() % jitter; 218 | if (vflag) 219 | printf("adding %lds for jitter.\n", delay); 220 | t += delay; 221 | } 222 | 223 | return t; 224 | } 225 | 226 | static char isobuf[25]; 227 | char * 228 | isotime(const struct tm *tm) 229 | { 230 | strftime(isobuf, sizeof isobuf, "%FT%T%z", tm); 231 | return isobuf; 232 | } 233 | 234 | int 235 | main(int argc, char *argv[]) 236 | { 237 | int c; 238 | time_t t; 239 | time_t now = time(0); 240 | time_t last = 0; 241 | 242 | /* default: every day at 00:00:00 */ 243 | memset(weekday, '*', sizeof weekday); 244 | memset(dayofmonth, '*', sizeof dayofmonth); 245 | memset(month, '*', sizeof month); 246 | memset(dayofyear, '*', sizeof dayofyear); 247 | memset(weekofyear, '*', sizeof weekofyear); 248 | hour[0] = '*'; 249 | minute[0] = '*'; 250 | second[0] = '*'; 251 | 252 | setvbuf(stdout, 0, _IOLBF, 0); 253 | 254 | while ((c = getopt(argc, argv, "+D:W:H:M:S:T:R:J:d:m:ns:t:vw:")) != -1) 255 | switch (c) { 256 | case 'D': parse(optarg, dayofyear, sizeof dayofyear, -1); break; 257 | case 'W': parse(optarg, weekofyear, sizeof weekofyear, -1); break; 258 | case 'H': parse(optarg, hour, sizeof hour, 0); break; 259 | case 'M': parse(optarg, minute, sizeof minute, 0); break; 260 | case 'S': parse(optarg, second, sizeof second, 0); break; 261 | case 'd': parse(optarg, dayofmonth, sizeof dayofmonth, -1); break; 262 | case 'm': parse(optarg, month, sizeof month, -1); break; 263 | case 'w': parse(optarg, weekday, sizeof weekday, 0); 264 | // special case: sunday is both 0 and 7. 265 | if (weekday[7] == '*') 266 | weekday[0] = '*'; 267 | break; 268 | case 'n': nflag++; break; 269 | case 'v': vflag++; break; 270 | case 's': slack = parse_dur(optarg); break; 271 | case 'T': timewait = parse_dur(optarg); break; 272 | case 't': timefile = optarg; break; 273 | case 'R': randdelay = parse_dur(optarg); break; 274 | case 'J': jitter = parse_dur(optarg); break; 275 | default: 276 | fprintf(stderr, "Usage: %s [-nv] [-t timefile] [-T timewait] [-R randdelay] [-J jitter] [-s slack]\n" 277 | " [-d mday] [-m mon] [-w wday] [-D yday] [-W yweek] [-H hour] [-M min] [-S sec] COMMAND...\n" 278 | "Timespec: exact: 1,3,5\n" 279 | " range: 1-7\n" 280 | " every n-th: /10\n", argv[0]); 281 | exit(2); 282 | } 283 | 284 | time_t start = now + 1; 285 | 286 | if (timefile) { 287 | struct stat st; 288 | if (stat(timefile, &st) < 0) { 289 | if (errno != ENOENT) 290 | perror("stat"); 291 | t = start - slack - 1 - timewait; 292 | } else { 293 | t = st.st_mtime + 1; 294 | } 295 | if (timewait == -1) { 296 | while (t < start - slack) 297 | t = find_next(t + 1); 298 | start = t; 299 | } else { 300 | if (t + timewait > start - slack) 301 | start = t + timewait; 302 | } 303 | } 304 | 305 | srand48(getpid() ^ start); 306 | 307 | if (randdelay) { 308 | long delay; 309 | delay = lrand48() % randdelay; 310 | if (vflag) 311 | printf("randomly delaying by %lds.\n", delay); 312 | start += delay; 313 | } 314 | 315 | t = find_next(start); 316 | if (t < 0) { 317 | fprintf(stderr, "no satisfying date found within a year.\n"); 318 | exit(2); 319 | } 320 | 321 | if (nflag) { 322 | /* dry-run, just output the next 5 dates. */ 323 | int i; 324 | for (i = 0; i < 5; i++) { 325 | char weekstr[4]; 326 | struct tm *tm = localtime(&t); 327 | strftime(weekstr, sizeof weekstr, "%a", tm); 328 | printf("%s %s %2dd%3dh%3dm%3ds ", 329 | isotime(tm), 330 | weekstr, 331 | ((int)(t - now) / (60*60*24)), 332 | ((int)(t - now) / (60*60)) % 24, 333 | ((int)(t - now) / 60) % 60, 334 | (int)(t - now) % 60); 335 | if(jitter) { 336 | printf("(plus up to %ds for jitter)\n", jitter); 337 | } else { 338 | printf("\n"); 339 | } 340 | t = find_next(t + 1); 341 | if (t < 0) { 342 | fprintf(stderr, 343 | "no satisfying date found within a year.\n"); 344 | exit(2); 345 | } 346 | } 347 | exit(0); 348 | } 349 | 350 | struct tm *tm = localtime(&t); 351 | if (vflag) 352 | printf("Snoozing until %s\n", isotime(tm)); 353 | 354 | // setup SIGALRM handler to force early execution 355 | struct sigaction sa = { 0 }; 356 | sa.sa_handler = &wakeup; 357 | sa.sa_flags = SA_RESTART; 358 | sigfillset(&sa.sa_mask); 359 | sigaction(SIGALRM, &sa, NULL); 360 | 361 | while (!alarm_rang) { 362 | now = time(0); 363 | if (now < last) { 364 | t = find_next(now); 365 | if (vflag) 366 | printf("Time moved backwards, rescheduled for %s\n", isotime(tm)); 367 | } 368 | t = mktime(tm); 369 | if (t <= now) { 370 | if (now - t <= slack) // still about time 371 | break; 372 | else { // reschedule to next event 373 | if (vflag) 374 | printf("Missed execution at %s\n", isobuf); 375 | t = find_next(now + 1); 376 | tm = localtime(&t); 377 | if (vflag) 378 | printf("Snoozing until %s\n", isotime(tm)); 379 | } 380 | } else { 381 | // do some sleeping, but not more than SLEEP_PHASE 382 | struct timespec ts; 383 | ts.tv_nsec = 0; 384 | ts.tv_sec = t - now > SLEEP_PHASE ? SLEEP_PHASE : t - now; 385 | last = now; 386 | nanosleep(&ts, 0); 387 | // we just iterate again when this exits early 388 | } 389 | } 390 | 391 | if (vflag) { 392 | now = time(0); 393 | tm = localtime(&now); 394 | printf("Starting execution at %s\n", isotime(tm)); 395 | } 396 | 397 | // no command to run, the outside script can go on 398 | if (argc == optind) 399 | return 0; 400 | 401 | execvp(argv[optind], argv+optind); 402 | perror("execvp"); 403 | return 255; 404 | } 405 | --------------------------------------------------------------------------------