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