xref: /illumos-gate/usr/src/cmd/logadm/main.c (revision 1e9e241fbdc7ae75a7b37c1e7afc8c96d2f43e1e)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright (c) 2001, 2010, Oracle and/or its affiliates. All rights reserved.
23  * Copyright 2019 Joyent, Inc.
24  *
25  * logadm/main.c -- main routines for logadm
26  *
27  * this program is 90% argument processing, 10% actions...
28  */
29 
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <unistd.h>
33 #include <strings.h>
34 #include <libintl.h>
35 #include <locale.h>
36 #include <fcntl.h>
37 #include <sys/types.h>
38 #include <sys/stat.h>
39 #include <sys/wait.h>
40 #include <sys/filio.h>
41 #include <sys/sysmacros.h>
42 #include <time.h>
43 #include <utime.h>
44 #include <poll.h>
45 #include <errno.h>
46 #include "err.h"
47 #include "lut.h"
48 #include "fn.h"
49 #include "opts.h"
50 #include "conf.h"
51 #include "glob.h"
52 #include "kw.h"
53 
54 /* forward declarations for functions in this file */
55 static void usage(const char *msg);
56 static void commajoin(const char *lhs, void *rhs, void *arg);
57 static void doaftercmd(const char *lhs, void *rhs, void *arg);
58 static void dologname(struct fn *fnp, struct opts *clopts);
59 static boolean_t rotatelog(struct fn *fnp, struct opts *opts);
60 static void rotateto(struct fn *fnp, struct opts *opts, int n,
61     struct fn *recentlog, boolean_t isgz);
62 static void do_delayed_gzip(const char *lhs, void *rhs, void *arg);
63 static void expirefiles(struct fn *fnp, struct opts *opts);
64 static void dorm(struct opts *opts, const char *msg, struct fn *fnp);
65 static void docmd(struct opts *opts, const char *msg, const char *cmd,
66     const char *arg1, const char *arg2, const char *arg3);
67 static void docopytruncate(struct opts *opts, const char *file,
68     const char *file_copy);
69 
70 /* our configuration file, unless otherwise specified by -f */
71 static char *Default_conffile = "/etc/logadm.conf";
72 /* our timestamps file, unless otherwise specified by -F */
73 static char *Default_timestamps = "/var/logadm/timestamps";
74 
75 /* default pathnames to the commands we invoke */
76 static char *Sh = "/bin/sh";
77 static char *Mv = "/bin/mv";
78 static char *Rm = "/bin/rm";
79 static char *Touch = "/bin/touch";
80 static char *Chmod = "/bin/chmod";
81 static char *Chown = "/bin/chown";
82 static char *Gzip = "/bin/gzip";
83 static char *Mkdir = "/bin/mkdir";
84 
85 int Debug;
86 
87 /* return from time(0), gathered early on to avoid slewed timestamps */
88 time_t Now;
89 
90 /* list of before commands that have been executed */
91 static struct lut *Beforecmds;
92 
93 /* list of after commands to execute before exiting */
94 static struct lut *Aftercmds;
95 
96 /* list of conffile entry names that are considered "done" */
97 static struct lut *Donenames;
98 
99 /* A list of names of files to be gzipped */
100 static struct lut *Gzipnames = NULL;
101 
102 /*
103  * only the "FfhnVv" options are allowed in the first form of this command,
104  * so this defines the list of options that are an error in they appear
105  * in the first form.  In other words, it is not allowed to run logadm
106  * with any of these options unless at least one logname is also provided.
107  */
108 #define	OPTIONS_NOT_FIRST_FORM	"eNrwpPsabcglmoRtzACEST"
109 
110 /* text that we spew with the -h flag */
111 #define	HELP1 \
112 "Usage: logadm [options]\n"\
113 "       (processes all entries in /etc/logadm.conf or conffile given by -f)\n"\
114 "   or: logadm [options] logname...\n"\
115 "       (processes the given lognames)\n"\
116 "\n"\
117 "General options:\n"\
118 "        -e mailaddr     mail errors to given address\n"\
119 "        -F timestamps   use timestamps instead of /var/logadm/timestamps\n"\
120 "        -f conffile     use conffile instead of /etc/logadm.conf\n"\
121 "        -h              display help\n"\
122 "        -N              not an error if log file nonexistent\n"\
123 "        -n              show actions, don't perform them\n"\
124 "        -r              remove logname entry from conffile\n"\
125 "        -V              ensure conffile entries exist, correct\n"\
126 "        -v              print info about actions happening\n"\
127 "        -w entryname    write entry to config file\n"\
128 "\n"\
129 "Options which control when a logfile is rotated:\n"\
130 "(default is: -s1b -p1w if no -s or -p)\n"\
131 "        -p period       only rotate if period passed since last rotate\n"\
132 "        -P timestamp    used to store rotation date in conffile\n"\
133 "        -s size         only rotate if given size or greater\n"\
134 "\n"
135 #define	HELP2 \
136 "Options which control how a logfile is rotated:\n"\
137 "(default is: -t '$file.$n', owner/group/mode taken from log file)\n"\
138 "        -a cmd          execute cmd after taking actions\n"\
139 "        -b cmd          execute cmd before taking actions\n"\
140 "        -c              copy & truncate logfile, don't rename\n"\
141 "        -g group        new empty log file group\n"\
142 "        -l              rotate log file with local time rather than UTC\n"\
143 "        -m mode         new empty log file mode\n"\
144 "        -M cmd          execute cmd to rotate the log file\n"\
145 "        -o owner        new empty log file owner\n"\
146 "        -R cmd          run cmd on file after rotate\n"\
147 "        -t template     template for naming old logs\n"\
148 "        -z count        gzip old logs except most recent count\n"\
149 "\n"\
150 "Options which control the expiration of old logfiles:\n"\
151 "(default is: -C10 if no -A, -C, or -S)\n"\
152 "        -A age          expire logs older than age\n"\
153 "        -C count        expire old logs until count remain\n"\
154 "        -E cmd          run cmd on file to expire\n"\
155 "        -S size         expire until space used is below size \n"\
156 "        -T pattern      pattern for finding old logs\n"
157 
158 /*
159  * main -- where it all begins
160  */
161 /*ARGSUSED*/
162 int
163 main(int argc, char *argv[])
164 {
165 	struct opts *clopts = NULL;	/* from parsing command line */
166 	const char *conffile;		/* our configuration file */
167 	const char *timestamps;		/* our timestamps file */
168 	struct fn_list *lognames;	/* list of lognames we're processing */
169 	struct fn *fnp;
170 	char *val;
171 	char *buf;
172 	int status;
173 
174 	(void) setlocale(LC_ALL, "");
175 
176 #if !defined(TEXT_DOMAIN)
177 #define	TEXT_DOMAIN "SYS_TEST"	/* only used if Makefiles don't define it */
178 #endif
179 
180 	(void) textdomain(TEXT_DOMAIN);
181 
182 	/* we only print times into the timestamps file, so make them uniform */
183 	(void) setlocale(LC_TIME, "C");
184 
185 	/* give our name to error routines & skip it for arg parsing */
186 	err_init(*argv++);
187 	(void) setlinebuf(stdout);
188 
189 	if (putenv("PATH=/bin"))
190 		err(EF_SYS, "putenv PATH");
191 	if (putenv("TZ=UTC"))
192 		err(EF_SYS, "putenv TZ");
193 	tzset();
194 
195 	(void) umask(0);
196 
197 	Now = time(0);
198 
199 	/* check for (undocumented) debugging environment variables */
200 	if (val = getenv("_LOGADM_DEFAULT_CONFFILE"))
201 		Default_conffile = val;
202 	if (val = getenv("_LOGADM_DEFAULT_TIMESTAMPS"))
203 		Default_timestamps = val;
204 	if (val = getenv("_LOGADM_DEBUG"))
205 		Debug = atoi(val);
206 	if (val = getenv("_LOGADM_SH"))
207 		Sh = val;
208 	if (val = getenv("_LOGADM_MV"))
209 		Mv = val;
210 	if (val = getenv("_LOGADM_RM"))
211 		Rm = val;
212 	if (val = getenv("_LOGADM_TOUCH"))
213 		Touch = val;
214 	if (val = getenv("_LOGADM_CHMOD"))
215 		Chmod = val;
216 	if (val = getenv("_LOGADM_CHOWN"))
217 		Chown = val;
218 	if (val = getenv("_LOGADM_GZIP"))
219 		Gzip = val;
220 	if (val = getenv("_LOGADM_MKDIR"))
221 		Mkdir = val;
222 
223 	opts_init(Opttable, Opttable_cnt);
224 
225 	/* parse command line arguments */
226 	if (SETJMP)
227 		usage("bailing out due to command line errors");
228 	else
229 		clopts = opts_parse(NULL, argv, OPTF_CLI);
230 
231 	if (Debug) {
232 		(void) fprintf(stderr, "command line opts:");
233 		opts_print(clopts, stderr, NULL);
234 		(void) fprintf(stderr, "\n");
235 	}
236 
237 	/*
238 	 * There are many moods of logadm:
239 	 *
240 	 *	1. "-h" for help was given.  We spew a canned help
241 	 *	   message and exit, regardless of any other options given.
242 	 *
243 	 *	2. "-r" or "-w" asking us to write to the conffile.  Lots
244 	 *	   of argument checking, then we make the change to conffile
245 	 *	   and exit.  (-r processing actually happens in dologname().)
246 	 *
247 	 *	3. "-V" to search/verify the conffile was given.  We do
248 	 *	   the appropriate run through the conffile and exit.
249 	 *	   (-V processing actually happens in dologname().)
250 	 *
251 	 *	4. No lognames were given, so we're being asked to go through
252 	 *	   every entry in conffile.  We verify that only the options
253 	 *	   that make sense for this form of the command are present
254 	 *	   and fall into the main processing loop below.
255 	 *
256 	 *	5. lognames were given, so we fall into the main processing
257 	 *	   loop below to work our way through them.
258 	 *
259 	 * The last two cases are where the option processing gets more
260 	 * complex.  Each time around the main processing loop, we're
261 	 * in one of these cases:
262 	 *
263 	 *	A. No cmdargs were found (we're in case 4), the entry
264 	 *	   in conffile supplies no log file names, so the entry
265 	 *	   name itself is the logfile name (or names, if it globs
266 	 *	   to multiple file names).
267 	 *
268 	 *	B. No cmdargs were found (we're in case 4), the entry
269 	 *	   in conffile gives log file names that we then loop
270 	 *	   through and rotate/expire.  In this case, the entry
271 	 *	   name is specifically NOT one of the log file names.
272 	 *
273 	 *	C. We're going through the cmdargs (we're in case 5),
274 	 *	   the entry in conffile either doesn't exist or it exists
275 	 *	   but supplies no log file names, so the cmdarg itself
276 	 *	   is the log file name.
277 	 *
278 	 *	D. We're going through the cmdargs (we're in case 5),
279 	 *	   a matching entry in conffile supplies log file names
280 	 *	   that we then loop through and rotate/expire.  In this
281 	 *	   case the entry name is specifically NOT one of the log
282 	 *	   file names.
283 	 *
284 	 * As we're doing all this, any options given on the command line
285 	 * override any found in the conffile, and we apply the defaults
286 	 * for rotation conditions and expiration conditions, etc. at the
287 	 * last opportunity, when we're sure they haven't been overridden
288 	 * by an option somewhere along the way.
289 	 *
290 	 */
291 
292 	/* help option overrides anything else */
293 	if (opts_count(clopts, "h")) {
294 		(void) fputs(HELP1, stderr);
295 		(void) fputs(HELP2, stderr);
296 		err_done(0);
297 		/*NOTREACHED*/
298 	}
299 
300 	/* detect illegal option combinations */
301 	if (opts_count(clopts, "rwV") > 1)
302 		usage("Only one of -r, -w, or -V may be used at a time.");
303 	if (opts_count(clopts, "cM") > 1)
304 		usage("Only one of -c or -M may be used at a time.");
305 
306 	/* arrange for error output to be mailed if clopts includes -e */
307 	if (opts_count(clopts, "e"))
308 		err_mailto(opts_optarg(clopts, "e"));
309 
310 	/* this implements the default conffile and timestamps */
311 	if ((conffile = opts_optarg(clopts, "f")) == NULL)
312 		conffile = Default_conffile;
313 	if ((timestamps = opts_optarg(clopts, "F")) == NULL)
314 		timestamps = Default_timestamps;
315 	if (opts_count(clopts, "v"))
316 		(void) out("# loading %s\n", conffile);
317 	status = conf_open(conffile, timestamps, clopts);
318 	if (!status && opts_count(clopts, "V"))
319 		err_done(0);
320 
321 	/* handle conffile write option */
322 	if (opts_count(clopts, "w")) {
323 		if (Debug)
324 			(void) fprintf(stderr,
325 			    "main: add/replace conffile entry: <%s>\n",
326 			    opts_optarg(clopts, "w"));
327 		conf_replace(opts_optarg(clopts, "w"), clopts);
328 		conf_close(clopts);
329 		err_done(0);
330 		/*NOTREACHED*/
331 	}
332 
333 	/*
334 	 * lognames is either a list supplied on the command line,
335 	 * or every entry in the conffile if none were supplied.
336 	 */
337 	lognames = opts_cmdargs(clopts);
338 	if (fn_list_empty(lognames)) {
339 		/*
340 		 * being asked to do all entries in conffile
341 		 *
342 		 * check to see if any options were given that only
343 		 * make sense when lognames are given specifically
344 		 * on the command line.
345 		 */
346 		if (opts_count(clopts, OPTIONS_NOT_FIRST_FORM))
347 			usage("some options require logname argument");
348 		if (Debug)
349 			(void) fprintf(stderr,
350 			    "main: run all entries in conffile\n");
351 		lognames = conf_entries();
352 	}
353 
354 	/* foreach logname... */
355 	fn_list_rewind(lognames);
356 	while ((fnp = fn_list_next(lognames)) != NULL) {
357 		buf = fn_s(fnp);
358 		if (buf != NULL && lut_lookup(Donenames, buf) != NULL) {
359 			if (Debug)
360 				(void) fprintf(stderr,
361 				    "main: logname already done: <%s>\n",
362 				    buf);
363 			continue;
364 		}
365 		if (buf != NULL && SETJMP)
366 			err(EF_FILE, "bailing out on logname \"%s\" "
367 			    "due to errors", buf);
368 		else
369 			dologname(fnp, clopts);
370 	}
371 
372 	/* execute any after commands */
373 	lut_walk(Aftercmds, doaftercmd, clopts);
374 
375 	/* execute any gzip commands */
376 	lut_walk(Gzipnames, do_delayed_gzip, clopts);
377 
378 	/* write out any conffile changes */
379 	conf_close(clopts);
380 
381 	err_done(0);
382 	/*NOTREACHED*/
383 	return (0);	/* for lint's little mind */
384 }
385 
386 /* spew a message, then a usage message, then exit */
387 static void
388 usage(const char *msg)
389 {
390 	if (msg)
391 		err(0, "%s\nUse \"logadm -h\" for help.", msg);
392 	else
393 		err(EF_RAW, "Use \"logadm -h\" for help.\n");
394 }
395 
396 /* helper function used by doaftercmd() to join mail addrs with commas */
397 /*ARGSUSED1*/
398 static void
399 commajoin(const char *lhs, void *rhs, void *arg)
400 {
401 	struct fn *fnp = (struct fn *)arg;
402 	char *buf;
403 
404 	buf = fn_s(fnp);
405 	if (buf != NULL && *buf)
406 		fn_putc(fnp, ',');
407 	fn_puts(fnp, lhs);
408 }
409 
410 /* helper function used by main() to run "after" commands */
411 static void
412 doaftercmd(const char *lhs, void *rhs, void *arg)
413 {
414 	struct opts *opts = (struct opts *)arg;
415 	struct lut *addrs = (struct lut *)rhs;
416 
417 	if (addrs) {
418 		struct fn *fnp = fn_new(NULL);
419 
420 		/*
421 		 * addrs contains list of email addrs that should get
422 		 * the error output when this after command is executed.
423 		 */
424 		lut_walk(addrs, commajoin, fnp);
425 		err_mailto(fn_s(fnp));
426 	}
427 
428 	docmd(opts, "-a cmd", Sh, "-c", lhs, NULL);
429 }
430 
431 /* perform delayed gzip */
432 
433 static void
434 do_delayed_gzip(const char *lhs, void *rhs, void *arg)
435 {
436 	struct opts *opts = (struct opts *)arg;
437 
438 	if (rhs == NULL) {
439 		if (Debug) {
440 			(void) fprintf(stderr, "do_delayed_gzip: not gzipping "
441 			    "expired file <%s>\n", lhs);
442 		}
443 		return;
444 	}
445 	docmd(opts, "compress old log (-z flag)", Gzip, "-f", lhs, NULL);
446 }
447 
448 
449 /* main logname processing */
450 static void
451 dologname(struct fn *fnp, struct opts *clopts)
452 {
453 	const char *logname = fn_s(fnp);
454 	struct opts *cfopts;
455 	struct opts *allopts;
456 	struct fn_list *logfiles;
457 	struct fn_list *globbedfiles;
458 	struct fn *nextfnp;
459 
460 	/* look up options set by config file */
461 	cfopts = conf_opts(logname);
462 
463 	if (opts_count(clopts, "v"))
464 		(void) out("# processing logname: %s\n", logname);
465 
466 	if (Debug) {
467 		if (logname != NULL)
468 			(void) fprintf(stderr, "dologname: logname <%s>\n",
469 			    logname);
470 		(void) fprintf(stderr, "conffile opts:");
471 		opts_print(cfopts, stderr, NULL);
472 		(void) fprintf(stderr, "\n");
473 	}
474 
475 	/* handle conffile lookup option */
476 	if (opts_count(clopts, "V")) {
477 		/* lookup an entry in conffile */
478 		if (Debug)
479 			(void) fprintf(stderr,
480 			    "dologname: lookup conffile entry\n");
481 		if (conf_lookup(logname)) {
482 			opts_printword(logname, stdout);
483 			opts_print(cfopts, stdout, NULL);
484 			(void) out("\n");
485 		} else
486 			err_exitcode(1);
487 		return;
488 	}
489 
490 	/* handle conffile removal option */
491 	if (opts_count(clopts, "r")) {
492 		if (Debug)
493 			(void) fprintf(stderr,
494 			    "dologname: remove conffile entry\n");
495 		if (conf_lookup(logname))
496 			conf_replace(logname, NULL);
497 		else
498 			err_exitcode(1);
499 		return;
500 	}
501 
502 	/* generate combined options */
503 	allopts = opts_merge(cfopts, clopts);
504 
505 	/* arrange for error output to be mailed if allopts includes -e */
506 	if (opts_count(allopts, "e"))
507 		err_mailto(opts_optarg(allopts, "e"));
508 	else
509 		err_mailto(NULL);
510 
511 	/* this implements the default rotation rules */
512 	if (opts_count(allopts, "sp") == 0) {
513 		if (opts_count(clopts, "v"))
514 			(void) out(
515 			    "#     using default rotate rules: -s1b -p1w\n");
516 		(void) opts_set(allopts, "s", "1b");
517 		(void) opts_set(allopts, "p", "1w");
518 	}
519 
520 	/* this implements the default expiration rules */
521 	if (opts_count(allopts, "ACS") == 0) {
522 		if (opts_count(clopts, "v"))
523 			(void) out("#     using default expire rule: -C10\n");
524 		(void) opts_set(allopts, "C", "10");
525 	}
526 
527 	/* this implements the default template */
528 	if (opts_count(allopts, "t") == 0) {
529 		if (opts_count(clopts, "v"))
530 			(void) out("#     using default template: $file.$n\n");
531 		(void) opts_set(allopts, "t", "$file.$n");
532 	}
533 
534 	if (Debug) {
535 		(void) fprintf(stderr, "merged opts:");
536 		opts_print(allopts, stderr, NULL);
537 		(void) fprintf(stderr, "\n");
538 	}
539 
540 	/*
541 	 * if the conffile entry supplied log file names, then
542 	 * logname is NOT one of the log file names (it was just
543 	 * the entry name in conffile).
544 	 */
545 	logfiles = opts_cmdargs(cfopts);
546 	if (Debug) {
547 		char *buf;
548 		(void) fprintf(stderr, "dologname: logfiles from cfopts:\n");
549 		fn_list_rewind(logfiles);
550 		while ((nextfnp = fn_list_next(logfiles)) != NULL) {
551 			buf = fn_s(nextfnp);
552 			if (buf != NULL)
553 				(void) fprintf(stderr, "    <%s>\n", buf);
554 		}
555 	}
556 	if (fn_list_empty(logfiles))
557 		globbedfiles = glob_glob(fnp);
558 	else
559 		globbedfiles = glob_glob_list(logfiles);
560 
561 	/* go through the list produced by glob expansion */
562 	fn_list_rewind(globbedfiles);
563 	while ((nextfnp = fn_list_next(globbedfiles)) != NULL)
564 		if (rotatelog(nextfnp, allopts))
565 			expirefiles(nextfnp, allopts);
566 
567 	fn_list_free(globbedfiles);
568 	opts_free(allopts);
569 }
570 
571 
572 /* absurdly long buffer lengths for holding user/group/mode strings */
573 #define	TIMESTRMAX	100
574 #define	MAXATTR		100
575 
576 /* rotate a log file if necessary, returns true if ok to go on to expire step */
577 static boolean_t
578 rotatelog(struct fn *fnp, struct opts *opts)
579 {
580 	char *fname = fn_s(fnp);
581 	struct stat stbuf;
582 	char nowstr[TIMESTRMAX];
583 	struct fn *recentlog = fn_new(NULL);	/* for -R cmd */
584 	char ownerbuf[MAXATTR];
585 	char groupbuf[MAXATTR];
586 	char modebuf[MAXATTR];
587 	const char *owner;
588 	const char *group;
589 	const char *mode;
590 
591 	if (Debug && fname != NULL)
592 		(void) fprintf(stderr, "rotatelog: fname <%s>\n", fname);
593 
594 	if (opts_count(opts, "p") && opts_optarg_int(opts, "p") == OPTP_NEVER)
595 		return (B_TRUE);	/* "-p never" forced no rotate */
596 
597 	/* prepare the keywords */
598 	kw_init(fnp, NULL);
599 	if (Debug > 1) {
600 		(void) fprintf(stderr, "rotatelog keywords:\n");
601 		kw_print(stderr);
602 	}
603 
604 	if (lstat(fname, &stbuf) < 0) {
605 		if (opts_count(opts, "N"))
606 			return (1);
607 		err(EF_WARN|EF_SYS, "%s", fname);
608 		return (B_FALSE);
609 	}
610 
611 	if ((stbuf.st_mode & S_IFMT) == S_IFLNK) {
612 		err(EF_WARN, "%s is a symlink", fname);
613 		return (B_FALSE);
614 	}
615 
616 	if ((stbuf.st_mode & S_IFMT) != S_IFREG) {
617 		err(EF_WARN, "%s is not a regular file", fname);
618 		return (B_FALSE);
619 	}
620 
621 	/* even if size condition is not met, this entry is "done" */
622 	if (opts_count(opts, "s") &&
623 	    stbuf.st_size < opts_optarg_int(opts, "s")) {
624 		Donenames = lut_add(Donenames, fname, "1");
625 		return (B_TRUE);
626 	}
627 
628 	/* see if age condition is present, and return if not met */
629 	if (opts_count(opts, "p")) {
630 		off_t when = opts_optarg_int(opts, "p");
631 		struct opts *cfopts;
632 
633 		/* unless rotate forced by "-p now", see if period has passed */
634 		if (when != OPTP_NOW) {
635 			/*
636 			 * "when" holds the number of seconds that must have
637 			 * passed since the last time this log was rotated.
638 			 * of course, running logadm can take a little time
639 			 * (typically a second or two, but longer if the
640 			 * conffile has lots of stuff in it) and that amount
641 			 * of time is variable, depending on system load, etc.
642 			 * so we want to allow a little "slop" in the value of
643 			 * "when".  this way, if a log should be rotated every
644 			 * week, and the number of seconds passed is really a
645 			 * few seconds short of a week, we'll go ahead and
646 			 * rotate the log as expected.
647 			 *
648 			 */
649 			if (when >= 60 * 60)
650 				when -= 59;
651 
652 			/*
653 			 * last rotation is recorded as argument to -P,
654 			 * but if logname isn't the same as log file name
655 			 * then the timestamp would be recorded on a
656 			 * separate line in the timestamp file.  so if we
657 			 * haven't seen a -P already, we check to see if
658 			 * it is part of a specific entry for the log
659 			 * file name.  this handles the case where the
660 			 * logname is "apache", it supplies a log file
661 			 * name like "/var/apache/logs/[a-z]*_log",
662 			 * which expands to multiple file names.  if one
663 			 * of the file names is "/var/apache/logs/access_log"
664 			 * the the -P will be attached to a line with that
665 			 * logname in the timestamp file.
666 			 */
667 			if (opts_count(opts, "P")) {
668 				off_t last = opts_optarg_int(opts, "P");
669 
670 				/* return if not enough time has passed */
671 				if (Now - last < when)
672 					return (B_TRUE);
673 			} else if ((cfopts = conf_opts(fname)) != NULL &&
674 			    opts_count(cfopts, "P")) {
675 				off_t last = opts_optarg_int(cfopts, "P");
676 
677 				/*
678 				 * just checking this means this entry
679 				 * is now "done" if we're going through
680 				 * the entire conffile
681 				 */
682 				Donenames = lut_add(Donenames, fname, "1");
683 
684 				/* return if not enough time has passed */
685 				if (Now - last < when)
686 					return (B_TRUE);
687 			}
688 		}
689 	}
690 
691 	if (Debug)
692 		(void) fprintf(stderr, "rotatelog: conditions met\n");
693 	if (opts_count(opts, "l")) {
694 		/* Change the time zone to local time zone */
695 		if (putenv("TZ="))
696 			err(EF_SYS, "putenv TZ");
697 		tzset();
698 		Now = time(0);
699 
700 		/* rename the log file */
701 		rotateto(fnp, opts, 0, recentlog, B_FALSE);
702 
703 		/* Change the time zone to UTC */
704 		if (putenv("TZ=UTC"))
705 			err(EF_SYS, "putenv TZ");
706 		tzset();
707 		Now = time(0);
708 	} else {
709 		/* rename the log file */
710 		rotateto(fnp, opts, 0, recentlog, B_FALSE);
711 	}
712 
713 	/* determine owner, group, mode for empty log file */
714 	if (opts_count(opts, "o"))
715 		(void) strlcpy(ownerbuf, opts_optarg(opts, "o"), MAXATTR);
716 	else {
717 		(void) snprintf(ownerbuf, MAXATTR, "%ld", stbuf.st_uid);
718 	}
719 	owner = ownerbuf;
720 	if (opts_count(opts, "g"))
721 		group = opts_optarg(opts, "g");
722 	else {
723 		(void) snprintf(groupbuf, MAXATTR, "%ld", stbuf.st_gid);
724 		group = groupbuf;
725 	}
726 	(void) strlcat(ownerbuf, ":", MAXATTR - strlen(ownerbuf));
727 	(void) strlcat(ownerbuf, group, MAXATTR - strlen(ownerbuf));
728 	if (opts_count(opts, "m"))
729 		mode = opts_optarg(opts, "m");
730 	else {
731 		(void) snprintf(modebuf, MAXATTR,
732 		    "%03lo", stbuf.st_mode & 0777);
733 		mode = modebuf;
734 	}
735 
736 	/* create the empty log file */
737 	docmd(opts, NULL, Touch, fname, NULL, NULL);
738 	docmd(opts, NULL, Chown, owner, fname, NULL);
739 	docmd(opts, NULL, Chmod, mode, fname, NULL);
740 
741 	/* execute post-rotation command */
742 	if (opts_count(opts, "R")) {
743 		struct fn *rawcmd = fn_new(opts_optarg(opts, "R"));
744 		struct fn *cmd = fn_new(NULL);
745 
746 		kw_init(recentlog, NULL);
747 		(void) kw_expand(rawcmd, cmd, 0, B_FALSE);
748 		docmd(opts, "-R cmd", Sh, "-c", fn_s(cmd), NULL);
749 		fn_free(rawcmd);
750 		fn_free(cmd);
751 	}
752 	fn_free(recentlog);
753 
754 	/*
755 	 * add "after" command to list of after commands.  we also record
756 	 * the email address, if any, where the error output of the after
757 	 * command should be sent.  if the after command is already on
758 	 * our list, add the email addr to the list the email addrs for
759 	 * that command (the after command will only be executed once,
760 	 * so the error output gets mailed to every address we've come
761 	 * across associated with this command).
762 	 */
763 	if (opts_count(opts, "a")) {
764 		const char *cmd = opts_optarg(opts, "a");
765 		struct lut *addrs = (struct lut *)lut_lookup(Aftercmds, cmd);
766 		if (opts_count(opts, "e"))
767 			addrs = lut_add(addrs, opts_optarg(opts, "e"), NULL);
768 		Aftercmds = lut_add(Aftercmds, opts_optarg(opts, "a"), addrs);
769 	}
770 
771 	/* record the rotation date */
772 	(void) strftime(nowstr, sizeof (nowstr),
773 	    "%a %b %e %T %Y", gmtime(&Now));
774 	if (opts_count(opts, "v") && fname != NULL)
775 		(void) out("#     recording rotation date %s for %s\n",
776 		    nowstr, fname);
777 	conf_set(fname, "P", STRDUP(nowstr));
778 	Donenames = lut_add(Donenames, fname, "1");
779 	return (B_TRUE);
780 }
781 
782 /* rotate files "up" according to current template */
783 static void
784 rotateto(struct fn *fnp, struct opts *opts, int n, struct fn *recentlog,
785     boolean_t isgz)
786 {
787 	struct fn *template = fn_new(opts_optarg(opts, "t"));
788 	struct fn *newfile = fn_new(NULL);
789 	struct fn *dirname;
790 	int hasn;
791 	struct stat stbuf;
792 	char *buf1;
793 	char *buf2;
794 
795 	/* expand template to figure out new filename */
796 	hasn = kw_expand(template, newfile, n, isgz);
797 
798 	buf1 = fn_s(fnp);
799 	buf2 = fn_s(newfile);
800 
801 	if (Debug)
802 		if (buf1 != NULL && buf2 != NULL) {
803 			(void) fprintf(stderr, "rotateto: %s -> %s (%d)\n",
804 			    buf1, buf2, n);
805 		}
806 	/* if filename is there already, rotate "up" */
807 	if (hasn && lstat(buf2, &stbuf) != -1)
808 		rotateto(newfile, opts, n + 1, recentlog, isgz);
809 	else if (hasn && opts_count(opts, "z")) {
810 		struct fn *gzfnp = fn_dup(newfile);
811 		/*
812 		 * since we're compressing old files, see if we
813 		 * about to rotate into one.
814 		 */
815 		fn_puts(gzfnp, ".gz");
816 		if (lstat(fn_s(gzfnp), &stbuf) != -1)
817 			rotateto(gzfnp, opts, n + 1, recentlog, B_TRUE);
818 		fn_free(gzfnp);
819 	}
820 
821 	/* first time through run "before" cmd if not run already */
822 	if (n == 0 && opts_count(opts, "b")) {
823 		const char *cmd = opts_optarg(opts, "b");
824 
825 		if (lut_lookup(Beforecmds, cmd) == NULL) {
826 			docmd(opts, "-b cmd", Sh, "-c", cmd, NULL);
827 			Beforecmds = lut_add(Beforecmds, cmd, "1");
828 		}
829 	}
830 
831 	/* ensure destination directory exists */
832 	dirname = fn_dirname(newfile);
833 	docmd(opts, "verify directory exists", Mkdir, "-p",
834 	    fn_s(dirname), NULL);
835 	fn_free(dirname);
836 
837 	/* do the rename */
838 	if (n == 0 && opts_count(opts, "c") != 0) {
839 		docopytruncate(opts, fn_s(fnp), fn_s(newfile));
840 	} else if (n == 0 && opts_count(opts, "M")) {
841 		struct fn *rawcmd = fn_new(opts_optarg(opts, "M"));
842 		struct fn *cmd = fn_new(NULL);
843 
844 		/* use specified command to mv the log file */
845 		kw_init(fnp, newfile);
846 		(void) kw_expand(rawcmd, cmd, 0, B_FALSE);
847 		docmd(opts, "-M cmd", Sh, "-c", fn_s(cmd), NULL);
848 		fn_free(rawcmd);
849 		fn_free(cmd);
850 	} else
851 		/* common case: we call "mv" to handle the actual rename */
852 		docmd(opts, "rotate log file", Mv, "-f",
853 		    fn_s(fnp), fn_s(newfile));
854 
855 	/* first time through, gather interesting info for caller */
856 	if (n == 0)
857 		fn_renew(recentlog, fn_s(newfile));
858 }
859 
860 /* expire phase of logname processing */
861 static void
862 expirefiles(struct fn *fnp, struct opts *opts)
863 {
864 	char *fname = fn_s(fnp);
865 	struct fn *template;
866 	struct fn *pattern;
867 	struct fn_list *files;
868 	struct fn *nextfnp;
869 	off_t count;
870 	off_t size;
871 
872 	if (Debug && fname != NULL)
873 		(void) fprintf(stderr, "expirefiles: fname <%s>\n", fname);
874 
875 	/* return if no potential expire conditions */
876 	if (opts_count(opts, "zAS") == 0 && opts_optarg_int(opts, "C") == 0)
877 		return;
878 
879 	kw_init(fnp, NULL);
880 	if (Debug > 1) {
881 		(void) fprintf(stderr, "expirefiles keywords:\n");
882 		kw_print(stderr);
883 	}
884 
885 	/* see if pattern was supplied by user */
886 	if (opts_count(opts, "T")) {
887 		template = fn_new(opts_optarg(opts, "T"));
888 		pattern = glob_to_reglob(template);
889 	} else {
890 		/* nope, generate pattern based on rotation template */
891 		template = fn_new(opts_optarg(opts, "t"));
892 		pattern = fn_new(NULL);
893 		(void) kw_expand(template, pattern, -1,
894 		    opts_count(opts, "z") != 0);
895 	}
896 
897 	/* match all old log files (hopefully not any others as well!) */
898 	files = glob_reglob(pattern);
899 
900 	if (Debug) {
901 		char *buf;
902 
903 		buf = fn_s(pattern);
904 		if (buf != NULL) {
905 			(void) fprintf(stderr, "expirefiles: pattern <%s>\n",
906 			    buf);
907 		}
908 		fn_list_rewind(files);
909 		while ((nextfnp = fn_list_next(files)) != NULL) {
910 			buf = fn_s(nextfnp);
911 			if (buf != NULL)
912 				(void) fprintf(stderr, "    <%s>\n", buf);
913 		}
914 	}
915 
916 	/* see if count causes expiration */
917 	if ((count = opts_optarg_int(opts, "C")) > 0) {
918 		int needexpire = fn_list_count(files) - count;
919 
920 		if (Debug)
921 			(void) fprintf(stderr, "expirefiles: needexpire %d\n",
922 			    needexpire);
923 
924 		while (needexpire > 0 &&
925 		    ((nextfnp = fn_list_popoldest(files)) != NULL)) {
926 			dorm(opts, "expire by count rule", nextfnp);
927 			fn_free(nextfnp);
928 			needexpire--;
929 		}
930 	}
931 
932 	/* see if total size causes expiration */
933 	if (opts_count(opts, "S") && (size = opts_optarg_int(opts, "S")) > 0) {
934 		while (fn_list_totalsize(files) > size &&
935 		    ((nextfnp = fn_list_popoldest(files)) != NULL)) {
936 			dorm(opts, "expire by size rule", nextfnp);
937 			fn_free(nextfnp);
938 		}
939 	}
940 
941 	/* see if age causes expiration */
942 	if (opts_count(opts, "A")) {
943 		int mtime = (int)time(0) - (int)opts_optarg_int(opts, "A");
944 
945 		while ((nextfnp = fn_list_popoldest(files)) != NULL) {
946 			if (fn_getstat(nextfnp)->st_mtime < mtime) {
947 				dorm(opts, "expire by age rule", nextfnp);
948 				fn_free(nextfnp);
949 			} else {
950 				fn_list_addfn(files, nextfnp);
951 				break;
952 			}
953 		}
954 	}
955 
956 	/* record old log files to be gzip'ed according to -z count */
957 	if (opts_count(opts, "z")) {
958 		int zcount = (int)opts_optarg_int(opts, "z");
959 		int fcount = fn_list_count(files);
960 
961 		while (fcount > zcount &&
962 		    (nextfnp = fn_list_popoldest(files)) != NULL) {
963 			if (!fn_isgz(nextfnp)) {
964 				/*
965 				 * Don't gzip the old log file yet -
966 				 * it takes too long. Just remember that we
967 				 * need to gzip.
968 				 */
969 				if (Debug) {
970 					(void) fprintf(stderr,
971 					    "will compress %s count %d\n",
972 					    fn_s(nextfnp), fcount);
973 				}
974 				Gzipnames = lut_add(Gzipnames,
975 				    fn_s(nextfnp), "1");
976 			}
977 			fn_free(nextfnp);
978 			fcount--;
979 		}
980 	}
981 
982 	fn_free(template);
983 	fn_list_free(files);
984 }
985 
986 /* execute a command to remove an expired log file */
987 static void
988 dorm(struct opts *opts, const char *msg, struct fn *fnp)
989 {
990 	if (opts_count(opts, "E")) {
991 		struct fn *rawcmd = fn_new(opts_optarg(opts, "E"));
992 		struct fn *cmd = fn_new(NULL);
993 
994 		/* user supplied cmd, expand $file */
995 		kw_init(fnp, NULL);
996 		(void) kw_expand(rawcmd, cmd, 0, B_FALSE);
997 		docmd(opts, msg, Sh, "-c", fn_s(cmd), NULL);
998 		fn_free(rawcmd);
999 		fn_free(cmd);
1000 	} else
1001 		docmd(opts, msg, Rm, "-f", fn_s(fnp), NULL);
1002 	Gzipnames = lut_add(Gzipnames, fn_s(fnp), NULL);
1003 }
1004 
1005 /* execute a command, producing -n and -v output as necessary */
1006 static void
1007 docmd(struct opts *opts, const char *msg, const char *cmd,
1008     const char *arg1, const char *arg2, const char *arg3)
1009 {
1010 	int pid;
1011 	int errpipe[2];
1012 
1013 	/* print info about command if necessary */
1014 	if (opts_count(opts, "vn")) {
1015 		const char *simplecmd;
1016 
1017 		if ((simplecmd = strrchr(cmd, '/')) == NULL)
1018 			simplecmd = cmd;
1019 		else
1020 			simplecmd++;
1021 		(void) out("%s", simplecmd);
1022 		if (arg1)
1023 			(void) out(" %s", arg1);
1024 		if (arg2)
1025 			(void) out(" %s", arg2);
1026 		if (arg3)
1027 			(void) out(" %s", arg3);
1028 		if (msg)
1029 			(void) out(" # %s", msg);
1030 		(void) out("\n");
1031 	}
1032 
1033 	if (opts_count(opts, "n"))
1034 		return;		/* -n means don't really do it */
1035 
1036 	/*
1037 	 * Run the cmd and see if it failed.  This function is *not* a generic
1038 	 * command runner.  The command name must be an absolute pathname, and
1039 	 * two args must be supplied (the second may be NULL, or they may both
1040 	 * be NULL).  Any output (stdout and stderr) from the child process is
1041 	 * logged to stderr and perhaps sent to an email recipient.
1042 	 */
1043 	if (pipe(errpipe) < 0)
1044 		err(EF_SYS, "pipe");
1045 
1046 	if ((pid = fork()) < 0)
1047 		err(EF_SYS, "fork");
1048 	else if (pid) {
1049 		int wstat;
1050 		struct pollfd pfd;
1051 		boolean_t first = B_TRUE;
1052 
1053 		/* parent */
1054 		(void) close(errpipe[1]);
1055 
1056 		pfd.fd = errpipe[0];
1057 		pfd.events = POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI;
1058 		for (;;) {
1059 
1060 			pfd.revents = 0;
1061 			if (poll(&pfd, 1, -1) == -1) {
1062 				if (errno == EINTR) {
1063 					continue;
1064 				}
1065 				err(EF_SYS, "poll");
1066 				break;
1067 			}
1068 			if ((pfd.events & pfd.revents) != 0) {
1069 				if (first) {
1070 					err(EF_WARN,
1071 					    "command failed: %s%s%s%s%s%s%s",
1072 					    cmd,
1073 					    (arg1) ? " " : "",
1074 					    (arg1) ? arg1 : "",
1075 					    (arg2) ? " " : "",
1076 					    (arg2) ? arg2 : "",
1077 					    (arg3) ? " " : "",
1078 					    (arg3) ? arg3 : "");
1079 					first = B_FALSE;
1080 				}
1081 				err_fromfd(pfd.fd);
1082 			}
1083 			if ((pfd.revents & (POLLERR | POLLHUP)) != 0) {
1084 				break;
1085 			}
1086 		}
1087 		if (waitpid(pid, &wstat, 0) < 0) {
1088 			err(EF_SYS, "waitpid");
1089 			return;
1090 		}
1091 
1092 		if (!first) {
1093 			/* Assume the command gave a useful error */
1094 		} else if (WIFSIGNALED(wstat)) {
1095 			err(EF_WARN,
1096 			    "command died, signal %d: %s%s%s%s%s%s%s",
1097 			    WTERMSIG(wstat),
1098 			    cmd,
1099 			    (arg1) ? " " : "",
1100 			    (arg1) ? arg1 : "",
1101 			    (arg2) ? " " : "",
1102 			    (arg2) ? arg2 : "",
1103 			    (arg3) ? " " : "",
1104 			    (arg3) ? arg3 : "");
1105 		} else if (WIFEXITED(wstat) && WEXITSTATUS(wstat)) {
1106 			err(EF_WARN,
1107 			    "command error, exit %d: %s%s%s%s%s%s%s",
1108 			    WEXITSTATUS(wstat),
1109 			    cmd,
1110 			    (arg1) ? " " : "",
1111 			    (arg1) ? arg1 : "",
1112 			    (arg2) ? " " : "",
1113 			    (arg2) ? arg2 : "",
1114 			    (arg3) ? " " : "",
1115 			    (arg3) ? arg3 : "");
1116 		}
1117 
1118 		(void) close(errpipe[0]);
1119 	} else {
1120 		/* child */
1121 		(void) dup2(errpipe[1], fileno(stderr));
1122 		(void) close(errpipe[0]);
1123 		(void) execl(cmd, cmd, arg1, arg2, arg3, 0);
1124 		perror(cmd);
1125 		_exit(1);
1126 	}
1127 }
1128 
1129 /* do internal atomic file copy and truncation */
1130 static void
1131 docopytruncate(struct opts *opts, const char *file, const char *file_copy)
1132 {
1133 	int fi, fo;
1134 	char buf[128 * 1024];
1135 	struct stat s;
1136 	struct utimbuf times;
1137 	off_t written = 0, rem, last = 0, thresh = 1024 * 1024;
1138 	ssize_t len = 0;
1139 
1140 	/* print info if necessary */
1141 	if (opts_count(opts, "vn") != 0) {
1142 		(void) out("# log rotation via atomic copy and truncation"
1143 		    " (-c flag):\n");
1144 		(void) out("# copy %s to %s\n", file, file_copy);
1145 		(void) out("# truncate %s\n", file);
1146 	}
1147 
1148 	if (opts_count(opts, "n"))
1149 		return;		/* -n means don't really do it */
1150 
1151 	/* open log file to be rotated and remember its chmod mask */
1152 	if ((fi = open(file, O_RDWR)) < 0) {
1153 		err(EF_SYS, "cannot open file %s", file);
1154 		return;
1155 	}
1156 
1157 	if (fstat(fi, &s) < 0) {
1158 		err(EF_SYS, "cannot access: %s", file);
1159 		(void) close(fi);
1160 		return;
1161 	}
1162 
1163 	/* create new file for copy destination with correct attributes */
1164 	if ((fo = open(file_copy, O_CREAT|O_TRUNC|O_WRONLY, s.st_mode)) < 0) {
1165 		err(EF_SYS, "cannot create file: %s", file_copy);
1166 		(void) close(fi);
1167 		return;
1168 	}
1169 
1170 	(void) fchown(fo, s.st_uid, s.st_gid);
1171 
1172 	/*
1173 	 * Now we'll loop, reading the log file and writing it to our copy
1174 	 * until the bytes remaining are beneath our atomicity threshold -- at
1175 	 * which point we'll lock the file and copy the remainder atomically.
1176 	 * The body of this loop is non-atomic with respect to writers, the
1177 	 * rationale being that total atomicity (that is, locking the file for
1178 	 * the entire duration of the copy) comes at too great a cost for a
1179 	 * large log file, as the writer (i.e., the daemon whose log is being
1180 	 * rolled) can be blocked for an unacceptable duration.  (For one
1181 	 * particularly loquacious daemon, this period was observed to be
1182 	 * several minutes in length -- a time so long that it induced
1183 	 * additional failures in dependent components.)  Note that this means
1184 	 * that if the log file is not always appended to -- if it is opened
1185 	 * without O_APPEND or otherwise truncated outside of logadm -- this
1186 	 * will result in our log snapshot being incorrect.  But of course, in
1187 	 * either of these cases, the use of logadm at all is itself
1188 	 * suspect...
1189 	 */
1190 	do {
1191 		if (fstat(fi, &s) < 0) {
1192 			err(EF_SYS, "cannot stat: %s", file);
1193 			(void) close(fi);
1194 			(void) close(fo);
1195 			(void) remove(file_copy);
1196 			return;
1197 		}
1198 
1199 		if ((rem = s.st_size - written) < thresh) {
1200 			if (rem >= 0)
1201 				break;
1202 
1203 			/*
1204 			 * If the file became smaller, something fishy is going
1205 			 * on; we'll truncate our copy, reset our seek offset
1206 			 * and break into the atomic copy.
1207 			 */
1208 			(void) ftruncate(fo, 0);
1209 			(void) lseek(fo, 0, SEEK_SET);
1210 			(void) lseek(fi, 0, SEEK_SET);
1211 			break;
1212 		}
1213 
1214 		if (written != 0 && rem > last) {
1215 			/*
1216 			 * We're falling behind -- this file is getting bigger
1217 			 * faster than we're able to write it; break out and
1218 			 * lock the file to block the writer.
1219 			 */
1220 			break;
1221 		}
1222 
1223 		last = rem;
1224 
1225 		while (rem > 0) {
1226 			if ((len = read(fi, buf, MIN(sizeof (buf), rem))) <= 0)
1227 				break;
1228 
1229 			if (write(fo, buf, len) == len) {
1230 				rem -= len;
1231 				written += len;
1232 				continue;
1233 			}
1234 
1235 			err(EF_SYS, "cannot write into file %s", file_copy);
1236 			(void) close(fi);
1237 			(void) close(fo);
1238 			(void) remove(file_copy);
1239 			return;
1240 		}
1241 	} while (len >= 0);
1242 
1243 	/* lock log file so that nobody can write into it before we are done */
1244 	if (fchmod(fi, s.st_mode|S_ISGID) < 0)
1245 		err(EF_SYS, "cannot set mandatory lock bit for: %s", file);
1246 
1247 	if (lockf(fi, F_LOCK, 0) == -1)
1248 		err(EF_SYS, "cannot lock file %s", file);
1249 
1250 	/* do atomic copy and truncation */
1251 	while ((len = read(fi, buf, sizeof (buf))) > 0)
1252 		if (write(fo, buf, len) != len) {
1253 			err(EF_SYS, "cannot write into file %s", file_copy);
1254 			(void) lockf(fi, F_ULOCK, 0);
1255 			(void) fchmod(fi, s.st_mode);
1256 			(void) close(fi);
1257 			(void) close(fo);
1258 			(void) remove(file_copy);
1259 			return;
1260 		}
1261 
1262 	(void) ftruncate(fi, 0);
1263 
1264 	/* unlock log file */
1265 	if (lockf(fi, F_ULOCK, 0) == -1)
1266 		err(EF_SYS, "cannot unlock file %s", file);
1267 
1268 	if (fchmod(fi, s.st_mode) < 0)
1269 		err(EF_SYS, "cannot reset mandatory lock bit for: %s", file);
1270 
1271 	(void) close(fi);
1272 	(void) close(fo);
1273 
1274 	/* keep times from original file */
1275 	times.actime = s.st_atime;
1276 	times.modtime = s.st_mtime;
1277 	(void) utime(file_copy, &times);
1278 }
1279