1 /*-
2 * SPDX-License-Identifier: BSD-3-Clause
3 *
4 * Copyright (c) 2020 The DragonFly Project. All rights reserved.
5 * Copyright (c) 1989, 1993, 1994
6 * The Regents of the University of California. All rights reserved.
7 *
8 * This code is derived from software contributed to The DragonFly Project
9 * by Aaron LI <aly@aaronly.me>
10 *
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions
13 * are met:
14 * 1. Redistributions of source code must retain the above copyright
15 * notice, this list of conditions and the following disclaimer.
16 * 2. Redistributions in binary form must reproduce the above copyright
17 * notice, this list of conditions and the following disclaimer in the
18 * documentation and/or other materials provided with the distribution.
19 * 3. Neither the name of the University nor the names of its contributors
20 * may be used to endorse or promote products derived from this software
21 * without specific prior written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
27 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33 * SUCH DAMAGE.
34 *
35 * @(#)calendar.c 8.3 (Berkeley) 3/25/94
36 * $FreeBSD: head/usr.bin/calendar/calendar.c 326025 2017-11-20 19:49:47Z pfg $
37 */
38
39 #include <sys/param.h>
40 #include <sys/types.h>
41 #include <sys/wait.h>
42
43 #include <err.h>
44 #include <grp.h> /* required on Linux for initgroups() */
45 #include <locale.h>
46 #include <math.h>
47 #include <pwd.h>
48 #include <signal.h>
49 #include <stdarg.h>
50 #include <stdbool.h>
51 #include <stdio.h>
52 #include <stdlib.h>
53 #include <string.h>
54 #include <time.h>
55 #include <unistd.h>
56
57 #include "calendar.h"
58 #include "basics.h"
59 #include "chinese.h"
60 #include "dates.h"
61 #include "days.h"
62 #include "gregorian.h"
63 #include "io.h"
64 #include "julian.h"
65 #include "moon.h"
66 #include "nnames.h"
67 #include "parsedata.h"
68 #include "sun.h"
69 #include "utils.h"
70
71
72 struct cal_options Options = {
73 .time = 0.5, /* noon */
74 .allmode = false,
75 .debug = 0,
76 };
77
78 /* paths to search for calendar files for inclusion */
79 const char *calendarDirs[] = {
80 ".", /* i.e., '~/.calendar' */
81 CALENDAR_ETCDIR,
82 CALENDAR_DIR,
83 NULL,
84 };
85
86 /* currently selected calendar to use */
87 struct calendar *Calendar;
88
89 /* all supported calendars */
90 static struct calendar calendars[] = {
91 { /* the default */
92 .id = CAL_GREGORIAN,
93 .name = "Gregorian",
94 .format_date = NULL,
95 .find_days_ymd = find_days_ymd,
96 .find_days_dom = find_days_dom,
97 .find_days_month = find_days_month,
98 .find_days_mdow = find_days_mdow,
99 },
100 {
101 .id = CAL_JULIAN,
102 .name = "Julian",
103 .format_date = julian_format_date,
104 .find_days_ymd = julian_find_days_ymd,
105 .find_days_dom = julian_find_days_dom,
106 .find_days_month = julian_find_days_month,
107 .find_days_mdow = NULL,
108 },
109 {
110 .id = CAL_CHINESE,
111 .name = "Chinese",
112 .format_date = chinese_format_date,
113 .find_days_ymd = chinese_find_days_ymd,
114 .find_days_dom = chinese_find_days_dom,
115 .find_days_month = NULL,
116 .find_days_mdow = NULL,
117 },
118 };
119
120 /* user's calendar home directory (relative to $HOME) */
121 static const char *calendarHome = ".calendar";
122 /* default calendar file to use if exists in current dir or ~/.calendar */
123 static const char *calendarFile = "calendar";
124 /* system-wide calendar file to use if user doesn't have one */
125 static const char *calendarFileSys = CALENDAR_ETCDIR "/default";
126 /* don't send mail if this file exists in ~/.calendar */
127 static const char *calendarNoMail = "nomail";
128 /* maximum time in seconds that 'calendar -a' can spend for each user */
129 static const int user_timeout = 10;
130 /* maximum time in seconds that 'calendar -a' can spend in total */
131 static const int total_timeout = 3600;
132
133 static bool cd_home(const char *home);
134 static int get_fixed_of_today(void);
135 static double get_time_of_now(void);
136 static int get_utc_offset(void);
137 static void handle_sigchld(int signo __unused);
138 static void print_datetime(double t, const struct location *loc);
139 static void print_location(const struct location *loc, bool warn);
140 static void usage(const char *progname) __dead2;
141
142
143 bool
set_calendar(const char * name)144 set_calendar(const char *name)
145 {
146 struct calendar *cal;
147
148 if (name == NULL) {
149 Calendar = &calendars[0];
150 return true;
151 }
152
153 for (size_t i = 0; i < nitems(calendars); i++) {
154 cal = &calendars[i];
155 if (strcasecmp(name, cal->name) == 0) {
156 Calendar = cal;
157 return true;
158 }
159 }
160
161 warnx("%s: unknown calendar: |%s|", __func__, name);
162 return false;
163 }
164
165
166 int
main(int argc,char * argv[])167 main(int argc, char *argv[])
168 {
169 bool L_flag = false;
170 int ret = 0;
171 int days_before = 0;
172 int days_after = 0;
173 int Friday = 5; /* days before weekend */
174 int dow;
175 int ch, utc_offset;
176 struct passwd *pw;
177 struct location loc = { 0 };
178 const char *show_info = NULL;
179 const char *calfile = NULL;
180 const char *calhome = NULL;
181 const char *optstring;
182 FILE *fp = NULL;
183
184 Options.location = &loc;
185 Options.time = get_time_of_now();
186 Options.today = get_fixed_of_today();
187 loc.zone = get_utc_offset() / (3600.0 * 24.0);
188
189 optstring = "-A:aB:dF:f:hH:L:l:s:T:t:U:W:";
190 while ((ch = getopt(argc, argv, optstring)) != -1) {
191 switch (ch) {
192 case '-': /* backward compatible */
193 case 'a':
194 if (getuid() != 0)
195 errx(1, "must be root to run with '-a'");
196 Options.allmode = true;
197 break;
198
199 case 'W': /* don't need to specially deal with Fridays */
200 Friday = -1;
201 /* FALLTHROUGH */
202 case 'A': /* days after current date */
203 days_after = (int)strtol(optarg, NULL, 10);
204 if (days_after < 0)
205 errx(1, "number of days must be positive");
206 break;
207
208 case 'B': /* days before current date */
209 days_before = (int)strtol(optarg, NULL, 10);
210 if (days_before < 0)
211 errx(1, "number of days must be positive");
212 break;
213
214 case 'd': /* show debug information */
215 Options.debug++;
216 break;
217
218 case 'F': /* change when the weekend starts */
219 Friday = (int)strtol(optarg, NULL, 10);
220 break;
221
222 case 'f': /* other calendar file */
223 calfile = optarg;
224 if (strcmp(optarg, "-") == 0)
225 calfile = "/dev/stdin";
226 break;
227
228 case 'H': /* calendar home directory */
229 calhome = optarg;
230 break;
231
232 case 'L': /* location */
233 if (!parse_location(optarg, &loc.latitude,
234 &loc.longitude, &loc.elevation)) {
235 errx(1, "invalid location: |%s|", optarg);
236 }
237 L_flag = true;
238 break;
239
240 case 's': /* show info of specified category */
241 show_info = optarg;
242 break;
243
244 case 'T': /* specify time of day */
245 if (!parse_time(optarg, &Options.time))
246 errx(1, "invalid time: |%s|", optarg);
247 break;
248
249 case 't': /* specify date */
250 if (!parse_date(optarg, &Options.today))
251 errx(1, "invalid date: |%s|", optarg);
252 break;
253
254 case 'U': /* specify timezone */
255 if (!parse_timezone(optarg, &utc_offset))
256 errx(1, "invalid timezone: |%s|", optarg);
257 loc.zone = utc_offset / (3600.0 * 24.0);
258 break;
259
260 case 'h':
261 default:
262 usage(argv[0]);
263 }
264 }
265
266 if (argc > optind)
267 usage(argv[0]);
268
269 if (Options.allmode && calfile != NULL)
270 errx(1, "flags -a and -f cannot be used together");
271 if (Options.allmode && calhome != NULL)
272 errx(1, "flags -a and -H cannot be used together");
273
274 if (!L_flag)
275 loc.longitude = loc.zone * 360.0;
276
277 /* Friday displays Monday's events */
278 dow = dayofweek_from_fixed(Options.today);
279 if (days_after == 0 && Friday != -1)
280 days_after = (dow == Friday) ? 3 : 1;
281
282 Options.day_begin = Options.today - days_before;
283 Options.day_end = Options.today + days_after;
284 generate_dates();
285 set_calendar(NULL);
286
287 setlocale(LC_ALL, "");
288 set_nnames();
289
290 if (setenv("TZ", "UTC", 1) != 0)
291 err(1, "setenv");
292 tzset();
293 /* We're in UTC from now on */
294
295 if (show_info != NULL) {
296 double t = Options.today + Options.time;
297 if (strcmp(show_info, "chinese") == 0) {
298 show_chinese_calendar(Options.today);
299 } else if (strcmp(show_info, "julian") == 0) {
300 show_julian_calendar(Options.today);
301 } else if (strcmp(show_info, "moon") == 0) {
302 print_datetime(t, Options.location);
303 print_location(Options.location, !L_flag);
304 show_moon_info(t, Options.location);
305 } else if (strcmp(show_info, "sun") == 0) {
306 print_datetime(t, Options.location);
307 print_location(Options.location, !L_flag);
308 show_sun_info(t, Options.location);
309 } else {
310 errx(1, "unknown -s value: |%s|", show_info);
311 }
312
313 exit(0);
314 }
315
316 if (Options.allmode) {
317 pid_t kid, deadkid, gkid;
318 time_t t;
319 bool reaped;
320 int kidstat, runningkids;
321 unsigned int sleeptime;
322
323 if (signal(SIGCHLD, handle_sigchld) == SIG_ERR)
324 err(1, "signal");
325 runningkids = 0;
326 t = time(NULL);
327
328 while ((pw = getpwent()) != NULL) {
329 /*
330 * Enter '~/.calendar' and only try 'calendar'
331 */
332 if (!cd_home(pw->pw_dir))
333 continue;
334 if (access(calendarNoMail, F_OK) == 0)
335 continue;
336 if ((fp = fopen(calendarFile, "r")) == NULL)
337 continue;
338
339 sleeptime = user_timeout;
340 kid = fork();
341 if (kid < 0) {
342 warn("fork");
343 continue;
344 }
345 if (kid == 0) {
346 gkid = getpid();
347 if (setpgid(gkid, gkid) == -1)
348 err(1, "setpgid");
349 if (setgid(pw->pw_gid) == -1)
350 err(1, "setgid(%u)", pw->pw_gid);
351 if (initgroups(pw->pw_name, pw->pw_gid) == -1)
352 err(1, "initgroups(%s)", pw->pw_name);
353 if (setuid(pw->pw_uid) == -1)
354 err(1, "setuid(%u)", pw->pw_uid);
355
356 ret = cal(fp);
357 fclose(fp);
358 _exit(ret);
359 }
360 /*
361 * Parent: wait a reasonable time, then kill child
362 * if necessary.
363 */
364 runningkids++;
365 reaped = false;
366 do {
367 sleeptime = sleep(sleeptime);
368 /*
369 * Note that there is the possibility, if the
370 * sleep stops early due to some other signal,
371 * of the child terminating and not getting
372 * detected during the next sleep. In that
373 * unlikely worst case, we just sleep too long
374 * for that user.
375 */
376 for (;;) {
377 deadkid = waitpid(-1, &kidstat, WNOHANG);
378 if (deadkid <= 0)
379 break;
380 runningkids--;
381 if (deadkid == kid) {
382 reaped = true;
383 sleeptime = 0;
384 }
385 }
386 } while (sleeptime);
387
388 if (!reaped) {
389 /*
390 * It doesn't really matter if the kill fails;
391 * there is only one more zombie now.
392 */
393 gkid = getpgid(kid);
394 if (gkid != getpgrp())
395 killpg(gkid, SIGTERM);
396 else
397 kill(kid, SIGTERM);
398 warnx("user %s (uid %u) did not finish in time "
399 "(%d seconds)",
400 pw->pw_name, pw->pw_uid, user_timeout);
401 }
402
403 if (time(NULL) - t > total_timeout) {
404 errx(2, "'calendar -a' timed out (%d seconds); "
405 "stop at user %s (uid %u)",
406 total_timeout, pw->pw_name, pw->pw_uid);
407 }
408 }
409
410 for (;;) {
411 deadkid = waitpid(-1, &kidstat, WNOHANG);
412 if (deadkid <= 0)
413 break;
414 runningkids--;
415 }
416 if (runningkids) {
417 warnx("%d child processes still running when "
418 "'calendar -a' finished", runningkids);
419 }
420
421 } else {
422 if (calfile && (fp = fopen(calfile, "r")) == NULL)
423 errx(1, "Cannot open calendar file: '%s'", calfile);
424
425 /* try 'calendar' in current directory */
426 if (fp == NULL)
427 fp = fopen(calendarFile, "r");
428
429 if (calhome) {
430 if (chdir(calhome) == -1)
431 errx(1, "Cannot enter home: '%s'", calhome);
432 /* try 'calendar' in home directory */
433 if (fp == NULL)
434 fp = fopen(calendarFile, "r");
435 } else if (cd_home(NULL)) { /* try to enter '~/.calendar' */
436 /* try 'calendar' in home directory */
437 if (fp == NULL)
438 fp = fopen(calendarFile, "r");
439 } else {
440 DPRINTF("Fallback to enter '%s'\n", calendarDirs[1]);
441 /* fallback to '/etc/calendar' as home directory */
442 if (chdir(calendarDirs[1]) == -1)
443 errx(1, "Cannot enter directory: '%s'",
444 calendarDirs[1]);
445 }
446
447 /* fallback to '/etc/calendar/default' */
448 if (fp == NULL) {
449 warnx("No user's calendar file; "
450 "fallback to system default: '%s'",
451 calendarFileSys);
452 fp = fopen(calendarFileSys, "r");
453 if (fp == NULL)
454 errx(1, "Cannot find calendar file");
455 }
456
457 ret = cal(fp);
458 fclose(fp);
459 }
460
461 free_dates();
462 return (ret);
463 }
464
465
466 static void
handle_sigchld(int signo __unused)467 handle_sigchld(int signo __unused)
468 {
469 /* empty; just let the main() to reap the child */
470 }
471
472 static double
get_time_of_now(void)473 get_time_of_now(void)
474 {
475 time_t now;
476 struct tm tm;
477
478 now = time(NULL);
479 tzset();
480 localtime_r(&now, &tm);
481
482 return (tm.tm_hour + tm.tm_min/60.0 + tm.tm_sec/3600.0) / 24.0;
483 }
484
485 static int
get_fixed_of_today(void)486 get_fixed_of_today(void)
487 {
488 time_t now;
489 struct tm tm;
490 struct date gdate;
491
492 now = time(NULL);
493 tzset();
494 localtime_r(&now, &tm);
495 date_set(&gdate, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
496
497 return fixed_from_gregorian(&gdate);
498 }
499
500 static int
get_utc_offset(void)501 get_utc_offset(void)
502 {
503 time_t now;
504 struct tm tm;
505
506 now = time(NULL);
507 tzset();
508 localtime_r(&now, &tm);
509
510 return tm.tm_gmtoff;
511 }
512
513 static bool
cd_home(const char * home)514 cd_home(const char *home)
515 {
516 char path[MAXPATHLEN];
517
518 if (home == NULL) {
519 home = getenv("HOME");
520 if (home == NULL || *home == '\0') {
521 warnx("Cannot get '$HOME'");
522 return false;
523 }
524 }
525
526 snprintf(path, sizeof(path), "%s/%s", home, calendarHome);
527 if (chdir(path) == -1) {
528 DPRINTF("Cannot enter home directory: '%s'\n", path);
529 return false;
530 }
531
532 return true;
533 }
534
535 static void
print_datetime(double t,const struct location * loc)536 print_datetime(double t, const struct location *loc)
537 {
538 struct date date;
539 char buf[64];
540
541 gregorian_from_fixed(floor(t), &date);
542 printf("Gregorian date: %d-%02d-%02d\n",
543 date.year, date.month, date.day);
544
545 format_time(buf, sizeof(buf), t);
546 printf("Time: %s", buf);
547 if (loc != NULL) {
548 format_zone(buf, sizeof(buf), loc->zone);
549 printf(" %s\n", buf);
550 } else {
551 printf("\n");
552 }
553 }
554
555 static void
print_location(const struct location * loc,bool warn)556 print_location(const struct location *loc, bool warn)
557 {
558 char buf[64];
559
560 format_location(buf, sizeof(buf), loc);
561 printf("Location: %s%s\n", buf,
562 warn ? " [WARNING: use '-L' to specify]" : "");
563 }
564
565 static void __dead2
usage(const char * progname)566 usage(const char *progname)
567 {
568 fprintf(stderr,
569 "usage:\n"
570 "%s [-A days] [-a] [-B days] [-d] [-F friday]\n"
571 "\t[-f calendar_file] [-H calendar_home]\n"
572 "\t[-L latitude,longitude[,elevation]] [-s category]\n"
573 "\t[-T hh:mm[:ss]] [-t [[[CC]YY]MM]DD] [-U ±hh[[:]mm]] [-W days]\n",
574 progname);
575 exit(1);
576 }
577