2837 - remove print/lp* from gate and use CUPS from userland

   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
 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
 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
 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
 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
 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
 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
 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/apache2/2.2/logs/[a-z]*_log",
 657                          * which expands to multiple file names.  if one of
 658                          * the file names is "/var/apache2/2.2/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
 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
 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
 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
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
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 }
--- EOF ---