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