/* * 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 volatile 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 = { 0 }; sa.sa_handler = &wakeup; sa.sa_flags = SA_RESTART; sigfillset(&sa.sa_mask); sigaction(SIGALRM, &sa, NULL); 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; }