[mod.sources] v06i084: A multiple "tail -f" program

sources-request@mirror.UUCP (08/01/86)

Submitted by: ihnp4!poseidon!brent
Mod.sources: Volume 6, Issue 84
Archive-name: watch

[  This program does not use the portable directory access routines,
   and references a <values.h> file.  I didn't notice any "non-generic"
   curses calls (e.g., curses/terminfo, PD curses, etc.), but I'm
   not sure that I would.  I will happily publish a set of context
   diffs if someone ports this to 4.[23]BSD.  --r$ ]

I've found this program pretty useful in testing an application with
lots of asynchronous processes creating spool files and writing logs.

Essentially it allows you to do multiple "tail -f"s with each one in a
separate window on the screen.  Watch also allows you to "tail" a
directory and see files as soon as they appear in the directory.

Each window can have an wakeup alarm set to go off on any output to the
window - useful if you want to look at something more interesting.

Have fun!

Made in New Zealand -->		Brent Callaghan
				AT&T Information Systems, Lincroft, NJ
				{ihnp4|mtuxo|pegasus}!poseidon!brent
				(201) 576-3475

-- cut here -- cut here -- cut here -- cut here -- cut here -- cut here 
#!/bin/sh
# This is a shar archive.
# The rest of this file is a shell script which will extract:
# watch.1 watch.c
# Archive created: Fri Jul 11 14:21:28 EDT 1986
echo x - watch.1
sed 's/^X//' > watch.1 << '~FUNKY STUFF~'
X.TH WATCH 1 ""
X.SH NAME
watch \- watch files or directories

X.SH SYNOPSIS
X.B watch 
[
X.I flags
]
X.I file ...

X.SH DESCRIPTION
X.B Watch
is a program for monitoring files or directories.
It accepts a list of files as arguments
and allocates a window on the screen to display
each file.
X.P
The top line of each window is reserved for a label
containing the name of the file.
If the file is a directory a slash "/" is appended to the name.
The 
X.B -l
flag dispenses with the labels and gives you an extra line
per window (but you'll quickly forget what you were watching).
X.P
For an 
X.I ordinary file
it functions like
X.B
"tail -f".
The window displays the lines at the end of the file and includes
new characters as they are appended.
X.P
If the file is a
X.I directory
it functions like 
X.B
"ls -lrt".
The window displays the directory entries sorted by
modification time with the most recently added files at the
bottom.
The display is updated if a new file is added to the directory
or if the modification time of an existing file is updated.
For each entry the modification time, owner, file size
and file name are displayed.
X.P
X.B Watch
allocates equal window space to each file argument.
You can control the allocation by giving the window
size after the file name.
X.P
X.RS
watch iqueue OPTRACE 15 oqueue
X.RE
X.P
assigns a 15 line window to OPTRACE.
The other two files get what's left, on a 24 line screen
one would get 4 lines and the other 5 lines.
X.P
Once watch is running, you can refresh the screen with
ctrl-L or quit by typing a "q" ctrl-d or "del" (interrupt).
X.P
You can set alarms in any or all of the windows to wake
you up when something happens.
To set an alarm just hit a digit corresponding to the window.
For the first (top) window just hit "1".
You'll see a star appear just to the right of the label to
remind you that the alarm is set. If the screen gets some
output the terminal bell will go off to wake you up and
the star will disappear.
You can set alarms in all windows by hitting "0".

X.SH BUGS
Can't handle all-numeric file names.
~FUNKY STUFF~
ls -l watch.1
echo x - watch.c
sed 's/^X//' > watch.c << '~FUNKY STUFF~'
#include <curses.h>
#include <ctype.h>
#include <fcntl.h>
#include <pwd.h>
#include <values.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/dir.h>
#include <signal.h>

#define BUFFSZ 256
#define MAXLINES 70
#define MAXFILES 16
#define MAX(x,y) ((x) > (y) ? (x) : (y))

struct fl {
   char *name ;  /* name of file or dir */
   int fd ;      /* file descriptor     */
   int isdir ;   /* TRUE if directory   */
   int lines ;   /* screen lines        */
   WINDOW *win ; /* window descriptor   */
   long tstamp ; /* timestamp for dir   */
   int bell ;    /* ring bell on output */
   int indpos ;  /* posn of bell ind    */
   int lc ;      /* last char in last buff */
   } file [MAXFILES] ;

struct fileinfo {
   char name [DIRSIZ+1] ;
   time_t mtime ;
   off_t fsize ;
   int uid ;
   } ;

int nf ;
int interval = 1 ;
int label = 1 ;

void indicate(f, c)
   struct fl *f ; int c ;
   {
   int x, y ;

   if (c == ' ')
      beep() ;
   if (label) {
      getyx(f->win, y, x) ;
      mvwaddch(f->win, 0, f->indpos+1, c) ;
      wmove(f->win, y, x) ;
      wrefresh(f->win) ;
      }
   f->bell = c ;
   }

int compare (a, b)
   /* Compare file creation times */
   struct fileinfo *a, *b ;
   {
   if (a->mtime == b->mtime)
      return strcmp(a->name, b->name) ;
   return a->mtime < b->mtime ? -1 : 1 ;
   }

int dodirect (d)
   struct fl *d ;
   /*
    * Check a directory for new files.
    * First scan requires a sort to
    * get files into time order.
    */
   {
   register int i ;
   int rflag = FALSE ;
   int n = 0 ;
   int slot ;
   time_t stamp, oldest ;
   static char fname [DIRSIZ+1] ;
   static char sname [128] ;
   static char tbuf [26] ;
   struct passwd *pwd, *getpwuid() ;
   struct direct dir ;
   struct stat fs ;

   struct fileinfo finfo [MAXLINES] ;

   if (d->tstamp == 0L) {
      for (i = 0 ; i < d->lines ; i++) /* initialize */
	 finfo[i].mtime = 0L ;

      while (read(d->fd, &dir, sizeof(struct direct)) ==
	 sizeof(struct direct))
	 if (dir.d_ino && dir.d_name[0] != '.') { /* non-empty */
	    strncpy(fname, dir.d_name, DIRSIZ) ;
	    sprintf(sname, "%s/%s", d->name, fname) ;
	    stat(sname, &fs) ;
	    slot = -1 ;
	    if (n < d->lines)
	       slot = n++ ;
	    else
	       for (oldest = MAXLONG, i = 0 ; i < d->lines ; i++)
		  if (finfo[i].mtime < oldest) {
		     oldest = finfo[i].mtime ;
		     slot = i ;
		     }
	    if (slot >= 0) {
	       strcpy(finfo[slot].name, fname) ;
	       finfo[slot].mtime = fs.st_mtime ;
	       finfo[slot].fsize = fs.st_size ;
	       finfo[slot].uid = fs.st_uid ;
	       }
	    }

      if (n > 1)
	 qsort((char *)finfo, n, sizeof(struct fileinfo), compare) ;

      for (i = 0 ; i < n ; i++) {
	 strncpy(tbuf, ctime(&finfo[i].mtime), 24) ;
	 if ((pwd = getpwuid(finfo[i].uid)) == NULL)
	    sprintf(sname, "%4d", finfo[i].uid) ;
	 else
	    strcpy(sname, pwd->pw_name) ;

	 if (i > 0)
	    waddch(d->win, '\n') ;
	 wprintw(d->win, "%s %10s %8ld  %s",
	 tbuf, sname, finfo[i].fsize, finfo[i].name) ;
	 stamp = finfo[i].mtime ;
	 }
      rflag = TRUE ;
      }

   else {                    /* look for new files */
      lseek(d->fd, 0L, 0) ;
      stamp = d->tstamp ;

      while (read(d->fd, &dir, sizeof(struct direct)) ==
	 sizeof(struct direct))
	 if (dir.d_ino && dir.d_name[0] != '.') { /* non-empty */
	    strncpy(fname, dir.d_name, DIRSIZ) ;
	    sprintf(sname, "%s/%s", d->name, fname) ;
	    stat(sname, &fs) ;
	    if (fs.st_mtime > d->tstamp) {
	       stamp = MAX(stamp, fs.st_mtime) ;
	       strncpy(tbuf, ctime(&fs.st_mtime), 24) ;
	       if ((pwd = getpwuid(fs.st_uid)) == NULL)
		  sprintf(sname, "%4d", fs.st_uid) ;
	       else
		  strcpy(sname, pwd->pw_name) ;
	       if (d->tstamp > 0L)
		  waddch(d->win, '\n') ;
	       wprintw(d->win, "%s %10s %8ld  %s",
	       tbuf, sname, fs.st_size, fname) ;
	       rflag = TRUE ;
	       }
	    }
	 }
   d->tstamp = stamp ;
   return rflag ;
   }

int dofile (f)
   struct fl *f ;
   /*
    * Display stuff added to the file.
    */
   {
   static char buff [BUFFSZ+1] ;
   int n ;
   int rflag = FALSE ;

   while ((n = read(f->fd, buff, BUFFSZ)) > 0) {
      if (f->lc == '\n')
	 waddch(f->win, '\n') ;
      if ((f->lc = buff[n-1]) == '\n')
	 buff[n-1] = '\0' ; /* remove final nl */
      waddstr(f->win, buff) ;
      rflag = TRUE ;
      }
   return rflag || f->lc == 0 ;
   }

void quit()
   /* clean up curses stuff */
   {
   alarm(0) ;
   signal(SIGINT,  SIG_IGN) ; /* Avoid interrupts */
   signal(SIGQUIT, SIG_IGN) ; /*      while       */
   signal(SIGTERM, SIG_IGN) ; /*   cleaning up    */
   endwin() ;
   putchar('\n') ;
   exit(0) ;
   }

long line2byte (fd, lines)
   int fd, lines ;
   /*
    * Convert an EOF relative line number
    * offset to a byte offset for lseek.
    */
   {
   int lc=0, cc=0, n, off ;
   register char *p ;
   long fsize ;
   static char buff [BUFFSZ] ;
   struct stat fs ;

   fstat(fd, &fs) ;
   fsize = fs.st_size ; /* file size in bytes */

   for (off = BUFFSZ ; ; off += BUFFSZ) {
      if (off >= fsize) {
	 lseek(fd, 0L, 0) ;
	 off = fsize ;
	 }
      else
	 lseek(fd, -(long)off, 2) ;

      n = read(fd, buff, BUFFSZ) ;
      if (lc == 0 && buff[n-1] == '\n')
	 lc = -1 ; /* don't count nl at EOF */

      for (p = buff+(n-1) ; p >= buff ; p--)
	 if (*p == '\n') {
	    if ((lc += cc / COLS + 1) >= lines)
	       return off - (p-buff) - ((lc-lines) * COLS + 1) ;
	    cc = 0 ;
	    }
	 else
	    cc++ ;

      if (off >= fsize)
	 return fsize ;
      }
   }

void wakeup ()
   {
   register int i ;
   int upd = 0 ;

   for (i = 0 ; i < nf ; i++)
      if ( (file[i].isdir ? dodirect : dofile) (&file[i]) ) {
	 if (file[i].bell != ' ')
	    indicate(&file[i], ' ') ;
	 wnoutrefresh(file[i].win) ;
	 upd = 1 ;
	 }
   
   if (upd)
      doupdate() ;
   signal(SIGALRM, wakeup) ;
   alarm(interval) ;
   }
    
int num (p)
   char *p ;
   /* test if p all digits */
   {
   for (; *p ; p++)
      if (!isdigit(*p))
	 return FALSE ;
   return TRUE ;
   }

main (argc, argv)
   int argc ; char *argv[] ;
   {
   register int i, c ;
   char *p ;
   int curline, tlines=0, defs ;
   int bad = FALSE ;
   int dummy ;
   struct stat fs ;
   WINDOW *w ;
   extern int optind ;
   extern char *optarg ;

   while ((i = getopt(argc, argv, "li:")) != EOF)
      switch(i) {
	 case 'l':
	    label = 0 ;
	    break ;
	 case 'i':
	    interval = atoi(optarg) ;
	    break ;
	 case '?': /* invalid flag */
	    bad = TRUE ;
	    break ;
	    }

   if (bad || optind == argc) {
      fprintf(stderr, "\nUsage: %s [-lt] [-i n] file [lines] ...\n", argv[0]) ;
      fprintf(stderr, "\nflags:\tl - Omit window labels\n") ;
      fprintf(stderr, "\ti - Set sleep interval to n secs (default 1)\n") ;
      exit(1) ;
      }

   for (nf = 0, i = optind ; i < argc ; i++) {
      p = argv[i] ;
      if (num(p) && nf > 0)
	 file[nf-1].lines = atoi(p) ;  /* screen lines */
      else {
	 if (nf >= MAXFILES) {
	    fprintf(stderr, "Too many files (max %d)\n", MAXFILES) ;
	    exit (1) ;
	    }
	 if ((file[nf].fd = open(file[nf].name = p, O_RDONLY)) < 0) {
	    fprintf(stderr, "Couldn't open %s\n", p) ;
	    bad = TRUE ;
	    }
	 else {                       /* dir or regular file ? */
	    fstat(file[nf].fd, &fs) ;
	    if (fs.st_mode & S_IFDIR)
	       file[nf].isdir = TRUE ;
	    else if (!fs.st_mode & S_IFREG) {
	       fprintf(stderr, "Cannot seek on %s\n", p) ;
	       bad = TRUE ;
	       }
	    }
	 nf++ ;
	 }
      }
   if (bad)
      exit(1) ;

   if (getenv("TERM") == NULL) {
      fprintf(stderr, "set TERM and try again\n") ;
      exit(1) ;
      }

   /* Check allocations of screen lines and */
   /* compute leftover to be allocated to   */
   /* unspecified allocations.              */

   for (i = 0 ; i < nf ; i++) {
      tlines += file[i].lines ;
      if (file[i].lines == 0)
	 defs++ ;
      }

   initscr() ;

   for (i = 0 ; i < nf ; i++)  /* allocate screen space */
      if (file[i].lines == 0) {
	 file[i].lines = MAX((LINES-tlines) / defs, label+1) ;
	 tlines += file[i].lines ;
	 defs-- ;
	 }

   if (tlines > LINES) {
      fprintf(stderr, "Need more than %d lines (%d)\n", LINES, tlines) ;
      quit() ;
      }

   /* Open and initialize windows */

   signal(SIGINT , quit) ; /* set up             */
   signal(SIGQUIT, quit) ; /*   signals for      */
   signal(SIGTERM, quit) ; /*     graceful death */

   for (curline = 0, i = 0 ; i < nf ; i++) {
      w = file[i].win = newwin(file[i].lines, 0, curline, 0) ;
      curline += file[i].lines ;
      wsetscrreg(w, label, file[i].lines-label) ;
      if (label) {
	 wmove(w, 0, 0) ;
	 if (nf > 1)
	    wprintw(w, "%d ", i+1) ;
	 wstandout(w) ;
	 waddstr(w, file[i].name) ;
	 if (file[i].isdir && 
	    *(file[i].name + (strlen(file[i].name)-1)) != '/')
	    waddch(file[i].win, '/') ;
	 getyx(w, dummy, file[i].indpos) ;
	 waddch(file[i].win, '\n') ;
	 wstandend(w) ;
	 file[i].lines-- ;
	 }
      file[i].bell = ' ' ;
      idlok(w, TRUE) ;
      scrollok(w, TRUE) ;
      leaveok(w, TRUE) ;
      }

   /* Seek to end of files */

   for (i = 0 ; i < nf ; i++)
      if (!file[i].isdir)
	 lseek(file[i].fd, -line2byte(file[i].fd, file[i].lines), 2) ;

   /* Set up modes for input */
   
   crmode() ;
   nonl() ;
   cbreak() ;
   noecho() ;
   clear() ;

   wakeup() ;

   for (;;) {
      while ((c = getchar()) == -1 && errno == EINTR)
         ;
      switch (c) {

	 case '\f':  /* refresh */
	    alarm(0) ;
	    clearok(curscr, TRUE) ;
	    wrefresh(curscr) ;
	    alarm(interval) ;
	    break ;

	 case '0':
	    for (i = 0 ; i < nf ; i++)
	       indicate(&file[i], '*') ;
	    break ;

	 case '1':
	 case '2':
	 case '3':
	 case '4':
	 case '5':
	 case '6':
	 case '7':
	 case '8':
	 case '9':
	    c -= '0' ;
	    if (c > nf)
	       flash() ;
	    else
	       indicate(&file[c-1], '*') ;
	    break ;

	 case 'q' :   /* quit */
	 case '\4':
	    quit() ;
	 }
      }
   /*NOTREACHED*/
   }
~FUNKY STUFF~
ls -l watch.c
sed 's/^X//' > Makefile << '~FUNKY STUFF~'
XCFLAGS=-O
Xwatch:	watch.c
X	cc $(CFLAGS) watch.c -o watch -lcurses
~FUNKY STUFF~
# The following exit is to ensure that extra garbage 
# after the end of the shar file will be ignored.
exit 0