/* * taillog - tail the end of a log file while dealing with file moves & removes * * @(#) $Revision: 1.13 $ * @(#) $Id: taillog.c,v 1.13 2000/06/05 00:19:20 chongo Exp $ * @(#) $Source: /usr/local/src/cmd/taillog/RCS/taillog.c,v $ * * usage: * taillog [-1] [-d debuglevel] [-p pausetime] file * * -1 do not exit if we become orphaned (ppid becomes 1) * -d debuglevel debug level (0 => no debugging) * -p pausetime change the time to wait for the file to change * * tailmessages [-1] [-d debuglevel] [-p pausetime] * * -1 do not exit if we become orphaned (ppid becomes 1) * -d debuglevel debug level (0 => no debugging) * -p pausetime change the time to wait for the file to change * * tailmaillog [-1] [-d debuglevel] [-p pausetime] * * -1 do not exit if we become orphaned (ppid becomes 1) * -d debuglevel debug level (0 => no debugging) * -p pausetime change the time to wait for the file to change * * by: chongo /\oo/\ * http://reality.sgi.com/chongo * * Copyright (c) 1999 by Landon Curt Noll. All Rights Reserved. * * Permission to use, copy, modify, and distribute this software and * its documentation for any purpose and without fee is hereby granted, * provided that the above copyright, this permission notice and text * this comment, and the disclaimer below appear in all of the following: * * supporting documentation * source copies * source works derived from this source * binaries derived from this source or from derived source * * LANDON CURT NOLL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO * EVENT SHALL LANDON CURT NOLL BE LIABLE FOR ANY SPECIAL, INDIRECT OR * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF * USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. * * chongo /\oo/\ * * Share and enjoy! */ #include #include #include #include #include #include #include #include #include #include #include #include /* * important constants */ /* truth as we know it :-) */ #if !defined(FALSE) #define FALSE 0 #endif #if !defined(TRUE) #define TRUE 1 #endif #define TAIL_RETRY 3 /* tail failures before we wait */ #define P_FLAG 67 /* default seconds between file status checks */ #define DBG_NONE 0 /* debugging disabled */ #define DBG_LOW 1 /* low debug level */ #define DBG_MED 5 /* medium debug level */ #define DBG_HI 9 /* hi debug level */ /* where tail may be found - from more to least common locations */ #define TAIL_1 "/usr/bin/tail" #define TAIL_2 "/usr/ucb/tail" #define TAIL_3 "/bin/tail" #define TAIL_4 "/sbin/tail" #define TAIL_5 "/usr/local/bin/tail" #define TAIL_6 "/usr/gnu/bin/tail" #define TAIL_7 "/usr/xpg4/bin/tail" /* usage message */ #define USAGE "taillog [-1] [-d debuglevel] [-p pausetime] file\n \ttailmessages [-1] [-d debuglevel] [-p pausetime]\n \ttailmaillog [-1] [-d debuglevel] [-p pausetime]" /* * We use macros such as log_stat and use the logstat typedef so that * we can choose which stat and file length to use. * * On IRIX, allow for large files by using the 64 bit file stat interface. */ #if defined(sgi) #define log_stat(x,y) stat64((x),(y)) #define log_fstat(x,y) fstat64((x),(y)) typedef struct stat64 logstat; typedef off64_t logoff_t; #else #define log_stat(x,y) stat((x),(y)) #define log_fstat(x,y) fstat((x),(y)) typedef struct stat logstat; typedef off_t logoff_t; #endif /* * static declarations */ static void debug(int minlevel, char *fmt, ...); static void sigchld(int sig); static pid_t tail(char *filename); static void tailkill(pid_t pid); static void cleanup(logoff_t *lastlen_p, time_t *modtime_p, int *logfd_p, char *logname); static int done = FALSE; /* TRUE => stop tailing on next cycle */ static int dbglvl = DBG_NONE; /* debug level (0 => no debugging) */ static int orphan_exit = TRUE; /* TRUE => exit if our parent becomes pid 1 */ static pid_t tailpid = -1; /* process ID of the tail child */ /* * global vars */ char *program; char *basename; int main(int argc, char *argv[]) { extern char *optarg; /* getopt flag */ extern int optind; /* getopt index */ int pausetime; /* time to pause between checks */ char *logname; /* name of the log file to tail */ int logfd; /* open logfile descriptor or -1 */ logstat pbuf; /* file status by path */ logstat fbuf; /* file status by open file descriptor */ int tail_cycle; /* number of tail failures in a row */ int gone_once; /* TRUE => if we found logfile missing, once */ logoff_t lastlen; /* the last length of the file */ time_t lastmod; /* last modification time */ char *p; int c; /* * setup */ /* preset defaults */ program = argv[0]; p = strrchr(program, '/'); if (p == NULL) { basename = program; } else { basename = p+1; } pausetime = P_FLAG; /* input is not needed */ fclose(stdin); /* watch for child processes */ (void) sigset(SIGCHLD, sigchld); /* * parse args */ /* parse args */ while ((c = getopt(argc, argv, "d:p:1")) != -1) { switch (c) { case 'd': /* set debug level */ dbglvl = atoi(optarg); break; case 'p': /* convert -p option to a pauseime */ pausetime = atoi(optarg); if (pausetime == 0) { pausetime = P_FLAG; } else if (pausetime < 0) { fprintf(stderr, "%s: -t value must be 0 (default) or greater\n", program); exit(1); } break; case '1': /* disable orphaned exit mode */ orphan_exit = FALSE; break; default: /* print usage message and quit */ fprintf(stderr, "usage: %s\n", USAGE); exit(2); } } if (strcmp(basename, "taillog") == 0) { if (optind != argc-1) { fprintf(stderr, "usage: %s\n", USAGE); exit(3); } logname = argv[optind]; /* get log filename */ } else if (strcmp(basename, "tailmessages") == 0) { if (optind != argc) { fprintf(stderr, "usage: %s\n", USAGE); exit(4); } logname = "/var/log/messages"; /* fixed log filename */ } else if (strcmp(basename, "tailmaillog") == 0) { if (optind != argc) { fprintf(stderr, "usage: %s\n", USAGE); exit(5); } logname = "/var/log/maillog"; /* fixed log filename */ } else { fprintf(stderr, "unknown program name: %s\n", basename); fprintf(stderr, "usage: %s\n", USAGE); exit(6); } debug(DBG_LOW, "logname will monitor %s", logname); debug(DBG_HI, "pausetime: %d", pausetime); /* * main loop */ logfd = -1; tailpid = -1; tail_cycle = 0; lastlen = 0; memset(&lastmod, 0, sizeof(lastmod)); gone_once = FALSE; while (!done) { /* * exit if we becomed orphaned - our patent process becomes 1 */ if ((orphan_exit == TRUE) && (getppid() == (pid_t)1)) { debug(DBG_LOW, "parent pid is now 1, exiting", logname); cleanup(&lastlen, &lastmod, &logfd, logname); exit(0); } /* * if logfile is not open, try to stat the path */ debug(DBG_HI, "top of main loop"); if (log_stat(logname, &pbuf) < 0) { /* * the path is no longer there * * close any open file and wait */ if (!gone_once) { debug(DBG_LOW, "%s is gone, will look for it later", logname); gone_once = TRUE; } cleanup(&lastlen, &lastmod, &logfd, logname); debug(DBG_MED, "pausing %d", pausetime); sleep((unsigned)pausetime); continue; } else if (gone_once) { debug(DBG_LOW, "%s now exists, will now open it", logname); } gone_once = FALSE; /* * launch the tailer if we do not have one */ if (tailpid < 0) { /* fork the tailer */ tailpid = tail(logname); if (tailpid < 0) { /* report tail failure */ ++tail_cycle; debug(DBG_LOW, "failed to tail %s, cycle: %d", logname, tail_cycle); cleanup(&lastlen, &lastmod, &logfd, logname); if (tail_cycle > TAIL_RETRY) { debug(DBG_MED, "pausing %d", pausetime); sleep((unsigned)pausetime); } continue; } /* tail is working now */ debug(DBG_HI, "tail process is: %d", tailpid); tail_cycle = 0; } /* * if logfile is not open, try to open it */ if (logfd < 0) { if (logfd = open(logname, O_RDONLY) < 0) { /* file does not (yet) exist, pause and retry */ debug(DBG_LOW, "cannot open %s", logname); cleanup(&lastlen, &lastmod, &logfd, logname); debug(DBG_MED, "pausing %d", pausetime); sleep((unsigned)pausetime); continue; } else { debug(DBG_MED, "opened %s on descriptor %d", logname, logfd); } } /* * Verify that we can still stat the open file */ if (log_fstat(logfd, &fbuf) < 0) { /* * failed to stat open file, close and pause */ debug(DBG_LOW, "opened stat of %s failed", logname); cleanup(&lastlen, &lastmod, &logfd, logname); debug(DBG_MED, "pausing %d", pausetime); sleep((unsigned)pausetime); continue; /* * Verify that the file we have opened is the same as the * file found by accessing it via the filesystem path */ } else if (pbuf.st_ino != fbuf.st_ino || pbuf.st_dev != fbuf.st_dev) { /* * file has been replaced, close file and immediately reopen */ debug(DBG_LOW, "%s has changed, reopening file", logname); debug(DBG_HI, "path dev/inode: %d/%d != open " "dev/inode: %d/%d", pbuf.st_dev, pbuf.st_ino, fbuf.st_dev, fbuf.st_ino); cleanup(&lastlen, &lastmod, &logfd, logname); continue; /* * Catch the case where the file shrinks */ } else if (pbuf.st_size < lastlen || fbuf.st_size < pbuf.st_size) { /* * file shrunk, close file and immediately reopen */ debug(DBG_LOW, "%s shrunk, reopening file", logname); cleanup(&lastlen, &lastmod, &logfd, logname); continue; /* * Catch the case where file has beem modified but the size is the same */ } else if (lastlen > 0 && fbuf.st_size == lastlen && memcmp(&(fbuf.st_mtime), &lastmod, sizeof(lastmod)) != 0) { /* * file touched but same size, close file and immediately reopen */ debug(DBG_LOW, "%s was touched with same size, reopening file", logname); cleanup(&lastlen, &lastmod, &logfd, logname); continue; } lastlen = fbuf.st_size; lastmod = fbuf.st_mtime; /* * let the tailer work and check up on it later */ debug(DBG_HI, "bottom of loop pausing %d", pausetime); sleep((unsigned)pausetime); } return 0; } /* * debug - print debug message if debug level is low enough * * given: * minlevel -d value that must be set to print a message * fmt format if the message (not including taillog: and \n) */ static void debug(int minlevel, char *fmt, ...) { va_list args; /* variable arg list */ /* * do nothing if debug level is too low */ if (minlevel > dbglvl) { return; } /* * print message */ fprintf(stderr, "note: taillog: ==> "); va_start(args, fmt); vfprintf(stderr, fmt, args); fputc('\n', stderr); return; } /* * sigchld - catch a child signal * * If the child is stopped (but not killed), we will kill it so that * it can restart. */ static void sigchld(int sig) { pid_t wpid; /* the pid we waited on */ /* * note signal */ debug(DBG_MED, "in sigchld, sig: %d, tailpid: %d", sig, tailpid); /* * ignore if not SIGCHLD */ if (sig != SIGCHLD) { debug(DBG_MED, "in sigchld, but sig:%d != SIGCHLD: %d", sig, SIGCHLD); return; } /* * be sure that the process has gone away * * But only if tailpid */ if (tailpid > 1) { /* reap the zombie if it exists */ do { /* if signal error, retry waitpid */ errno = 0; wpid = waitpid(tailpid, NULL, WNOHANG); } while (wpid < 0 && errno == EINTR); if (wpid < 0) { debug(DBG_MED, "sigchld waitpid on %d returned: %d, errno: %d", tailpid, wpid, errno); } else { debug(DBG_HI, "sigchld waitpid returned: %d", wpid); } /* kill process if it still exists */ if (kill(tailpid, 0) >= 0) { /* kill process and reap the zombie */ debug(DBG_HI, "tail process %d exists, doing a SIGKILL of %d", tailpid); (void) sigset(SIGCHLD, SIG_IGN); (void) kill(tailpid, SIGKILL); do { /* if signal error, retry waitpid */ errno = 0; wpid = waitpid(tailpid, NULL, WNOHANG); } while (wpid < 0 && errno == EINTR); (void) sigset(SIGCHLD, sigchld); } /* * no tailpid, so try to reap a zombie child if it still exists */ } else { /* reap any waiting zombie process, if one exists */ debug(DBG_HI, "tailpid is <= 1: %d", tailpid); do { /* if signal error, retry wait3 */ errno = 0; wpid = wait3(NULL, WNOHANG, NULL); } while (wpid < 0 && errno == EINTR); if (wpid < 0) { debug(DBG_MED, "sigchld wait3 returned: %d, errno: %d", wpid, errno); } else { debug(DBG_HI, "wait3 returned: %d", wpid); } } return; } /* * tail - launch a tail process on a filename * * given: * filename path of file to tail -f * * returns: * pid of child tail process or -1 * * We will forst search for a tail executable, and when found will * fork/exec it as needed. */ static pid_t tail(char *filename) { char *tailprog; /* name of tail program found */ pid_t pid; /* child pid */ int err_no; /* exec errro */ /* * look for a tail program */ if (access(TAIL_1, X_OK) >= 0) { tailprog = TAIL_1; } else if (access(TAIL_2, X_OK) >= 0) { tailprog = TAIL_2; } else if (access(TAIL_3, X_OK) >= 0) { tailprog = TAIL_3; } else if (access(TAIL_4, X_OK) >= 0) { tailprog = TAIL_4; } else if (access(TAIL_5, X_OK) >= 0) { tailprog = TAIL_5; } else if (access(TAIL_6, X_OK) >= 0) { tailprog = TAIL_6; } else if (access(TAIL_7, X_OK) >= 0) { tailprog = TAIL_7; } else { debug(DBG_LOW, "unable to find a tail program right now"); return -1; } debug(DBG_HI, "using tail program: %s", tailprog); /* * fork the child process */ fflush(stdout); fflush(stderr); pid = fork(); if (pid < 0) { debug(DBG_LOW, "fork failed"); return -1; } if (pid == 0) { /* * child processing */ errno = 0; execl(tailprog, "tail", "-f", filename, NULL); err_no = errno; /* * the exec failed, try to report and die */ fprintf(stderr, "WARNING: child pid: %d failed to exec %s: ", getpid(), tailprog); if (err_no > 0) { perror(NULL); } else { fprintf(stderr, "errno == 0\n"); } fflush(stderr); exit(7); } /* * parent process */ debug(DBG_MED, "tailing %s", filename); debug(DBG_HI, "fork of pid: %d was successful", pid); return pid; } /* * tailkill - kill off the tailer * * given: * pid the proecss ID to kill */ static void tailkill(pid_t pid) { pid_t wpid; /* the pid we waited on */ /* * kill the process */ debug(DBG_HI, "tailkill is about to do a SIGKILL of pid: %d", pid); if (kill(pid, SIGKILL) < 0) { debug(DBG_MED, "tailkill SIGKILL of pid: %d failed", pid); } debug(DBG_HI, "tailkill is about sleep for up to 1 second"); sleep(1); /* * check on the killed process */ wpid = waitpid(pid, NULL, WNOHANG); if (wpid < 0) { if (errno == ECHILD) { debug(DBG_HI, "tailkill waitpid said pid %d is gone", pid); } else { debug(DBG_MED, "tailkill waitpid on %d returned: %d, errno: %d", pid, wpid, errno); } } else { debug(DBG_HI, "tailkill waitpid returned: %d", wpid); } /* * return to normal */ return; } /* * cleanup - cleanup current tailing and prep for a new tail * * given: * lastlen_p pointer to last file length * modtime_p pointer the last mod time * logfd_p pointer to open file descriptor * logname name of the logfile * * Will clear the last length, close the file (if opened) and * clear the tailpid. */ static void cleanup(logoff_t *lastlen_p, time_t *modtime_p, int *logfd_p, char *logname) { /* * kill the tailer, if one exists */ if (tailpid > 0) { debug(DBG_MED, "killing tailer: %d", tailpid); tailkill(tailpid); } tailpid = -1; /* * close the file, if opened and possible */ if (logfd_p != NULL && *logfd_p >= 0) { debug(DBG_MED, "closing %s: %d", logname, *logfd_p); (void) close(*logfd_p); *logfd_p = -1; } /* * clear the length, if possible */ if (lastlen_p != NULL) { *lastlen_p = 0; } /* * clear the modification time, if possible */ if (modtime_p != NULL) { memset(modtime_p, 0, sizeof(*modtime_p)); } return; }