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