]> granicus.if.org Git - apache/blob - support/rotatelogs.c
Update scoreboard backport proposal with latest commit.
[apache] / support / rotatelogs.c
1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2  * contributor license agreements.  See the NOTICE file distributed with
3  * this work for additional information regarding copyright ownership.
4  * The ASF licenses this file to You under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with
6  * the License.  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #include "apr.h"
18 #include "apr_lib.h"
19 #include "apr_strings.h"
20 #include "apr_errno.h"
21 #include "apr_file_io.h"
22 #include "apr_file_info.h"
23 #include "apr_general.h"
24 #include "apr_time.h"
25 #include "apr_getopt.h"
26 #include "apr_thread_proc.h"
27 #include "apr_signal.h"
28 #if APR_FILES_AS_SOCKETS
29 #include "apr_poll.h"
30 #endif
31
32 #if APR_HAVE_STDLIB_H
33 #include <stdlib.h>
34 #endif
35 #define APR_WANT_STRFUNC
36 #include "apr_want.h"
37
38 #define BUFSIZE         65536
39 #define ERRMSGSZ        256
40
41 #define ROTATE_NONE     0
42 #define ROTATE_NEW      1
43 #define ROTATE_TIME     2
44 #define ROTATE_SIZE     3
45 #define ROTATE_FORCE    4
46
47 static const char *const ROTATE_REASONS[] = {
48     "None",
49     "Open a new file",
50     "Time interval expired",
51     "Maximum size reached",
52     "Forced rotation",
53     NULL
54 };
55
56 typedef struct rotate_config rotate_config_t;
57
58 struct rotate_config {
59     unsigned int sRotation;
60     int tRotation;
61     int utc_offset;
62     int use_localtime;
63     int use_strftime;
64     int force_open;
65     int verbose;
66     int echo;
67     const char *szLogRoot;
68     int truncate;
69     const char *linkfile;
70     const char *postrotate_prog;
71 #if APR_FILES_AS_SOCKETS
72     int create_empty;
73 #endif
74     int num_files;
75 };
76
77 typedef struct rotate_status rotate_status_t;
78
79 /* Structure to contain relevant logfile state: fd, pool and
80  * filename. */
81 struct logfile {
82     apr_pool_t *pool;
83     apr_file_t *fd;
84     char name[APR_PATH_MAX];
85 };
86
87 struct rotate_status {
88     struct logfile current; /* current logfile. */
89     apr_pool_t *pool; /* top-level pool */
90     char errbuf[ERRMSGSZ];
91     int rotateReason;
92     int tLogEnd;
93     int nMessCount;
94     int fileNum;
95 };
96
97 static rotate_config_t config;
98 static rotate_status_t status;
99
100 static void usage(const char *argv0, const char *reason)
101 {
102     if (reason) {
103         fprintf(stderr, "%s\n", reason);
104     }
105     fprintf(stderr,
106 #if APR_FILES_AS_SOCKETS
107             "Usage: %s [-v] [-l] [-L linkname] [-p prog] [-f] [-t] [-e] [-c] [-n number] <logfile> "
108 #else
109             "Usage: %s [-v] [-l] [-L linkname] [-p prog] [-f] [-t] [-e] [-n number] <logfile> "
110 #endif
111             "{<rotation time in seconds>|<rotation size>(B|K|M|G)} "
112             "[offset minutes from UTC]\n\n",
113             argv0);
114 #ifdef OS2
115     fprintf(stderr,
116             "Add this:\n\nTransferLog \"|%s.exe /some/where 86400\"\n\n",
117             argv0);
118 #else
119     fprintf(stderr,
120             "Add this:\n\nTransferLog \"|%s /some/where 86400\"\n\n",
121             argv0);
122     fprintf(stderr,
123             "or \n\nTransferLog \"|%s /some/where 5M\"\n\n", argv0);
124 #endif
125     fprintf(stderr,
126             "to httpd.conf. By default, the generated name will be\n"
127             "<logfile>.nnnn where nnnn is the system time at which the log\n"
128             "nominally starts (N.B. if using a rotation time, the time will\n"
129             "always be a multiple of the rotation time, so you can synchronize\n"
130             "cron scripts with it). If <logfile> contains strftime conversion\n"
131             "specifications, those will be used instead. At the end of each\n"
132             "rotation time or when the file size is reached a new log is\n"
133             "started.\n"
134             "\n"
135             "Options:\n"
136             "  -v       Verbose operation. Messages are written to stderr.\n"
137             "  -l       Base rotation on local time instead of UTC.\n"
138             "  -L path  Create hard link from current log to specified path.\n"
139             "  -p prog  Run specified program after opening a new log file. See below.\n"
140             "  -f       Force opening of log on program start.\n"
141             "  -t       Truncate logfile instead of rotating, tail friendly.\n"
142             "  -e       Echo log to stdout for further processing.\n"
143 #if APR_FILES_AS_SOCKETS
144             "  -c       Create log even if it is empty.\n"
145 #endif
146             "\n"
147             "The program is invoked as \"[prog] <curfile> [<prevfile>]\"\n"
148             "where <curfile> is the filename of the newly opened logfile, and\n"
149             "<prevfile>, if given, is the filename of the previously used logfile.\n"
150             "\n");
151     exit(1);
152 }
153
154 /*
155  * Get the unix time with timezone corrections
156  * given in the config struct.
157  */
158 static int get_now(rotate_config_t *config)
159 {
160     apr_time_t tNow = apr_time_now();
161     int utc_offset = config->utc_offset;
162     if (config->use_localtime) {
163         /* Check for our UTC offset before using it, since it might
164          * change if there's a switch between standard and daylight
165          * savings time.
166          */
167         apr_time_exp_t lt;
168         apr_time_exp_lt(&lt, tNow);
169         utc_offset = lt.tm_gmtoff;
170     }
171     return (int)apr_time_sec(tNow) + utc_offset;
172 }
173
174 /*
175  * Close a file and destroy the associated pool.
176  */
177 static void close_logfile(rotate_config_t *config, struct logfile *logfile)
178 {
179     if (config->verbose) {
180         fprintf(stderr, "Closing file %s\n", logfile->name);
181     }
182     apr_file_close(logfile->fd);
183     apr_pool_destroy(logfile->pool);
184 }
185
186 /*
187  * Dump the configuration parsing result to STDERR.
188  */
189 static void dumpConfig (rotate_config_t *config)
190 {
191     fprintf(stderr, "Rotation time interval:      %12d\n", config->tRotation);
192     fprintf(stderr, "Rotation size interval:      %12d\n", config->sRotation);
193     fprintf(stderr, "Rotation time UTC offset:    %12d\n", config->utc_offset);
194     fprintf(stderr, "Rotation based on localtime: %12s\n", config->use_localtime ? "yes" : "no");
195     fprintf(stderr, "Rotation file date pattern:  %12s\n", config->use_strftime ? "yes" : "no");
196     fprintf(stderr, "Rotation file forced open:   %12s\n", config->force_open ? "yes" : "no");
197     fprintf(stderr, "Rotation verbose:            %12s\n", config->verbose ? "yes" : "no");
198 #if APR_FILES_AS_SOCKETS
199     fprintf(stderr, "Rotation create empty logs:  %12s\n", config->create_empty ? "yes" : "no");
200 #endif
201     fprintf(stderr, "Rotation file name: %21s\n", config->szLogRoot);
202     fprintf(stderr, "Post-rotation prog: %21s\n", config->postrotate_prog);
203 }
204
205 /*
206  * Check whether we need to rotate.
207  * Possible reasons are:
208  * - No log file open (ROTATE_NEW)
209  * - User forces us to rotate (ROTATE_FORCE)
210  * - Our log file size is already bigger than the
211  *   allowed maximum (ROTATE_SIZE)
212  * - The next log time interval expired (ROTATE_TIME)
213  *
214  * When size and time constraints are both given,
215  * it suffices that one of them is fulfilled.
216  */
217 static void checkRotate(rotate_config_t *config, rotate_status_t *status)
218 {
219     if (status->current.fd == NULL) {
220         status->rotateReason = ROTATE_NEW;
221     }
222     else if (config->sRotation) {
223         apr_finfo_t finfo;
224         apr_off_t current_size = -1;
225
226         if (apr_file_info_get(&finfo, APR_FINFO_SIZE, status->current.fd) == APR_SUCCESS) {
227             current_size = finfo.size;
228         }
229
230         if (current_size > config->sRotation) {
231             status->rotateReason = ROTATE_SIZE;
232         }
233         else if (config->tRotation) {
234             if (get_now(config) >= status->tLogEnd) {
235                 status->rotateReason = ROTATE_TIME;
236             }
237         }
238     }
239     else if (config->tRotation) {
240         if (get_now(config) >= status->tLogEnd) {
241             status->rotateReason = ROTATE_TIME;
242         }
243     }
244     else {
245         fprintf(stderr, "No rotation time or size specified\n");
246         exit(2);
247     }
248
249     if (status->rotateReason != ROTATE_NONE && config->verbose) {
250         fprintf(stderr, "File rotation needed, reason: %s\n", ROTATE_REASONS[status->rotateReason]);
251     }
252 }
253
254 /*
255  * Handle post-rotate processing.
256  */
257 static void post_rotate(apr_pool_t *pool, struct logfile *newlog,
258                         rotate_config_t *config, rotate_status_t *status)
259 {
260     apr_status_t rv;
261     char error[120];
262     apr_procattr_t *pattr;
263     const char *argv[4];
264     apr_proc_t proc;
265
266     /* Handle link file, if configured. */
267     if (config->linkfile) {
268         apr_file_remove(config->linkfile, newlog->pool);
269         if (config->verbose) {
270             fprintf(stderr,"Linking %s to %s\n", newlog->name, config->linkfile);
271         }
272         rv = apr_file_link(newlog->name, config->linkfile);
273         if (rv != APR_SUCCESS) {
274             apr_strerror(rv, error, sizeof error);
275             fprintf(stderr, "Error linking file %s to %s (%s)\n",
276                     newlog->name, config->linkfile, error);
277             exit(2);
278         }
279     }
280
281     if (!config->postrotate_prog) {
282         /* Nothing more to do. */
283         return;
284     }
285
286     /* Collect any zombies from a previous run, but don't wait. */
287     while (apr_proc_wait_all_procs(&proc, NULL, NULL, APR_NOWAIT, pool) == APR_CHILD_DONE)
288         /* noop */;
289
290     if ((rv = apr_procattr_create(&pattr, pool)) != APR_SUCCESS) {
291         fprintf(stderr,
292                 "post_rotate: apr_procattr_create failed for '%s': %s\n",
293                 config->postrotate_prog,
294                 apr_strerror(rv, error, sizeof(error)));
295         return;
296     }
297
298     rv = apr_procattr_error_check_set(pattr, 1);
299     if (rv == APR_SUCCESS)
300         rv = apr_procattr_cmdtype_set(pattr, APR_PROGRAM_ENV);
301
302     if (rv != APR_SUCCESS) {
303         fprintf(stderr,
304                 "post_rotate: could not set up process attributes for '%s': %s\n",
305                 config->postrotate_prog,
306                 apr_strerror(rv, error, sizeof(error)));
307         return;
308     }
309
310     argv[0] = config->postrotate_prog;
311     argv[1] = newlog->name;
312     if (status->current.fd) {
313         argv[2] = status->current.name;
314         argv[3] = NULL;
315     }
316     else {
317         argv[2] = NULL;
318     }
319
320     if (config->verbose)
321         fprintf(stderr, "Calling post-rotate program: %s\n", argv[0]);
322
323     rv = apr_proc_create(&proc, argv[0], argv, NULL, pattr, pool);
324     if (rv != APR_SUCCESS) {
325         fprintf(stderr, "Could not spawn post-rotate process '%s': %s\n",
326                 config->postrotate_prog,
327                 apr_strerror(rv, error, sizeof(error)));
328         return;
329     }
330 }
331
332 /* After a error, truncate the current file and write out an error
333  * message, which must be contained in status->errbuf.  The process is
334  * terminated on failure.  */
335 static void truncate_and_write_error(rotate_status_t *status)
336 {
337     apr_size_t buflen = strlen(status->errbuf);
338
339     if (apr_file_trunc(status->current.fd, 0) != APR_SUCCESS) {
340         fprintf(stderr, "Error truncating the file %s\n", status->current.name);
341         exit(2);
342     }
343     if (apr_file_write_full(status->current.fd, status->errbuf, buflen, NULL) != APR_SUCCESS) {
344         fprintf(stderr, "Error writing error (%s) to the file %s\n", 
345                 status->errbuf, status->current.name);
346         exit(2);
347     }
348 }
349
350 /*
351  * Open a new log file, and if successful
352  * also close the old one.
353  *
354  * The timestamp for the calculation of the file
355  * name of the new log file will be the actual millisecond
356  * timestamp, except when a regular rotation based on a time
357  * interval is configured and the previous interval
358  * is over. Then the timestamp is the starting time
359  * of the actual interval.
360  */
361 static void doRotate(rotate_config_t *config, rotate_status_t *status)
362 {
363
364     int now = get_now(config);
365     int tLogStart;
366     apr_status_t rv;
367     struct logfile newlog;
368     int thisLogNum = -1;
369
370     status->rotateReason = ROTATE_NONE;
371
372     if (config->tRotation) {
373         int tLogEnd;
374         tLogStart = (now / config->tRotation) * config->tRotation;
375         tLogEnd = tLogStart + config->tRotation;
376         /*
377          * Check if rotation was forced and the last rotation
378          * interval is not yet over. Use the value of now instead
379          * of the time interval boundary for the file name then.
380          */
381         if (tLogStart < status->tLogEnd) {
382             tLogStart = now;
383         }
384         status->tLogEnd = tLogEnd;
385     }
386     else {
387         tLogStart = now;
388     }
389
390     if (config->use_strftime) {
391         apr_time_t tNow = apr_time_from_sec(tLogStart);
392         apr_time_exp_t e;
393         apr_size_t rs;
394
395         apr_time_exp_gmt(&e, tNow);
396         apr_strftime(newlog.name, &rs, sizeof(newlog.name), config->szLogRoot, &e);
397     }
398     else {
399         if (config->truncate) {
400             apr_snprintf(newlog.name, sizeof(newlog.name), "%s", config->szLogRoot);
401         }
402         else if (config->num_files > 0) { 
403             if (status->fileNum == -1 || status->fileNum == (config->num_files - 1)) {
404                 thisLogNum = 0;
405                 apr_snprintf(newlog.name, sizeof(newlog.name), "%s", config->szLogRoot);
406             }
407             else { 
408                 thisLogNum = status->fileNum + 1;
409                 apr_snprintf(newlog.name, sizeof(newlog.name), "%s.%d", config->szLogRoot, thisLogNum);
410             }
411         }
412         else {
413             apr_snprintf(newlog.name, sizeof(newlog.name), "%s.%010d", config->szLogRoot,
414                          tLogStart);
415         }
416     }
417     apr_pool_create(&newlog.pool, status->pool);
418     if (config->verbose) {
419         fprintf(stderr, "Opening file %s\n", newlog.name);
420     }
421     rv = apr_file_open(&newlog.fd, newlog.name, APR_WRITE | APR_CREATE | APR_APPEND
422                        | (config->truncate || (config->num_files > 0 && status->current.fd) ? APR_TRUNCATE : 0), 
423                        APR_OS_DEFAULT, newlog.pool);
424     if (rv == APR_SUCCESS) {
425         /* Handle post-rotate processing. */
426         post_rotate(newlog.pool, &newlog, config, status);
427
428         status->fileNum = thisLogNum;
429         /* Close out old (previously 'current') logfile, if any. */
430         if (status->current.fd) {
431             close_logfile(config, &status->current);
432         }
433
434         /* New log file is now 'current'. */
435         status->current = newlog;
436     }
437     else {
438         char error[120];
439
440         apr_strerror(rv, error, sizeof error);
441
442         /* Uh-oh. Failed to open the new log file. Try to clear
443          * the previous log file, note the lost log entries,
444          * and keep on truckin'. */
445         if (status->current.fd == NULL) {
446             fprintf(stderr, "Could not open log file '%s' (%s)\n", newlog.name, error);
447             exit(2);
448         }
449
450         /* Throw away new state; it isn't going to be used. */
451         apr_pool_destroy(newlog.pool);
452
453         /* Try to keep this error message constant length
454          * in case it occurs several times. */
455         apr_snprintf(status->errbuf, sizeof status->errbuf,
456                      "Resetting log file due to error opening "
457                      "new log file, %10d messages lost: %-25.25s\n",
458                      status->nMessCount, error);
459
460         truncate_and_write_error(status);
461     }
462
463     status->nMessCount = 0;
464 }
465
466 /*
467  * Get a size or time param from a string.
468  * Parameter 'last' indicates, whether the
469  * argument is the last commadnline argument.
470  * UTC offset is only allowed as a last argument
471  * in order to make is distinguishable from the
472  * rotation interval time.
473  */
474 static const char *get_time_or_size(rotate_config_t *config,
475                                     const char *arg, int last) {
476     char *ptr = NULL;
477     /* Byte multiplier */
478     unsigned int mult = 1;
479     if ((ptr = strchr(arg, 'B')) != NULL) { /* Found KB size */
480         mult = 1;
481     }
482     else if ((ptr = strchr(arg, 'K')) != NULL) { /* Found KB size */
483         mult = 1024;
484     }
485     else if ((ptr = strchr(arg, 'M')) != NULL) { /* Found MB size */
486         mult = 1024 * 1024;
487     }
488     else if ((ptr = strchr(arg, 'G')) != NULL) { /* Found GB size */
489         mult = 1024 * 1024 * 1024;
490     }
491     if (ptr) { /* rotation based on file size */
492         if (config->sRotation > 0) {
493             return "Rotation size parameter allowed only once";
494         }
495         if (*(ptr+1) == '\0') {
496             config->sRotation = atoi(arg) * mult;
497         }
498         if (config->sRotation == 0) {
499             return "Invalid rotation size parameter";
500         }
501     }
502     else if ((config->sRotation > 0 || config->tRotation > 0) && last) {
503         /* rotation based on elapsed time */
504         if (config->use_localtime) {
505             return "UTC offset parameter is not valid with -l";
506         }
507         config->utc_offset = atoi(arg) * 60;
508     }
509     else { /* rotation based on elapsed time */
510         if (config->tRotation > 0) {
511             return "Rotation time parameter allowed only once";
512         }
513         config->tRotation = atoi(arg);
514         if (config->tRotation <= 0) {
515             return "Invalid rotation time parameter";
516         }
517     }
518     return NULL;
519 }
520
521 int main (int argc, const char * const argv[])
522 {
523     char buf[BUFSIZE];
524     apr_size_t nRead, nWrite;
525     apr_file_t *f_stdin;
526     apr_file_t *f_stdout;
527     apr_getopt_t *opt;
528     apr_status_t rv;
529     char c;
530     const char *opt_arg;
531     const char *err = NULL;
532 #if APR_FILES_AS_SOCKETS
533     apr_pollfd_t pollfd = { 0 };
534     apr_status_t pollret = APR_SUCCESS;
535     int polltimeout;
536 #endif
537
538     apr_app_initialize(&argc, &argv, NULL);
539     atexit(apr_terminate);
540
541     memset(&config, 0, sizeof config);
542     memset(&status, 0, sizeof status);
543     status.rotateReason = ROTATE_NONE;
544
545     apr_pool_create(&status.pool, NULL);
546     apr_getopt_init(&opt, status.pool, argc, argv);
547 #if APR_FILES_AS_SOCKETS
548     while ((rv = apr_getopt(opt, "lL:p:ftvecn:", &c, &opt_arg)) == APR_SUCCESS) {
549 #else
550     while ((rv = apr_getopt(opt, "lL:p:ftven:", &c, &opt_arg)) == APR_SUCCESS) {
551 #endif
552         switch (c) {
553         case 'l':
554             config.use_localtime = 1;
555             break;
556         case 'L':
557             config.linkfile = opt_arg;
558             break;
559         case 'p':
560             config.postrotate_prog = opt_arg;
561 #ifdef SIGCHLD
562             /* Prevent creation of zombies (on modern Unix systems). */
563             apr_signal(SIGCHLD, SIG_IGN);
564 #endif
565             break;
566         case 'f':
567             config.force_open = 1;
568             break;
569         case 't':
570             config.truncate = 1;
571             break;
572         case 'v':
573             config.verbose = 1;
574             break;
575         case 'e':
576             config.echo = 1;
577             break;
578 #if APR_FILES_AS_SOCKETS
579         case 'c':
580             config.create_empty = 1;
581             break;
582 #endif
583         case 'n':
584             config.num_files = atoi(opt_arg);
585             status.fileNum = -1;
586             break;
587         }
588     }
589
590     if (rv != APR_EOF) {
591         usage(argv[0], NULL /* specific error message already issued */ );
592     }
593
594     /*
595      * After the initial flags we need 2 to 4 arguments,
596      * the file name, either the rotation interval time or size
597      * or both of them, and optionally the UTC offset.
598      */
599     if ((argc - opt->ind < 2) || (argc - opt->ind > 4) ) {
600         usage(argv[0], "Incorrect number of arguments");
601     }
602
603     config.szLogRoot = argv[opt->ind++];
604
605     /* Read in the remaining flags, namely time, size and UTC offset. */
606     for(; opt->ind < argc; opt->ind++) {
607         if ((err = get_time_or_size(&config, argv[opt->ind],
608                                     opt->ind < argc - 1 ? 0 : 1)) != NULL) {
609             usage(argv[0], err);
610         }
611     }
612
613     config.use_strftime = (strchr(config.szLogRoot, '%') != NULL);
614
615     if (config.use_strftime && config.num_files > 0) { 
616         fprintf(stderr, "Cannot use -n with %% in filename\n");
617         exit(1);
618     }
619
620     if (status.fileNum == -1 && config.num_files < 1) { 
621         fprintf(stderr, "Invalid -n argument\n");
622         exit(1);
623     }
624
625     if (apr_file_open_stdin(&f_stdin, status.pool) != APR_SUCCESS) {
626         fprintf(stderr, "Unable to open stdin\n");
627         exit(1);
628     }
629
630     if (apr_file_open_stdout(&f_stdout, status.pool) != APR_SUCCESS) {
631         fprintf(stderr, "Unable to open stdout\n");
632         exit(1);
633     }
634
635     /*
636      * Write out result of config parsing if verbose is set.
637      */
638     if (config.verbose) {
639         dumpConfig(&config);
640     }
641
642 #if APR_FILES_AS_SOCKETS
643     if (config.create_empty && config.tRotation) {
644         pollfd.p = status.pool;
645         pollfd.desc_type = APR_POLL_FILE;
646         pollfd.reqevents = APR_POLLIN;
647         pollfd.desc.f = f_stdin;
648     }
649 #endif
650
651     /*
652      * Immediately open the logfile as we start, if we were forced
653      * to do so via '-f'.
654      */
655     if (config.force_open) {
656         doRotate(&config, &status);
657     }
658
659     for (;;) {
660         nRead = sizeof(buf);
661 #if APR_FILES_AS_SOCKETS
662         if (config.create_empty && config.tRotation) {
663             polltimeout = status.tLogEnd ? status.tLogEnd - get_now(&config) : config.tRotation;
664             if (polltimeout <= 0) {
665                 pollret = APR_TIMEUP;
666             }
667             else {
668                 pollret = apr_poll(&pollfd, 1, &pollret, apr_time_from_sec(polltimeout));
669             }
670         }
671         if (pollret == APR_SUCCESS) {
672             rv = apr_file_read(f_stdin, buf, &nRead);
673             if (APR_STATUS_IS_EOF(rv)) {
674                 break;
675             }
676             else if (rv != APR_SUCCESS) {
677                 exit(3);
678             }
679         }
680         else if (pollret == APR_TIMEUP) {
681             *buf = 0;
682             nRead = 0;
683         }
684         else {
685             fprintf(stderr, "Unable to poll stdin\n");
686             exit(5);
687         }
688 #else /* APR_FILES_AS_SOCKETS */
689         rv = apr_file_read(f_stdin, buf, &nRead);
690         if (APR_STATUS_IS_EOF(rv)) {
691             break;
692         }
693         else if (rv != APR_SUCCESS) {
694             exit(3);
695         }
696 #endif /* APR_FILES_AS_SOCKETS */
697         checkRotate(&config, &status);
698         if (status.rotateReason != ROTATE_NONE) {
699             doRotate(&config, &status);
700         }
701
702         nWrite = nRead;
703         rv = apr_file_write_full(status.current.fd, buf, nWrite, &nWrite);
704         if (nWrite != nRead) {
705             apr_off_t cur_offset;
706
707             cur_offset = 0;
708             if (apr_file_seek(status.current.fd, APR_CUR, &cur_offset) != APR_SUCCESS) {
709                 cur_offset = -1;
710             }
711             status.nMessCount++;
712             apr_snprintf(status.errbuf, sizeof status.errbuf,
713                          "Error %d writing to log file at offset %" APR_OFF_T_FMT ". "
714                          "%10d messages lost (%pm)\n",
715                          rv, cur_offset, status.nMessCount, &rv);
716
717             truncate_and_write_error(&status);
718         }
719         else {
720             status.nMessCount++;
721         }
722         if (config.echo) {
723             if (apr_file_write_full(f_stdout, buf, nRead, &nWrite)) {
724                 fprintf(stderr, "Unable to write to stdout\n");
725                 exit(4);
726             }
727         }
728     }
729
730     return 0; /* reached only at stdin EOF. */
731 }