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