[net.sources] a new and improved version of lastcomm

forys@gloria.UUCP (Jeff Forys) (06/13/85)

	When the load on our VAX 11/780 approaches 4 (considered average
here), lastcomm (version 4.9 from Berkeley) runs very slowly.  I started
to hack the code but then realized an entire rewrite was necessary.  I
changed it so that is uses hashing tables for the tty's and user acct's.
We tested it here and found that a typical search through our acct file
took only 30 seconds as compared to 20 minutes (i.e. we liked it).  It is
just like Berkeley's lastcomm and I added a few enhancements (see the
revised manual page for more info).  Also, when displaying the exit time
for a process, I included the seconds.

	If you use it, the only change you may have to make is the size
of the hashing tables (in particular, the users' hashing table).  The
constants UTABSIZ (user hashing table) and TTABSIZ (tty hashing table)
must be powers of 2.  The formula that I used in selecting the size of
the user table is:
			 # of users in /etc/passwd
		   X  =  -------------------------
				    7

and then raised or lowered "X" to the nearest power of two.  The tty
hashing table is probably okay as it is.  Increasing the table sizes
make the program more efficient and waste very little memory so is it
much better to over-compensate.

	Compile it with:
				cc -O lastcomm.c

	We've been using this program for ~2 months now and have
had no complaints as yet.  If you have any problems or suggestions,
please send UUCP mail to:

	..![bbncca,decvax,dual,rocksanne,watmath]!sunybcs!forys


							Jeff Forys

-------------------------------- Cut here -----------------------------
: to unbundle, "sh" this file -- DO NOT use csh
:  SHAR archive format.  Archive created Wed Jun 12 02:42:23 EDT 1985
echo Extracting\: lastcomm.1
sed 's/^X//' >lastcomm.1 <<'+PENGUIN+'
X.TH LASTCOMM 1 "11 June 1985"
X.UC
X.SH NAME
Xlastcomm \- show last commands executed in reverse order
X.SH SYNOPSIS
X.B lastcomm
X{[-c]cmd [-u]usr [-t]tty} [-b] [-f<flnm>] [-F<day>]
X.SH DESCRIPTION
X.I Lastcomm
Xgives information on previously executed commands.
XWith no arguments,
X.I lastcomm
Xprints information about all the commands recorded
Xduring the current accounting file's lifetime.
XIf called with arguments, only accounting entries with a
Xmatching command
Xname, user name, or terminal name are printed.
XSo, for example, `lastcomm a.out root ttyd0'
Xwill produce a listing of all the
Xexecutions of commands named
X.I 'a.out'
Xby user
X.I root
Xon the terminal
X.IR ttyd0 .
X.PP
XSince
X.I lastcomm
Xcategorizes its arguments there is a chance
Xthat it will do so incorrectly (e.g. a system
Xcommand is also a users' account name).
XIt will first try to match an argument with a
X.I tty,
Xand then with a
X.I user name,
Xbefore classifying it as a
X.I command.
XTo override this, an argument can be
X.B forced
Xto a particular type by preceding it with "-c" (command),
X"-u" (user), or "-t" (tty).
X.PP
XWhen the flag "-b" is present,
X.I lastcomm
Xstarts its search from the
Xbeginning of the accounting file (as opposed to the end).
XThe "-f" flag is used to specify a different accounting
Xfile while "-F" will use the file from a particular day of
Xthe week (e.g. "-FMon" uses "/usr/adm/attic/acct.Mon").
X.PP
XThe following are displayed for each process entry:
X.in +0.5i
XThe name of the user who ran the process.
X.br
XFlags, as accumulated by the accounting facilities in the system.
X.br
XThe command name under which the process was called.
X.br
XThe amount of cpu time used by the process (in seconds).
X.br
XThe time the process exited.
X.in -0.5i
X.PP
XThe flags are encoded as follows: ``S'' indicates the command was
Xexecuted by the super-user, ``F'' indicates the command ran after
Xa fork, but without a following 
X.IR exec ,
X``C'' indicates the command was run in PDP-11 compatibility mode
X(VAX only),
X``D'' indicates the command terminated with the generation of a
X.I core
Xfile, and ``X'' indicates the command was terminated with the signal
XSIGTERM.
X.SH "SEE ALSO"
Xlast(1),
Xsigvec(2),
Xacct(5),
Xcore(5)
X.SH AUTHOR
XJeff Forys (2 functions borrowed from Berkeley 4.9)
+PENGUIN+
ls -l lastcomm.1
echo Extracting\: lastcomm.c
sed 's/^X//' >lastcomm.c <<'+PENGUIN+'
X#ifndef lint
Xstatic char *sccsid = "@(#)lastcomm.c	5.4 (Jeff Forys @ SUNY/Buffalo) 6/10/85";
X#endif
X
X/*
X * last command
X *
X * 4.9	09/08/83 (Berkley)
X *
X * 5.0	04/03/85 (JEF)	Use hashing tables for improved speed
X * 5.1	04/05/85 (JEF)	Add "seconds" to display, otherwise identical to 4.9
X * 5.2	04/11/85 (JEF)	Add arguments "-f", "-F", "-b", "-c", "-u", and "-t"
X * 5.3	04/17/85 (JEF)	Improve argument processing, release on an 11/780
X * 5.4	06/10/85 (JEF)	Add usage info, review comments, rewrite manual page
X */
X
X#include <sys/param.h>
X#include <sys/acct.h>
X#include <sys/file.h>
X#include <stdio.h>
X#include <pwd.h>
X#include <sys/stat.h>
X#include <utmp.h>
X#include <struct.h>
X#include <ctype.h>
X#include <strings.h>
X#include <sys/dir.h>
X
X#define UTABSIZ 64		/* Size of user table (must be power of 2) */
X#define UMASK	(UTABSIZ-1)	/* The user mask (for hashing) */
X
X#define TTABSIZ	16		/* Size of tty table (must be power of 2) */
X#define TMASK	(TTABSIZ-1)	/* The tty mask (for hashing) */
X
X#define NORECS	10		/* Number of acct entries read in each time */
X
X#define NMAX	fldsiz(utmp, ut_name)	/* Size of name field in utmp */
X#define TMAX	fldsiz(utmp, ut_line)	/* Size of tty field in utmp */
X#define CMAX	fldsiz(acct, ac_comm)	/* Size of command field in acct */
X#define FMAX	fldsiz(acct, ac_flag)	/* Size of flag field in acct */
X
Xtypedef struct s_spec {		/* Used for lists of ttys, cmds, usrs */
X    int idnumb;				/* uid or tty # */
X    char *spec;				/* Item being searched for */
X    struct s_spec *next;		/* Next item in list */
X} SPECTYPE;
X
Xtypedef struct s_usrs {		/* Each entry in the user table */
X    char usrnam[NMAX+1];		/* User name */
X    int uid;				/* User id */
X    struct s_usrs *next;		/* Next user */
X} USERTYPE;
X
Xtypedef struct s_ttys {		/* Each entry in the tty table */
X    char ttynam[TMAX+1];		/* tty name */
X    dev_t ttynum;			/* tty number */
X    struct s_ttys *next;		/* Next tty */
X} TTYTYPE;
X
Xstruct acct buf[NORECS];	/* Blocks of acct entries read from file */
X
XUSERTYPE *utable[UTABSIZ];	/* Hash table for users */
XTTYTYPE *dtable[TTABSIZ];	/* Hash table for ttys */
X
XSPECTYPE *ttylst, *usrlst, *cmdlst;	/* Lists of ttys, users, & cmds */
Xchar *gottty, *gotusr, *gotcmd;		/* Pointers to tty, usr & cmd names */
X
Xstatic char *pname;					/* Program name */
Xstatic char *devdir = "/dev";				/* Device directory */
Xstatic char *actfil = "/usr/adm/acct";			/* Acct entries */
Xstatic char attic[50] = "/usr/adm/attic/acct.";		/* Old acct files */
X
Xmain(argc, argv)
Xint argc;
Xchar *argv[];
X{
X    int getargs();		/* Returns 1 for rev search, 0 for forward */
X    long p, lseek();		/* p = pos of read head in acct file */
X    register SPECTYPE *index;	/* Used to find matched cmds, ttys, & usrs */
X    register struct acct *acp;	/* Pointer to acct entry being processed */
X    register int fd, cc, rev;	/* cc = chars read, REVerse/forward search */
X
X    pname = argv[0];		/* Save name program is executing under */
X
X    rev = getargs(argc, argv);	/* Parse argv into users, ttys, & cmds */
X
X    if (usrlst == NULL)		/* We'll need a user table */
X	uhasher();
X
X    if (ttylst == NULL)		/* We'll need a tty table */
X	dhasher();
X
X    if ((fd=open(actfil, O_RDONLY)) < 0) {	/* Can't open acct file */
X	fprintf(stderr, "%s: Can't open %s\n", pname, actfil);
X	exit(1);
X    }
X
X    /* Position read head for reverse or forward search */
X    if (rev) {
X	p = lseek(fd, 0L, L_XTND) / (long) (NORECS * sizeof(struct acct));
X	p *= (long) (NORECS * sizeof(struct acct));
X    } else
X	p = 0L;
X
X    /* Read in NORECS records at-a-time, stop at beginning or end of file */
X
X    while (lseek(fd, p, L_SET) >= 0L) {
X
X	/* Get position for next read */
X	p=rev? (p-NORECS*sizeof(struct acct)): (p+NORECS*sizeof(struct acct));
X
X	if ((cc = read(fd, (char *) buf, NORECS * sizeof(struct acct))) < 0) {
X	    fprintf(stderr, "%s: Read error\n", pname);
X	    exit(1);
X	}
X
X	if (!rev && !cc)	/* We reached the end of the file */
X	    break;
X
X	/*
X	 *  We now check each acct entry in our buffer, against the stuff
X	 *  specified by the user (now categorized by users, ttys, and
X	 *  commands).  To be displayed, an acct entry must match at least
X	 *  one entry in each category.  A NULL list in any category will
X	 *  guarantee a match for that category.
X	 */
X
X	for (acp = rev? (buf + (cc / sizeof(struct acct)) - 1): buf;
X	     (acp >= buf && acp < &buf[NORECS]); rev? acp--: acp++) {
X
X	    gottty = gotusr = gotcmd = NULL;	/* Init string pointers */
X
X	    for (index=usrlst; index!=NULL; index=index->next)	/* Uid's */
X		if (acp->ac_uid == index->idnumb) {	/* User id matched */
X		    gotusr = index->spec;		/* Save name ptr */
X		    break;
X		}
X
X	    if (usrlst && !gotusr)		/* Failed to match user */
X		continue;
X
X	    for (index=ttylst; index!=NULL; index=index->next)	/* tty #'s */
X		if (acp->ac_tty == index->idnumb) {	/* tty #'s matched */
X		    gottty = index->spec;		/* Save tty ptr */
X		    break;
X		}
X
X	    if (ttylst && !gottty)		/* Failed to match tty */
X		continue;
X
X	    for (index=cmdlst; index!=NULL; index=index->next)	/* Commands */
X		if (!strncmp(acp->ac_comm, index->spec, CMAX)) {
X		    gotcmd = index->spec;	/* Cmds matched, save ptr */
X		    break;
X		}
X
X	    if (!cmdlst || gotcmd)		/* Matched command, so... */
X		display(acp);				/* Display info */
X	}
X    }
X
X    exit(0);	/* We probably set an error code on the seek, ignore it */
X}
X
X#define ARGVAL() (*++(*argv) || (--argc && *++argv))
X
Xgetargs(argc, argv)	/* Parse command line into cmds, ttys & usrs */
Xint argc;
Xchar *argv[];
X{
X    SPECTYPE *newspec();		/* Returns new record for something */
X    struct passwd *pp;			/* An entry from the passwd file */
X    struct stat st;			/* Ret'd stat info (need st_rdev) */
X    int length;				/* Length (chars) of path to device */
X    int reverse = 1;			/* Reverse search on acct file */
X    char devpath[50];			/* Path to /dev */
X    register SPECTYPE *id, *iu, *ic;	/* Index variables */
X    register int help;			/* set if args are 'helpful' */
X
X    ttylst = usrlst = cmdlst = id = iu = ic = NULL;	/* Init everything */
X
X    length = strlen(strcat(strcpy(devpath, devdir), "/"));	/* "/dev/" */
X
X    while (--argc > 0) {	/* For each argument */
X	help = 0;
X	if (**(++argv) == '-') {	/* Found a flag -- deal with it */
X	    switch (*++(*argv)) {
X		case 'F':			/* Use archived acct file */
X		    if (!ARGVAL())
X			wrong();
X		    actfil = strcat(attic, *argv);
X		    help = -1;
X		    break;
X		case 'f':			/* Use different acct file */
X		    if (!ARGVAL())
X			wrong();
X		    actfil = *argv;
X		    help = -1;
X		    break;
X		case 'b':			/* Start search from BOF */
X		    reverse = 0;
X		    help = -1;
X		    break;
X		case 'c':			/* Command is next */
X		    help++;
X		case 'u':			/* Username is next */
X		    help++;
X		case 't':			/* tty is next */
X		    help++;
X		    break;
X		default:
X		    wrong();
X	    }
X
X	    if (help<0 || (help>0 && !ARGVAL()))	/* Next argument */
X		continue;
X	}
X
X	/* Check if command line argument is a tty */
X	if (help < 2) {
X	    devpath[length] = '\0';			/* Chop off old dev */
X	    (void) strcat(devpath, *argv);		/* Make new dev path */
X	    if ((stat(devpath, &st)>=0) && ((st.st_mode & S_IFMT)==S_IFCHR)) {
X		if (ttylst == NULL)
X		    id = ttylst = newspec(*argv, st.st_rdev);
X		else
X		    id = id->next = newspec(*argv, st.st_rdev);
X		continue;
X	    }
X	}
X
X	/* It's not a tty, check if a username */
X	if ((!help || help == 2) && (pp=getpwnam(*argv)) != NULL) {
X	    if (usrlst == NULL)
X		iu = usrlst = newspec(*argv, pp->pw_uid);
X	    else
X		iu = iu->next = newspec(*argv, pp->pw_uid);
X	    continue;
X	}
X
X	/* It's not a tty or username, it MUST be a command */
X	if (cmdlst == NULL)
X	    ic = cmdlst = newspec(*argv, 0);
X	else
X	    ic = ic->next = newspec(*argv, 0);
X    }
X
X    return(reverse);	/* Return direction of search */
X}
X
Xwrong()		/* Display usage information and exit */
X{
X    fprintf(stderr, "Usage: %s {[-c]cmd [-u]usr [-t]tty} [-b] [-f<flnm>] [-F<day>]\n", pname);
X    exit(1);
X}
X
XSPECTYPE *newspec(str, num)	/* malloc a new record for something */
Xchar *str;
Xint num;
X{
X    char *malloc();
X    SPECTYPE *temp;
X
X    if ((temp = (SPECTYPE *) malloc(sizeof(SPECTYPE))) == NULL) {
X	fprintf(stderr, "%s: Insufficient memory for spec list\n", pname);
X	exit(1);
X    }
X
X    temp->idnumb = num;		/* Save param info in record */
X    temp->spec = str;
X    temp->next = NULL;
X
X    return(temp);
X}
X
Xuhasher()	/* Hash users into "utable" */
X{
X    char *malloc();
X    USERTYPE *nextfree[UTABSIZ];	/* Ptr's to last rec in each row */
X    register USERTYPE *newusr;		/* A newly malloc'd user record */
X    register struct passwd *pp;		/* An entry from the passwd file */
X    register int i;
X
X    for (i=0; i<UTABSIZ; i++)		/* Init tables to NULL */
X	utable[i] = nextfree[i] = NULL;
X
X    (void) setpwent();			/* Open passwd file for reading */
X
X    while ((pp=getpwent()) != NULL) {	/* For each entry */
X
X	if ((newusr=(USERTYPE *) malloc(sizeof(USERTYPE))) == NULL) {
X	    fprintf(stderr, "%s: Insufficient memory for user table\n", pname);
X	    exit(1);
X	}
X
X	(void) strncpy(newusr->usrnam, pp->pw_name, NMAX);	/* Get name */
X	newusr->usrnam[NMAX] = '\0';
X	i = newusr->uid = pp->pw_uid;		/* Get uid */
X	newusr->next = NULL;
X
X	if (utable[(i &= UMASK)])	/* At least one entry in utable[i] */
X	    nextfree[i] = nextfree[i]->next = newusr;
X	else				/* utable[i] was NULL, init it */
X	    nextfree[i] = utable[i] = newusr;
X    }
X    (void) endpwent();			/* Close the passwd file */
X}
X
Xdhasher()	/* Hash ttys into "dtable" */
X{
X    TTYTYPE *nextfree[TTABSIZ];		/* Ptr's to last rec in each row */
X    struct stat statb;			/* Ret'd stat info (need st_rdev) */
X    char *malloc(), cwd[MAXPATHLEN];	/* Current working directory */
X    register TTYTYPE *newtty;		/* A newly malloc'd tty record */
X    register struct direct *dp;		/* Entries in a directory */
X    register int i;
X    register DIR *fd;			/* File desc for tty directory */
X
X    for (i=0; i<TTABSIZ; i++)		/* Init tables to NULL */
X	dtable[i] = nextfree[i] = NULL;
X
X    (void) getwd(cwd);			/* Get current working directory */
X
X    /* Open the device directory and chdir over there */
X    if ((fd = opendir(devdir)) == NULL) {
X	fprintf(stderr, "%s: Can't open device directory\n", pname);
X	exit(1);
X    } else if (chdir(devdir)) {
X	fprintf(stderr, "%s: Can't chdir to device directory\n", pname);
X	exit(1);
X    }
X
X    while (dp=readdir(fd)) {	/* For each file in /dev */
X	if (dp->d_ino == 0)
X	    continue;
X
X	if ((stat(dp->d_name,&statb)<0)||((statb.st_mode & S_IFMT)!=S_IFCHR))
X	    continue;
X
X	if ((newtty = (TTYTYPE *) malloc(sizeof(TTYTYPE))) == NULL) {
X	    fprintf(stderr, "%s: Insufficient memory for tty table\n", pname);
X	    exit(1);
X	}
X
X	(void) strncpy(newtty->ttynam, dp->d_name, TMAX);	/* Get name */
X	newtty->ttynam[TMAX] = '\0';
X	i = newtty->ttynum = statb.st_rdev;		/* Get tty number */
X	newtty->next = NULL;
X
X	if (dtable[(i &= TMASK)])	/* At least one entry in dtable[i] */
X	    nextfree[i] = nextfree[i]->next = newtty;
X	else				/* dtable[i] was NULL, init it */
X	    nextfree[i] = dtable[i] = newtty;
X    }
X
X    (void) closedir(fd);
X    (void) chdir(cwd);		/* Just to be complete... */
X}
X
Xdisplay(acp)	/* Display command (and assoc. info) in 'acp' */
Xregister struct acct *acp;
X{
X    register char *cp;
X    time_t cpusec, expand();
X    char *flagbits(), *getname(), *gettty(), *ctime();
X
X    cpusec=expand((unsigned) acp->ac_utime)+expand((unsigned) acp->ac_stime);
X
X    acp->ac_comm[CMAX] = '\0';		/* Damages ac_utime (used above) */
X    for (cp = acp->ac_comm; *cp; cp++)	/* Remove any ctrl chars in command */
X	if (iscntrl(*cp))			/* Replace with '?' */
X	    *cp = '?';
X
X    printf("%-*s %s %-*s %-*s %4d sec%s %19.19s\n", CMAX, acp->ac_comm,
X	flagbits(acp->ac_flag),
X	NMAX, gotusr? gotusr: getname(acp->ac_uid),
X	TMAX, gottty? gottty: gettty(acp->ac_tty),
X	cpusec, (cpusec == 1)? " ": "s", ctime(&acp->ac_btime));
X}
X
Xchar *getname(uid)	/* Get user name from uid in hashed user table */
Xregister int uid;
X{
X    static char baduid[NMAX+1];		/* Returned uid if not found */
X    register USERTYPE *iu;		/* User indexer */
X
X    for (iu=utable[uid & UMASK]; iu!=NULL; iu=iu->next)
X	if (uid == iu->uid)		/* Uid's matched */
X	    return(iu->usrnam);			/* Return user name */
X
X    /* Fell thru -- couldn't find user id, return it as "(<uid>)" */
X    (void) sprintf(baduid, "(%d)", uid);
X    return(baduid);
X}
X
Xchar *gettty(number)	/* Get tty name from rdev# in hashed tty table */
Xregister dev_t number;
X{
X    register TTYTYPE *id;	/* tty indexer */
X
X    if (number == NODEV)	/* Probably one of those "root" deals */
X	return("__");
X
X    for (id=dtable[number & TMASK]; id!=NULL; id=id->next)
X	if (number == id->ttynum)	/* tty number's matched */
X	    return(id->ttynam);			/* Return tty name */
X
X    return("??");		/* Baffling */
X}
X
X#ifndef lint
Xstatic char *berkfunc1 = "@(#)expand	4.9 (Berkeley) 9/8/83";
X#endif
X
Xtime_t expand (t)
Xregister unsigned t;
X{
X    register time_t nt;
X
X    nt = t & 017777;
X    t >>= 13;
X
X    while (t) {
X	t--;
X	nt <<= 3;
X    }
X
X    return (nt);
X}
X
X#ifndef lint
Xstatic char *berkfunc2 = "@(#)flagbits	4.9 (Berkeley) 9/8/83";
X#endif
X
Xchar *flagbits(f)
Xregister int f;
X{
X    static char flags[8*FMAX+1];
X    register int i = 0;
X
X#define BIT(flag, ch)	flags[i++] = (f & flag) ? ch : ' '
X
X    BIT(ASU, 'S');
X    BIT(AFORK, 'F');
X    BIT(ACOMPAT, 'C');
X    BIT(ACORE, 'D');
X    BIT(AXSIG, 'X');
X    flags[i] = '\0';
X
X    return (flags);
X}
+PENGUIN+
ls -l lastcomm.c
exit 0