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