/*
*
* Synopsis
*   taillog [-+options] [file]
*
* Description
*   This is a version of the usual Unix tail(1) command, optimized  to
*   deal with logfiles.  Its function is to copy the last <n> lines of
*   the file to stdout, then stay around  waiting  for  more  data  to
*   appear, and when it does, copy it to stdout.
*
*   The  most  important  thing  that this program does different from
*   tail is that it checks each time around its loop  to  see  if  the
*   file's magic numbers (dev, ino) have changed. If so, it closes the
*   file and reopens it.
*
*   Also, note that the  "forever"  option  is  true  by  default;  +f
*   enables it and -f disables it. Normally, you won't want to use any
*   options at all.
*
*   Another difference:  The default number of lines is zero.  This is
*   useful  with logfiles; it means that when you start taillog up, it
*   will only show you new lines that appear after that time.
*
* Options
*   This program uses the convention that the  '-'  flag  disables  an
*   option and the '+' flag enables it.  For some options, there is no
*   concept of enable/disable, and either may be used.  Option letters
*   may be either case.
*
* -<n>
*   Show the last <n> lines of the file.  The default is zero.
*
* -d<l><file>
*   Debug option. The debug level, <l>, is a single digit.  The <file>
*   names the output file, which is stderr if omitted.  The default is
*   -d1; fatal error messages only.
*
* -f
* +f
*   Disable or enable the "forever" option:  When the program hits the
*   end  of  the  file,  it checks every few seconds to see if there's
*   more data, and if so, copies it to stdout.
*
*/
#include "V.h"
#include "memchunk.h"
#include "sys_select.h"
#include "sys_stat.h"

char  buf[BUFSIZ];
int   delay = 1;
int   maxdelay = 5;
int   fd = -1;
Flag  fflag = 1;
char* fn = 0;
STat  fs = {0};
int   lines = 0;
Str*  lp = 0;
int   olddev=0, newdev=0;
int   oldino=0, newino=0;
Ulong oldsiz=0, newsiz=0;
TIMV  tmo = {1,0};

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*/
main(ac,av)
	char**av;
{	int   a, i, l, r=0;	/* Arg index, return value */
	int   c, c0, c1, count;
	char* p;
	fd_set fdrset, fdeset;
	ac = Vinit(ac,av);
	for (a=1; a<ac; a++) {
		switch(c0=av[a][0]) {
		case '-':
		case '+':
			switch (c1=av[a][1]) {
			case 'd':
			case 'D':
				Vopt(av[a]+2);
				break;
			case '7': case '8': case '9':
			case '4': case '5': case '6':
			case '1': case '2': case '3':
			case '0':
				for (p = av[a] + 1; c = *p; p++) {
					Switch(c) {
					case 'f': case 'F':
						fflag = (c0 == '+');
						break;
					case '7': case '8': case '9':
					case '4': case '5': case '6':
					case '1': case '2': case '3':
					case '0':
						lines = (10 * lines) + (c - '0');
						V4 "Lines: %d ...",lines D;
					}
					V2 "Lines: %d fflag=%d.",lines,fflag D;
				}
				break;
			default:
				P1 "%s\tOption \"%s\" unknown, ignored.",pname,av[a] D;
				break;
			}
			break;
		default:
			if (!fn) {
				fn = av[a];
				V2 "File name: %s",Dspp(fn) D;
			} else {
				P1 "%s\tExtra arg \"%s\" ignored.",pname,av[a] D;
			}
			break;
		}
	}
	if ((fd = Open(fn,0,0)) < 0) {
		r = errno;
		P1 "Can't read %s [Err %d=%s=%s]",Dspp(fn),Errinfo D;
		Loop;		/* Go ahead to loop portion */
	}
	nbio(fd);	/* Make sure we don't block */
	if (Stat(fn,&fs) < 0) {
		r = errno;
		P1 "Can't stat %s [Err %d=%s=%s]",Dspp(fn),Errinfo D;
		Fail;
	}
	olddev = fs.st_dev;
	oldino = fs.st_ino;
	oldsiz = fs.st_size;
	V4 "old dev=%d ino=%d siz=%d.",olddev,oldino,oldsiz D;
/*
* First, read in data until we stop getting data, which indicates the
* initial EOF.  The data is accumulated in the
*/
	if (lines > 0) {
		V2 "Find last %d lines ...",lines D;
		if (!(lp = (Str*)GetChunk((lines+1)*sizeof(Str),"lp"))) Fail;
		while (1) {
			FD_ZERO(&fdrset); FD_ZERO(&fdeset);
			FD_SET(fd, &fdrset); FD_SET(fd, &fdeset);
			tmo.tv_sec = delay;
			V4 "Before Select(%d,%08X,0,%08X,%08X)",fd+1,&fdrset,&fdeset,&tmo D;
			count = Select(fd+1,&fdrset,0,&fdeset,&tmo);
			V4 "After  Select(%d,%08X,0,%08X,%08X)=%d [Err %d=%s=%s",fd+1,&fdrset,&fdeset,&tmo,count,Errinfo D;
			if (count <= 0) {
				V1 "### Select returned %d [Err %d=%s=%s]",count,Errinfo D;
				Close(fd);
				fd = -1;
				break;
			}
			if ((count = Read(fd,buf,BUFSIZ)) <= 0) {
				V2 "### Read returned %d [Err %d=%s=%s]",count,Errinfo D;
				if (count < 0) {
					V1 "### Read returned %d [Err %d=%s=%s]",count,Errinfo D;
					Close(fd);
					fd = -1;
				}
				break;
			}
			for (i=0; i<count; i++) {
				if (lp[lines].l >= lp[lines].m) {
					V5 "Expand line %d from %d to %d bytes.",lines,lp[lines].m,lp[lines].m+100 D;
					if (!MinStr(&lp[lines],lp[lines].m + 100))
						Fail;
				}
				if ((lp[lines].v[lp[lines].l++] = buf[i]) == '\n') {
					V6 "Line %d: %s",lines,DspSV(lp[lines]) D;
					if (lp[0].m) {
						V4 "Free line 0." D;
						FreeStr(&lp[0]);
					}
					V4 "Newline; shift %d lines ...",lines D;
					for (l=0; l<lines; l++) {
						V5 "Shift line %d [%08X,%d,%d,%08X] to %d.",l+1,FldsBV(lp[l+1]),l D;
						lp[l] = lp[l+1];
						V5 "Shift line %d [%08X,%d,%d,%08X] done.",l,FldsBV(lp[l]) D;
						if (lp[l].l > 0) V3 "Line %d l=%d %s",l,lp[l].l,DspSV(lp[l]) D;
					}
					V4 "Zero line %d.",lines D;
					ZeroStr(lp[lines]);
				}
			}
		}
		V2 "EOF on file %d %s",fd,Dspp(fn) D;
		for (l=0; l<= lines; l++) {
			V3 "Line %d l=%d %s",l,lp[l].l,DspSV(lp[l]) D;
			if (lp[l].l > 0) {
				Write(1,lp[l].v,lp[l].l);
			}
		}
	} else {
		V2 "Lseek to EOF." D;
		Lseek(fd,0,SEEK_END);
	}
	if (!fflag) {
		V2 "End of file; exit because fflag=%d.",fflag D;
		r = 0;
		Done;
	}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Here's the main loop, which periodically checks to  see  if  there's *
* any new data in the file.  The first thing to do is to check and see *
* if the file's magic numbers have changed. If so, it's a new logfile, *
* and  we  have  to reopen it.  Our basic strategy is to use select to *
* tell us when there's data, and  if  anything  goes  wrong,  possibly *
* close the file and come back here.                                   *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
loop:
	if (Stat(fn,&fs) < 0) {
		r = errno;
		P1 "Can't stat %s [Err %d=%s=%s]",Dspp(fn),Errinfo D;
		Sleep(delay);
		delay = Min(maxdelay,delay+1);
		Loop;
	}
	newdev = fs.st_dev;
	newino = fs.st_ino;
	newsiz = fs.st_size;
	V4 "new dev=%d ino=%d siz=%d.",newdev,newino,newsiz D;
	if ((newdev != olddev) || (newino != oldino)) {
		if (newdev != olddev) V2 "dev changed from %d to %d.",olddev,newdev D;
		if (newino != oldino) V2 "ino changed from %d to %d.",oldino,newino D;
		V5 "Close fd=%d ...",fd D;
		Close(fd);
		V5 "Closed fd=%d.",fd D;
		fd = -1;
		Sleep(delay);	/* Decrease the chance that the new log won't exist */
	}
	if (fd < 0) {
		V5 "Open %s ...",Dspp(fn) D;
		if ((fd = Open(fn,0,0)) < 0) {
			r = errno;
			P1 "Can't read %s [Err %d=%s=%s]",Dspp(fn),Errinfo D;
			if (r == ENOENT) {
				Sleep(delay);	/* Wait for it to reappear */
				delay = Min(maxdelay,delay+1);
				Loop;
			}
			Fail;
		}
		V5 "File %d is %s.",fd,Dspp(fn) D;
		nbio(fd);	/* Make sure we don't block */
		V5 "After nbio(fd)",fd D;
		if (Stat(fn,&fs) < 0) {	/* Get magic numbers for logfile */
			r = errno;
			P1 "Can't stat %s [Err %d=%s=%s]",Dspp(fn),Errinfo D;
			Fail;
		}
		V5 "After Stat(fd)",fd D;
		olddev = fs.st_dev;
		oldino = fs.st_ino;
		oldsiz = fs.st_size;
		V4 "old dev=%d ino=%d siz=%d.",olddev,oldino,oldsiz D;
	}
	FD_ZERO(&fdrset); FD_ZERO(&fdeset);
	FD_SET(fd, &fdrset); FD_SET(fd, &fdeset);
	tmo.tv_sec = delay;
	V4 "Before Select(%d,%08X,0,%08X,%08X)",fd+1,&fdrset,&fdeset,&tmo D;
	count = Select(fd+1,&fdrset,0,&fdeset,&tmo);
	V4 "After  Select(%d,%08X,0,%08X,%08X)=%d [Err %d=%s=%s",fd+1,&fdrset,&fdeset,&tmo,count,Errinfo D;
	if (count <= 0) {
		V1 "### Select returned %d [Err %d=%s=%s]",count,Errinfo D;
		Close(fd);
		fd = -1;
		Loop;
	}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* We now know that there is at least one byte of  data  in  the  file. *
* There may or may not be more than that. Anyhow, we try to read a big *
* chunk, and the nbio above should guarantee that  we  get  back  some *
* data.                                                                *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
	if ((count = Read(fd,buf,BUFSIZ)) <= 0) {
		V3 "### Read returned %d [Err %d=%s=%s]",count,Errinfo D;
		if (count < 0) {
			V1 "### Read returned %d [Err %d=%s=%s]",count,Errinfo D;
			Close(fd);
			fd = -1;
		} else {
			delay = Min(maxdelay,delay+1);
			V5 "Delay is now %d sec.",delay D;
			Sleep(delay);
		}
		Loop;
	}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* We got count bytes of data; pass them on to stdout.  We  also  reset *
* the delay to 1 sec here every time we get data.                      *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
	delay = 1;
	Write(1,buf,count);
	Loop;
done:
fail:
	Exit(r);
	exit(r);
	_exit(r);	/* Paranoia */
}
