]> git.netwichtig.de Git - user/henk/code/snooze.git/blob - snooze.c
Log starting time with -v
[user/henk/code/snooze.git] / snooze.c
1 /*
2  * snooze - run a command at a particular time
3  *
4  * To the extent possible under law,
5  * Christian Neukirchen <chneukirchen@gmail.com>
6  * has waived all copyright and related or neighboring rights to this work.
7  * http://creativecommons.org/publicdomain/zero/1.0/
8  */
9
10 /*
11 ##% gcc -Os -Wall -g -o $STEM $FILE -Wextra -Wwrite-strings
12 */
13
14 #include <sys/stat.h>
15 #include <sys/types.h>
16
17 #include <ctype.h>
18 #include <errno.h>
19 #include <signal.h>
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <time.h>
24 #include <unistd.h>
25
26 #ifdef __linux__
27 #include <sys/auxv.h>
28 #endif
29
30 static long slack = 60;
31 #define SLEEP_PHASE 300
32 static int nflag, vflag;
33
34 static int timewait = 1;
35 static int randdelay = 0;
36 static char *timefile;
37
38 static sig_atomic_t alarm_rang = 0;
39
40 static void
41 wakeup(int sig)
42 {
43         (void)sig;
44         alarm_rang = 1;
45 }
46
47 static long
48 parse_int(char **s, size_t minn, size_t maxn)
49 {
50         long n;
51         char *end;
52
53         errno = 0;
54         n = strtol(*s, &end, 10);
55         if (errno) {
56                 perror("strtol");
57                 exit(1);
58         }
59         if (n < (long)minn || n >= (long)maxn) {
60                 fprintf(stderr, "number outside %zd <= n < %zd\n", minn, maxn);
61                 exit(1);
62         }
63         *s = end;
64         return n;
65 }
66
67 static int
68 parse(char *expr, char *buf, long bufsiz, int offset)
69 {
70         char *s;
71         long i, n = 0, n0 = 0;
72
73         memset(buf, ' ', bufsiz);
74
75         s = expr;
76         while (*s) {
77                 switch (*s) {
78                 case '0': case '1': case '2': case '3': case '4':
79                 case '5': case '6': case '7': case '8': case '9':
80                         n = parse_int(&s, -offset, bufsiz);
81                         buf[n+offset] = '*';
82                         break;
83                 case '-':
84                         n0 = n;
85                         s++;
86                         n = parse_int(&s, -offset, bufsiz);
87                         for (i = n0; i <= n; i++)
88                                 buf[i+offset] = '*';
89                         break;
90                 case '/':
91                         s++;
92                         n0 = n;
93                         n = parse_int(&s, -offset, bufsiz);
94                         if (n == 0)  // / = *
95                                 n = 1;
96                         for (i = n0; i < bufsiz; i += n)
97                                 buf[i+offset] = '*';
98                         break;
99                 case ',':
100                         s++;
101                         n = 0;
102                         break;
103                 case '*':
104                         s++;
105                         n = 0;
106                         for (i = 0; i < bufsiz; i++)
107                                 buf[i+offset] = '*';
108                         break;
109                 default:
110                         fprintf(stderr, "can't parse: %s %s\n", expr, s);
111                         exit(1);
112                 }
113         }
114
115         return 0;
116 }
117
118 char weekday[8] = {0};
119 char dayofmonth[31] = {0};
120 char month[12] = {0};
121 char dayofyear[366] = {0};
122 char weekofyear[53] = {0};
123 char hour[24] = {0};
124 char minute[60] = {0};
125 char second[61] = {0};
126
127 int
128 isoweek(struct tm *tm)
129 {
130         /* ugh, but easier than the correct formula... */
131         char weekstr[3];
132         char *w = weekstr;
133         strftime(weekstr, sizeof weekstr, "%V", tm);
134         return parse_int(&w, 1, 54);
135 }
136
137 time_t
138 find_next(time_t from)
139 {
140         time_t t;
141         struct tm *tm;
142
143         t = from;
144         tm = localtime(&t);
145
146 next_day:
147         while (!(weekday[tm->tm_wday] == '*'
148             && dayofmonth[tm->tm_mday-1] == '*'
149             && month[tm->tm_mon] == '*'
150             && weekofyear[isoweek(tm)] == '*'
151             && dayofyear[tm->tm_yday] == '*')) {
152                 if (month[tm->tm_mon] != '*') {
153                         // if month is not good, step month
154                         tm->tm_mon++;
155                         tm->tm_mday = 1;
156                 } else {
157                         tm->tm_mday++;
158                 }
159
160                 tm->tm_sec = 0;
161                 tm->tm_min = 0;
162                 tm->tm_hour = 0;
163
164                 t = mktime(tm);
165                 if (t > from+(365*24*60*60))  // no result within a year
166                         return -1;
167         }
168
169         int y = tm->tm_yday;  // save yday
170
171         while (!(hour[tm->tm_hour] == '*'
172             && minute[tm->tm_min] == '*'
173             && second[tm->tm_sec] == '*')) {
174                 if (hour[tm->tm_hour] != '*') {
175                         tm->tm_hour++;
176                         tm->tm_min = 0;
177                         tm->tm_sec = 0;
178                 } else if (minute[tm->tm_min] != '*') {
179                         tm->tm_min++;
180                         tm->tm_sec = 0;
181                 } else {
182                         tm->tm_sec++;
183                 }
184                 t = mktime(tm);
185                 if (tm->tm_yday != y)  // hit a different day, retry...
186                         goto next_day;
187         }
188             
189         return t;
190 }
191
192 static char isobuf[25];
193 char *
194 isotime(const struct tm *tm)
195 {
196         strftime(isobuf, sizeof isobuf, "%FT%T%z", tm);
197         return isobuf;
198 }
199
200 int main(int argc, char *argv[])
201 {
202         int c;
203         time_t now = time(0);
204
205         /* default: every day at 00:00:00 */
206         memset(weekday, '*', sizeof weekday);
207         memset(dayofmonth, '*', sizeof dayofmonth);
208         memset(month, '*', sizeof month);
209         memset(dayofyear, '*', sizeof dayofyear);
210         memset(weekofyear, '*', sizeof weekofyear);
211         hour[0] = '*';
212         minute[0] = '*';
213         second[0] = '*';
214
215         while ((c = getopt(argc, argv, "D:W:H:M:S:T:R:d:m:ns:t:vw:")) != -1)
216                 switch(c) {
217                 case 'D': parse(optarg, dayofyear, sizeof dayofyear, -1); break;
218                 case 'W': parse(optarg, weekofyear, sizeof weekofyear, -1); break;
219                 case 'H': parse(optarg, hour, sizeof hour, 0); break;
220                 case 'M': parse(optarg, minute, sizeof minute, 0); break;
221                 case 'S': parse(optarg, second, sizeof second, 0); break;
222                 case 'd': parse(optarg, dayofmonth, sizeof dayofmonth, -1); break;
223                 case 'm': parse(optarg, month, sizeof month, -1); break;
224                 case 'w': parse(optarg, weekday, sizeof weekday, 0);
225                         // special case: sunday is both 0 and 7.
226                         if (weekday[7] == '*') 
227                                 weekday[0] = '*';
228                         break;
229                 case 'n': nflag++; break;
230                 case 'v': vflag++; break;
231                 case 's': slack = atoi(optarg); break;
232                 case 'T': timewait = atoi(optarg); break;
233                 case 't': timefile = optarg; break;
234                 case 'R': randdelay = atoi(optarg); break;
235                 default:
236                         fprintf(stderr, "Usage: %s [-nv] [-t timefile] [-T timewait] [-R randdelay] [-s slack]\n"
237                             "  [-d mday] [-m mon] [-w wday] [-D yday] [-W yweek] [-H hour] [-M min] [-S sec] COMMAND...\n"
238                             "Timespec: exact: 1,3,5\n"
239                             "          range: 1-7\n"
240                             "          every n-th: /10\n", argv[0]);
241                         exit(2);
242                 }
243
244         time_t start = now + 1;
245
246         if (timefile) {
247                 struct stat st;
248                 if (stat(timefile, &st) < 0) {
249                         if (errno != ENOENT)
250                                 perror("stat");
251                 } else {
252                         if (st.st_mtime + timewait > start)
253                                 start = st.st_mtime + timewait;
254                 }
255         }
256
257         if (randdelay) {
258                 long delay;
259 #ifdef __linux__
260                 long rnd = getauxval(AT_RANDOM);
261                 if (rnd > 0)
262                         delay = rnd % randdelay;
263                 else
264 #endif
265                 {
266                         srand48(getpid() ^ start);
267                         delay = lrand48() % randdelay;
268                 }
269                 if (vflag)
270                         printf("randomly delaying by %lds.\n", delay);
271                 start += delay;
272         }
273
274         time_t t = find_next(start);
275         if (t < 0) {
276                 fprintf(stderr, "no satisfying date found within a year.\n");
277                 exit(2);
278         }
279
280         if (nflag) {
281                 /* dry-run, just output the next 5 dates. */
282                 int i;
283                 for (i = 0; i < 5; i++) {
284                         if (t > 0)
285                                 printf("%s\n", isotime(localtime(&t)));
286                         t = find_next(t + 1);
287                 }
288                 exit(0);
289         }
290
291         struct tm *tm = localtime(&t);
292         if (vflag)
293                 printf("Snoozing until %s\n", isotime(tm));
294
295         // setup SIGALRM handler to force early execution
296         struct sigaction sa;
297         sa.sa_handler = &wakeup;
298         sa.sa_flags = SA_RESTART;
299         sigfillset(&sa.sa_mask);
300         sigaction(SIGALRM, &sa, NULL);  // XXX error handling
301
302         while (!alarm_rang) {
303                 now = time(0);
304                 t = mktime(tm);
305                 if (t <= now) {
306                         if (now - t <= slack)  // still about time
307                                 break;
308                         else {  // reschedule to next event
309                                 if (vflag)
310                                         printf("Missed execution at %s\n", isobuf);
311                                 t = find_next(now + 1);
312                                 tm = localtime(&t);
313                                 if (vflag)
314                                         printf("Snoozing until %s\n", isotime(tm));
315                         }
316                 } else {
317                         // do some sleeping, but not more than SLEEP_PHASE
318                         struct timespec ts;
319                         ts.tv_nsec = 0;
320                         ts.tv_sec = t - now > SLEEP_PHASE ? SLEEP_PHASE : t - now;
321                         nanosleep(&ts, 0);
322                         // we just iterate again when this exits early
323                 }
324         }
325
326         // no command to run, the outside script can go on
327         if (argc == optind)
328                 return 0;
329
330         if (vflag) {
331                 now = time(0);
332                 tm = localtime(&now);
333                 printf("Starting execution at %s\n", isotime(tm));
334         }
335
336         execvp(argv[optind], argv+optind);
337         perror("execvp");
338         return 255;
339 }