xref: /dragonfly/usr.bin/calendar/calendar.c (revision 2b7dbe20)
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
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
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
467 handle_sigchld(int signo __unused)
468 {
469 	/* empty; just let the main() to reap the child */
470 }
471 
472 static double
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
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
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
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
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
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
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