/*	@(#)who.c	2.1		*/

/* Program to read the "/etc/utmp" file and list the various */
/* entries. */

#include <sys/types.h>
#include <sys/stat.h>
#include <utmp.h>
#include <stdio.h>
#include <pwd.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>

#define TRUE	1
#define FALSE	0
#define FAILURE	-1

#define MAXLINE	100

#define FLAGS	UTMAXTYPE

#define SIZEUSER	8

char username[SIZEUSER + 1];

int shortflag;		/* Set when fast form of "who" is to run. */
int ttystatus;		/* Set when write to tty status desired. */
int allflag;		/* Set when all entries are to be dumped. */

main(argc,argv)
int argc;
register char **argv;
{
	static int entries[FLAGS+1];	/* Flag for each type entry */
	register struct utmp *utmp;
	extern struct utmp *getutent();
	register int i;
	char *ptr,*eptr,*tptr;
	extern char *file,*strncpy(),*user(),*mode(),*line(),*etime(),*id();
	extern char *loc(),*ttyname(),*trash();
	char termstat[4],exitstat[4];
	struct passwd *passwd;
	extern char username[];
	extern struct passwd *getpwuid();
	char outbuf[BUFSIZ];
	extern int shortflag,ttystatus,allflag;

/* Make "stdout" buffered to speed things up. */

	setbuf(stdout, outbuf);

/* Look up our own name for reference purposes. */

	if((passwd = getpwuid(getuid())) == (struct passwd *)NULL) {
		 username[0] = '\0';
	} else {
/* Copy the user's name to the array "username". */
		 strncpy(&username[0],passwd->pw_name,SIZEUSER);
		 username[SIZEUSER] = '\0';
	}

/* Set "ptr" to null pointer so that in normal case of "who" */
/* the "ut_line" field will not be looked at.  "ptr" is used */
/* only for "w me" command. */

	ptr = NULL;
	shortflag = 0;		/* not set yet */
	ttystatus = FALSE;

	++argv;
	--argc;

	while(argc > 0) {
/* Is this the "who am i" sequence? */

		if(argc >= 2 && strcmp(*argv,"am") == 0 &&
		   strcmp(*(argv+1),"i") == 0) {
			entries[USER_PROCESS] = TRUE;

/* Which tty am I at?  Get the name and set "ptr" to just past */
/* the "/dev/" part of the pathname. */

			if((ptr = ttyname(fileno(stdin))) == NULL &&
			   (ptr = ttyname(fileno(stdout))) == NULL &&
			   (ptr = ttyname(fileno(stderr))) == NULL) {
				usage("process not attached to terminal");
			}
			ptr += sizeof("/dev/") - 1;
			argc -= 2;
			argv += 2;
			continue;
		} else if(**argv == '-') {

/* Analyze the switches and set the flags for each type of entry */
/* that the user requests to be printed out. */

			while(*++*argv) {
				switch(**argv) {
				case 'r' :
					entries[RUN_LVL] = TRUE;
					break;
				case 'b' :
					entries[BOOT_TIME] = TRUE;
					break;
				case 't' :
					entries[OLD_TIME] = TRUE;
					entries[NEW_TIME] = TRUE;
					break;
				case 'p' :
					entries[INIT_PROCESS] = TRUE;
					break;
				case 'l' :
					entries[LOGIN_PROCESS] = TRUE;
					if(shortflag == 0) shortflag = -1;
					break;
				case 'u' :
					entries[USER_PROCESS] = TRUE;
					if(shortflag == 0) shortflag = -1;
					break;
				case 'd' :
					entries[DEAD_PROCESS] = TRUE;
					break;
				case 'A' :
					entries[ACCOUNTING] = TRUE;
					break;
				case 'a' :
					for(i = 1; i < FLAGS; i++) entries[i] = TRUE ;
					if(shortflag == 0) shortflag = -1;
					allflag = TRUE;
					break;
				case 's' :
					shortflag = 1;
					break;
				case 'T' :
					ttystatus = TRUE;
					break;
				case '?' :
				case 'h' :
					usage("");
					break;
				default :
					usage("bad switch");
				}
			}		/* End of "while (*++*argv)" */

/* Advance to next argument and reduce count of arguments left. */

			++argv;
			--argc;
			continue;
		} else if(access(*argv,0) != FAILURE) {
/* Is there an argument left?  If so, assume that it is a utmp */
/* like file.  If there isn't one, use the default file. */

			utmpname(*argv);
			++argv;
			--argc;
			continue;
		} else {
			usage("%s doesn't exist or isn't readable",*argv);
		}
	}

/* Make sure at least one flag is set. */

	for(i=0; entries[i] == FALSE && i <= FLAGS;i++) ;
	if(i > FLAGS) entries[USER_PROCESS] = TRUE;

/* Now scan through the entries in the utmp type file and list */
/* those matching the requested types. */

	while((utmp = getutent()) != NULL) {
/* Are we looking for this type of entry? */

		if(allflag || ((utmp->ut_type >= 0 && utmp->ut_type <= FLAGS) &&
		   entries[utmp->ut_type] == TRUE) ) {
			if(utmp->ut_type == EMPTY) {
				fprintf(stdout, "Empty slot.\n");
				continue;
			}
			if(ptr != NULL && strncmp(utmp->ut_line,ptr,sizeof(utmp->ut_line)) != 0)
				continue;
			fprintf(stdout, "%8.8s %s %12.12s %s",
				user(utmp), mode(utmp), line(utmp), etime(utmp));
			if(utmp->ut_type == RUN_LVL) {
				fprintf(stdout, "    %c    %d    %c", utmp->ut_exit.e_termination,
					utmp->ut_pid, utmp->ut_exit.e_exit);
			}
			if(shortflag < 0 &&
			   (utmp->ut_type == LOGIN_PROCESS
			    || utmp->ut_type == USER_PROCESS
			    || utmp->ut_type == INIT_PROCESS
			    || utmp->ut_type == DEAD_PROCESS)) {
				fprintf(stdout, "  %5d", utmp->ut_pid);
				if(utmp->ut_type == INIT_PROCESS
				   || utmp->ut_type == DEAD_PROCESS) {
					fprintf(stdout, "  id=%4.4s", id(utmp));
					if(utmp->ut_type == DEAD_PROCESS) {
						fprintf(stdout, " term=%-3d exit=%-3d",
							utmp->ut_exit.e_termination, utmp->ut_exit.e_exit);
					}
				} else if(utmp->ut_type == LOGIN_PROCESS
				    || utmp->ut_type == USER_PROCESS) {
					 fprintf(stdout, "  %s", loc(utmp));
				}
			}
		 	fprintf(stdout,"\n");
		}

	}		/* End of "while ((utmp = getutent()" */
	fclose(stdout);
}

usage(format,arg1)
char *format;
int arg1;
{
	fprintf(stderr,format,arg1);
	fprintf(stderr,"\nUsage:\twho [-rbtpludAasT] [am i] [utmp_like_file]\n");
	fprintf(stderr,"\nr\trun level\nb\tboot time\nt\ttime changes\n");
	fprintf(stderr,"p\tprocesses other than getty or users\n");
	fprintf(stderr,"l\tlogin processes\nu\tuseful information\n");
	fprintf(stderr,"d\tdead processes\nA\taccounting information\n");
	fprintf(stderr,"a\tall (rbtpludA options)\n");
	fprintf(stderr,"s\tshort form of who (no time since last output\
 or pid)\n");
	fprintf(stderr,"T\tstatus of tty (+ writable, - not writable,\
 x exclusive open, ? hung)\n");
	exit(1);
}

char *user(utmp)
struct utmp *utmp;
{
	static char uuser[sizeof(utmp->ut_user)+1];

	copypad(&uuser[0],&utmp->ut_user[0],sizeof(uuser));
	return(&uuser[0]);
}

char *line(utmp)
struct utmp *utmp;
{
	static char uline[sizeof(utmp->ut_line)+1];

	copypad(&uline[0],&utmp->ut_line[0],sizeof(uline));
	return(&uline[0]);
}

copypad(to,from,size)
register char *to;
char *from;
int size;
{
	register int i;
	register char *ptr;
	short printable;
	int halfway;

	size--;	/* Leave room for null */

/* Scan for something textual in the field.  If there is */
/* nothing except spaces, tabs, and nulls, then substitute */
/* '-' for the contents. */

	for(printable = FALSE,i=0,ptr = from;
	    *ptr != '\0' && i < size;i++,ptr++) {
/* Break out if a printable character found.  Note that the test */
/* for "> ' '" eliminates all control characters and spaces from */
/* consideration as printing characters. */

		if(*ptr > ' ') {
			printable = TRUE;
			break;
		}
	}
	i = 0;
	halfway = 0;
	if(printable) {
		for(; *from != '\0' && i < size;i++) *to++ = *from++ ;
	} else {
		halfway = (size+1)/2 - 1;	/* Where to put "-" */
	}

/* Add pad at end of string consisting of spaces and a '\0'. */
/* Put an asterisk at the halfway position.  (This only happens */
/* when padding out a null field.) */

	for(; i < size;i++) {
		*to++ = (i == halfway ? '.' : ' ');
	}
	*to = '\0';	/* Add null at end. */
}

char *id(utmp)
register struct utmp *utmp;
{
	static char uid[sizeof(utmp->ut_id)+1];
	register char *ptr;
	register int i;

	for(ptr= &uid[0],i=0; i < sizeof(utmp->ut_id);i++) {
		if(isprint(utmp->ut_id[i]) || utmp->ut_id[i] == '\0') {
			*ptr++ = utmp->ut_id[i];
		} else {
			*ptr++ = '^';
			*ptr = (utmp->ut_id[i] & 0x17) | 0100;
		}
	}
	*ptr = '\0';
	return(&uid[0]);
}

char *etime(utmp)
struct utmp *utmp;
{
	extern char *ctime();
	register char *ptr;
	long lastactivity;
	char device[20];
	static char eetime[24];
	extern long time();
	struct stat statbuf;
	extern int shortflag;

	ptr = ctime(&utmp->ut_time);

/* Erase the seconds, year and \n at the end of the string. */

	*(ptr + 16) = '\0';

/* Advance past the day of the week. */

	ptr += 4;

	strcpy(&eetime[0],ptr);
	if(shortflag < 0
	   && (utmp->ut_type == INIT_PROCESS
	   || utmp->ut_type == LOGIN_PROCESS
	   || utmp->ut_type == USER_PROCESS
	   || utmp->ut_type == DEAD_PROCESS)) {
		sprintf(&device[0],"/dev/%s",&utmp->ut_line[0]);

/* If the device can't be accessed, add a hyphen at the end. */

		if(stat(&device[0],&statbuf) == FAILURE) {
			strcat(&eetime[0],"   .  ");
		} else {
/* Compute the amount of time since the last character was sent to */
/* the device.  If it is older than a day, just exclaim, otherwise */
/* if it is less than a minute, put in an '-', otherwise put in */
/* the hours and the minutes. */

			lastactivity = time(NULL) - statbuf.st_mtime;
			if(lastactivity > 24L*3600L) {
				strcat(&eetime[0],"  old ");
			} else if(lastactivity < 60L) {
				strcat(&eetime[0],"   .  ");
			} else {
				sprintf(&eetime[12]," %2u:%2.2u",
					(unsigned)(lastactivity/3600),
					(unsigned)((lastactivity/60)%60));
			}
		}
	}
	return(&eetime[0]);
}

static int badsys;	/* Set by alarmclk() if open or close times out. */

char *mode(utmp)
struct utmp *utmp;
{
	char device[20];
	int fd;
	struct stat statbuf;
	register char *answer;
	extern char username[];
	extern alarmclk();
	extern ttystatus;

	if(ttystatus == FALSE
	   || utmp->ut_type == RUN_LVL
	   || utmp->ut_type == BOOT_TIME
	   || utmp->ut_type == OLD_TIME
	   || utmp->ut_type == NEW_TIME
	   || utmp->ut_type == ACCOUNTING
	   || utmp->ut_type == DEAD_PROCESS) return(" ");

	sprintf(&device[0],"/dev/%s",&utmp->ut_line[0]);

/* Check "access" for writing to the line. */
/* To avoid getting hung, set up any alarm around the open.  If */
/* the alarm goes off, print a question mark. */

	badsys = FALSE;
	signal(SIGALRM,alarmclk);
	alarm(3);
#ifdef	CBUNIX
	fd = open(&device[0],O_WRONLY);
#else
	fd = open(&device[0],O_WRONLY|O_NDELAY);
#endif
	alarm(0);
	if(badsys) return("?");

	if(fd == FAILURE) {
/* If our effective id is "root", then send back "x", since it */
/* must be exclusive use. */

		if(geteuid() == 0) return("x");
		else return("-");
	}

/* If we are effectively root or this is a login we own then we */
/* will have been able to open this line except when the exclusive */
/* use bit is set.  In these cases, we want to report the state as */
/* other people will experience it. */

	if(geteuid() == 0 || strncmp(&username[0],&utmp->ut_user[0],
	   SIZEUSER) == 0) {
		if(fstat(fd,&statbuf) == FAILURE) answer = "-";
		if(statbuf.st_mode&2) answer = "+";
		else answer = "-";
	} else answer = "+";

/* To avoid getting hung set up any alarm around the close.  If */
/* the alarm goes off, print a question mark. */

	badsys = FALSE;
	signal(SIGALRM,alarmclk);
	alarm(3);
	close(fd);
	alarm(0);
	if(badsys) answer = "?";
	return(answer);
}

alarmclk()
{
/* Set flag saying that "close" timed out. */

	badsys = TRUE;
}

struct ttyline
{
	char *t_id;		/* Id of the inittab entry */
	char *t_comment;	/* Comment field if one found */
};

static FILE *fpit;
static long start,current;

char *loc(utmp)
struct utmp *utmp;
{
	struct ttyline *parseitab();
	register struct ttyline *pitab;
	long start,current;
	register int wrap;
	extern long ftell();

/* Open /etc/inittab if it is not already open. */

	if(fpit == NULL) {
		if((fpit = fopen("/etc/inittab","r")) == NULL) return("");
	}

/* Save the start position in /etc/inittab. */

	start = ftell(fpit);

/* Look through /etc/inittab for the entry relating to */
/* the line of interest.  To save time, do not start at beginning */
/* of /etc/inittab, but at the current point.  Care must be taken */
/* to stop after the whole file is scanned. */

	for(current=start,wrap=FALSE; wrap == FALSE || current < start;
	    current = ftell(fpit)) {
/* If a line of interest was found, see if it was the line we */
/* were looking for. */

		if((pitab = parseitab()) != (struct ttyline *)NULL) {
			if(strncmp(pitab->t_id,utmp->ut_id,sizeof(utmp->ut_id)) == 0)
				return(pitab->t_comment);
		} else {

/* If a line wasn't found, we've hit the end of /etc/inittab.  Set */
/* the wrap around flag.  This will start checking to prevent us */
/* from cycling through /etc/inittab looking for something that is */
/* not there. */

			wrap = TRUE;
		}
	}
	return("");
}

struct ttyline *parseitab()
{
	static struct ttyline answer;
	static char itline[512];
	register char *ptr,*optr;
	register int lastc;
	int i,cflag;
	extern char *strrchr();

	for(;;) {
/* Read in a full line, including continuations if there are any. */

		ptr = &itline[0];
		do {
			if(ptr == &itline[0]
			   && fgets(ptr, &itline[sizeof(itline)]-ptr,fpit) == (char *)NULL) {
				fseek(fpit,0L,0);
				return((struct ttyline *)NULL);
			}

/* Search for the <nl> at the end of the line, keeping track of */
/* quoting so that we will know if the <nl> is quoted. */

			for(lastc='\0'; *ptr && *ptr != '\n';
			    lastc = (lastc == '\\'? '\0':*ptr) , ptr++);

/* If there is no <nl> at the end of the line or the <nl> is not */
/* quoted, then set "cflag" to FALSE so that the "do" will */
/* terminate. */

			if(*ptr == '\0' || lastc != '\\') {
				cflag = FALSE;
				if(*ptr) *ptr = '\0';	/* Cover the <nl> */
				else {
					cflag = TRUE;

/* Cover up the quoted <nl> by backing up to the backslash. */

					--ptr;
				}
			}
		} while(cflag);

/* Save pointer to the id field of the inittab entry. */

		answer.t_id = &itline[0];
		answer.t_comment = "";

/* Skip over the first three fields (delimited by ':'). */

		for(i=3,ptr= &itline[0],cflag=TRUE; i;ptr++) {
/* If the line is badly formatted, ignore it. */

			if(*ptr == '\0') {
				cflag = FALSE;
				break;
			}

/* Turn colons into nulls so that fields are seperate strings. */

			if(*ptr == ':') {
				*ptr = '\0';
				i--;
			}
		}

		if(cflag == FALSE) continue;

/* Search for a comment field.  This may be one of the form */
/* ";: comment <null>" or one of the form "# comment <null>" */
/* Note that while ";: comment; command" is legal shell syntax, */
/* it is not expected on a getty line in inittab and is actually */
/* improperly formed since only the first command in inittab */
/* will be executed since there is an implicit "exec" to the */
/* shell command line. */

		for(lastc='\0';
		    *ptr && *ptr != ';' && lastc != '\\' && *ptr != '#';
		    lastc = (lastc == '\\'?'\0':*ptr), ptr++);

/* If there is no comment, return immediately. */

		if(*ptr == '\0') return(&answer);

/* If there is an unquoted semicolon look to see if the argument */
/* following it is colon and a space or a tab. */

		else if(*ptr == ';') {
			while(*++ptr == ' ' || *ptr == '\t');
			if(*ptr != ':' || (*++ptr != ' ' && *ptr != '\t'))
				return(&answer);
		}

/* After the "#" or ":; " sequence, skip over white space and */
/* return the remainder as the comment. */

		while(*++ptr == ' ' || *ptr == '\t');
		if(*ptr == '\0') return(&answer);
		answer.t_comment = ptr;
		return (&answer);
	}
}

/* "trash" prints possibly garbage strings so that non-printing */
/* characters appear as visible characters. */

char *trash(ptrin,size)
register char *ptrin;
register int size;
{
	static char answer[128];
	register char *ptrout;

	ptrout = &answer[0];
	while(--size >= 0) {
/* If the character to be printed is negative, print it as <-x>. */

		if(*ptrin & 0x80) {
			*ptrout++ = '<';
			*ptrout++ = '-';
		}
		if(isprint(*ptrin&0x7f)) *ptrout++ = (*ptrin&0x7f);

/* If the low seven bits of the character are not printable, */
/* print as ^x, where 'x' is the low seven bits plus 0x40. */

		else {
			*ptrout++ = '^';
			*ptrout++ = (*ptrin&0x7f) + 0x40;
		}

/* Finish up the corner brackets if the character was negative. */

		if(*ptrin & 0x80) *ptrout++ = '>';
		ptrin++;
	}
	return(&answer[0]);
}
