/*
 * w.c  v1.2
 *
 * An alternative "w" program for Linux.
 * Shows users and their processes.
 *
 * Copyright (c) Dec 1993 Steve "Mr. Bassman" Bryant
 * 			  bassman@hpbbi30.bbn.hp.com
 *
 * Info:
 *	I starting writing as an improvement of the w program included
 * with linux. The idea was to add in some extra functionality to the
 * program, and see if I could fix a couple of bugs which seemed to
 * occur.
 *	The code was written from scratch by myself, and initial versions
 * simply returned information from /etc/utmp and /proc. However, I since
 * received a copy of the procps source, and have changed my code to call
 * some of the routines there.
 *	Thanks to the other writers, without whom I'd have probably still
 * been stuck on one or two things !
 *
 *						Mr. Bassman, 12/93
 *
 * Acknowledgments:
 *
 * The original version of w:
 *	Copyright (c) 1993 Larry Greenfield  (greenfie@gauss.rutgers.edu)
 *
 * Uptime routine and w mods:
 *	Michael K. Johnson  (johnsonm@stolaf.edu)
 *
 *
 * Distribution:
 *	This program is freely distributable under the terms of copyleft.
 *	No warranty, no support, use at your own risk etc.
 *
 * Compilation:
 *	gcc -o w sysinfo.c whattime.c w.c
 *
 * Usage:
 *	w [-hus] [user]
 *
 * $Log: w2.c,v $
 * Revision 1.4  1994/01/01  12:57:21  johnsonm
 * Fixed log messages...
 *
 * Revision 1.3  1994/01/01  10:37:44  johnsonm
 * More changes, still doesn't quite work.
 *
 * Revision 1.2  1994/01/01  08:47:02  johnsonm
 * More changes, still doesn't quite work.
 *
 * Revision 1.1  1993/12/31  20:16:37  johnsonm
 * Eliminated GCC warnings, took out unnecessary dead variables in
 * fscanf, replacing them with *'d format qualifiers.
 * Also added RCS stuff.
 *
 *
 */

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <utmp.h>
#include <unistd.h>
#include <errno.h>
#include <pwd.h>
#include "whattime.h"


#define TRUE 1
#define FALSE 0
#define ZOMBIE "<zombie>"


void put_syntax();
char *idletime();
char *logintime();

static char rcsid[]="$Id: w2.c,v 1.4 1994/01/01 12:57:21 johnsonm Exp $";

int main (argc, argv)

int argc;
char *argv[];

{
    int header=TRUE, long_format=TRUE, ignore_user=FALSE;
    int i, j, k;
    struct utmp *utmp_rec;
    struct stat stat_rec;
    struct passwd *passwd_entry;
    uid_t uid=0;
    char username[9], tty[13], rhost[17], login_time[27];
    char idle_time[7], what[256], pid[10];
    char out_line[256], file_name[256];
    char search_name[9];
    int jcpu, pcpu, tpgid, curr_pid, utime, stime, cutime, cstime;
    char state, comm[256];
    FILE *fp;


    search_name[0] = '\0';

/*
 * Process the command line
 */
    if (argc > 1)
    {
	if (argv[1][0] == '-')		/* 1st arg starts with '-' */
	{
	    for (j = 1; j < strlen(argv[1]); j++)
	    {
		switch (argv[1][j])
		{
		    case 'h':
			header = FALSE;
			break;
		    case 's':
			long_format = FALSE;
			break;
                    case 'l':
                        long_format = TRUE;
                        break;
		    case 'u':
			ignore_user = TRUE;
			break;
		    default:
			fprintf(stderr, "w: unknown option: '%c'\n", argv[1][j]);
			put_syntax();
			break;
		}
	    }

	    if (argc > 2)
	    {
		strncpy(search_name, argv[2], 8);

		if (argc > 3)
		{
		    fprintf(stderr, "w: syntax error\n");
		    put_syntax();
		}
	    }
	}
	else
	{
	    strncpy(search_name, argv[1], 8);

	    if (argc > 2)
	    {
		fprintf(stderr, "w: syntax error\n");
		put_syntax();
	    }
	}
    }



/*
 * Check that /proc is actually there, or else we can't
 * get all the information.
 */

    if (chdir("/proc"))
    {
	fprintf(stderr, "w: fatal error: cannot access /proc\n");
	perror(strerror(errno));
	exit(-1);
    }




/*
 * Print whatever headers
 */
    if (header == TRUE)
    {
	print_uptime();

	if (long_format == TRUE)
	    printf("User     tty    From             login@   idle  JCPU  PCPU  what\n");
	else
	    printf("User     tty       idle  what\n");
    }




/*
 * Process user information.
 *
 * getutent() opens the utmp file for us,
 * and reads off the first record.
 * If the file was already opened, it reads the
 * subsequent record for us. See man page for more info.
 */
    while (utmp_rec = getutent())
    {

/*
 * Check we actually want to see this record.
 * It must be a valid active user process,
 * and match a specified search name.
 */
	if ( (utmp_rec->ut_type == USER_PROCESS)
	  && (strcmp(utmp_rec->ut_user, ""))
	  && ( (search_name[0] == '\0')
	    || ( (search_name[0] != '\0')
	    && !strncmp(search_name, utmp_rec->ut_user, 8) ) ) )
	{

/*
 * Important note:
 *
 * Strings in the utmp struct are *NOT*
 * necessarily null terminated :-(
 * This only occurs if the string fills
 * up its allocated space exactly.
 * If it is shorter, it is terminated by a null.
 * Because of this, strcpy breaks, and strncpy
 * won't necessarily add a terminator, so we have to
 * do it ourselves. The non-null-terminated string
 * can also cause problems for printf type calls,
 * otherwise I'd just read it direct !
 */

/* Get the username */
	    for (i = 0; i < 8; i++)
	    {
		username[i] = '\0';

		if (utmp_rec->ut_user[i] != '\0')
		    username[i] = utmp_rec->ut_user[i];
		else
		    i = 8;
	    }


/* If necessary, find out the uid of that user (from their passwd entry) */
	    if (ignore_user == FALSE)
	    {
		passwd_entry = getpwnam(username);
		uid = passwd_entry->pw_uid;
	    }


/* Get the tty line */
	    for (i = 0; i < 6; i++)
	    {
		tty[i] = '\0';

		if (utmp_rec->ut_line[i] != '\0')
		    tty[i] = utmp_rec->ut_line[i];
		else
		    i = 6;
	    }


/* Don't other getting info if it's not asked for */
	    if (long_format == TRUE)
	    {

/* Get the remote hostname */
		for (i = 0; i < 15; i++)
		{
		    rhost[i] = '\0';

		    if (utmp_rec->ut_host[i] != '\0')
			rhost[i] = utmp_rec->ut_host[i];
		    else
			i = 15;
		}


/*
 * Get the login time
 * (Calculated by LG's routine, below)
 */
		strcpy(login_time, logintime(utmp_rec->ut_time));

	    }



/*
 * Get the idle time.
 * (Calculated by LG's routine, below)
 */
	    strcpy(idle_time, idletime(tty));


/*
 * That's all the info out of /etc/utmp.
 * The rest is more difficult.
 * We use the pid from utmp_rec->ut_pid to look in /proc for the info.
 * NOTE: This is not necessarily the active pid, so we chase
 * down the path of parent -> child pids until we find it,
 * according to the information given in /proc/<pid>/stat.
 */

	    sprintf(pid, "%d", utmp_rec->ut_pid);

	    what[0] = '\0';
	    strcpy(file_name, pid);
	    strcat(file_name, "/stat");
	    jcpu = 0;
	    pcpu = 0;

	    if (fp = fopen(file_name, "r"))
	    {
		if (ignore_user == FALSE)
		{
/*
 * Use stat to find out who owns the file,
 * and therefore the process.
 */
		    stat(file_name, &stat_rec);
		    if (stat_rec.st_uid != uid)
			strcpy(what, "-");
		}

		while (what[0] == '\0')
		{
/*
 * Check /proc/<pid>/stat to see if the process
 * controlling the tty is the current one
 */
		    fscanf(fp, "%d %s %c %*d %*d %*d %*d %d %*u %*u %*u %*u %*u %d %d %d %d",
			&curr_pid, comm, &state, &tpgid,
			&utime, &stime, &cutime, &cstime);

		    fclose(fp);

		    if (comm[0] == '\0')
			strcpy(comm, "-");

/* Now get rid of the parentheses.  It doesn't work to put () around
 * the %s in the fscanf above, unfortunately.
 */
		    if (comm[0] == '(') /* this ought to be true*/
		    {
		        k = strlen(comm);
			k -= 2;
			memmove(&comm[0], &comm[1], k);
			comm[k] = '\0';
		    }

/*
 * Calculate jcpu and pcpu.
 * jcpu is the time used by all processes and their
 * children, attached to the tty.
 * pcpu is the time used by the current process
 * (calculated once after the loop, using last obtained values).
 *
 * NOTE:
 * 	While pcpu gives correct readings, jcpu does not.
 * I have tried two different methods (below).
 *	One reads from cutime and cstime, the first time the loop is
 * executed. These variables should give information about that process
 * and all its children.
 *	The other method adds everything up as we go. This second
 * method is probably less accurate, as not all processes are checked,
 * only those which have had control of the terminal, have given control
 * to another process, and are waiting for it to return, plus the current
 * controlling process; ie: it ignores background jobs. This should be
 * reasonably accurate if the user isn't running any background jobs.
 * However, both seem to return inaccurate information, and I don't
 * know why !
 *	The method used in the original program was to chase a
 * linked list. I may patch a version of that in, in addition to my
 * method. There are three possibilities why the results are wrong:
 * one, I've misinterpreted (but pcpu is ok, and I've processed both the
 * same); two, the info was wrong in the first place (is this possible ?);
 * three, it's something else. If anyone knows, please mail me, and I'll
 * patch it !!
 *	One more thought: cutime and cstime will retain records of process
 * time used by processes which no longer exist, so does this mean that they
 * are more accurate than going through the process list, and adding up
 * times from current processes ?
 */
		    if (!jcpu)
			jcpu = cutime + cstime;
/*
		    jcpu += utime + stime;
*/


/* Check for a zombie first... */
		    if (state == 'Z')
			strcpy(what, ZOMBIE);
		    else if (curr_pid == tpgid)
		    {
/*
 * If it is the current process, read cmdline
 * If that's empty, then the process is swapped out,
 * or is a zombie, so we use the command given in stat
 * which is in normal round brackets, ie: "()".
 */
			strcpy(file_name, pid);
			strcat(file_name, "/cmdline");
			if (fp = fopen(file_name, "r"))
			{
			    fgets(what, 255, fp);
			    fclose(fp);
			}

			if (what[0] != '\0')
			    strcpy(what, comm);
		    }
		    else
		    {
/* 
 * Check out the next process
 * If we can't open it, use info from this process,
 * so we have to check out cmdline first.
 * If we're not using "-u" then should we just
 * say "-" instead of a command line ?
 * If so, we should strpcy(what, "-"); when we fclose()
 * in the if after the stat().
 */
			strcpy(file_name, pid);
			strcat(file_name, "/cmdline");
			if (fp = fopen(file_name, "r"))
			{
			    fgets(what, 255, fp);
			    fclose(fp);
			}

			if (what[0] != '\0')
			    strcpy(what, comm);

/*
 * Now we have something in the what variable,
 * in case we can't open the next process.
 */
			sprintf(pid, "%d", tpgid);
			strcpy(file_name, pid);
			strcat(file_name, "/stat");

			fp = fopen(file_name, "r");

			if (fp && (ignore_user == FALSE))
			{
/*
 * We don't necessarily go onto the next process,
 * unless we are either ignoring who the effective
 * user is, or it's the same uid
 */
			    stat(file_name, &stat_rec);
			    if (stat_rec.st_uid != uid)
/* The next process is not owned by this user; finish loop. */
				fclose(fp);
/* strcpy(what, "-");  see comment above somewhere */
			    else
				what[0] = '\0';
			}
			else if (fp)
/* else we are ignoring uid's */
			    what[0] = '\0';
		    }
		}
	    }
	    else	/* Could not open first process for user */
		strcpy(what, "?");
/*
 * There is a bug somewhere in my version of linux
 * which means that utmp records are not cleaned
 * up properly when users log out. However, we
 * can detect this, by the users first process
 * not being there when we look in /proc.
 */


/*
 * Don't output a line for "dead" users.
 * This gets round a bug which doesn't update utmp/wtmp
 * when users log out.
 */
	    if (what[0] != '?')
	    {

/*
 * Format the line for output
 */
		if (long_format == TRUE)
		{
		    pcpu = utime + stime;
		    jcpu /= 100;
		    pcpu /= 100;


		    sprintf(out_line, "%-9.8s%-7.6s%-15.16s%8.8s%7.6s",
			    username, tty, rhost, login_time, idle_time);

		    if (!jcpu)
			sprintf(out_line, "%s      ", out_line);
		    else if (jcpu/60)
			sprintf(out_line, "%s%3d:%02d", out_line,
				jcpu/60, jcpu%60);
		    else
			sprintf(out_line, "%s    %2d", out_line, jcpu);

		    if (!pcpu)
			sprintf(out_line, "%s      ", out_line);
                    else if (pcpu/60)
			sprintf(out_line, "%s%3d:%02d", out_line,
				pcpu/60, pcpu%60);
		    else
			sprintf(out_line, "%s    %2d", out_line, pcpu);

		    strcat(out_line, "  ");
		    strcat(out_line, what);
		}
		else
		{
		    sprintf(out_line, "%-9.8s%-7.6s%7.6s  %s",
			username, tty, idle_time, what);
		}


		out_line[79] = '\0';		/* So we aren't too long */
		printf("%s\n", out_line);
	    }
	}
    }
    return(0);
}



/*
 * put_syntax()
 *
 * Routine to print the correct syntax to call this program,
 * and then exit out appropriately
 */
void put_syntax()
{
	fprintf(stderr, "usage: w [-hus] [user]\n");
	exit(-1);
}



/*
 * idletime()
 *
 * Routine which returns a string containing
 * the idle time of a given user.
 *
 * This routine was lifted from the original w program
 * by Larry Greenfield  (greenfie@gauss.rutgers.edu)
 * Copyright (c) 1993 Larry Greenfield
 *
 */
char *idletime(tty)

char *tty;

{
    struct stat terminfo;
    long idle;
    char ttytmp[40];
    static char give[20];
    time_t curtime;

    curtime = time(NULL);
    sprintf(ttytmp, "/dev/%s", tty);
    stat(ttytmp, &terminfo);
    idle = (curtime - terminfo.st_atime);

    if (idle >= (60 * 60))		/* more than an hour */
    {
	if (idle >= (60 * 60 * 48))	/* more than two days */
	    sprintf(give, "%2ddays", idle / (60 * 60 * 24));
	else
	    sprintf(give, " %2d:%s%d", idle / (60 * 60), 
		(idle / 60) % 60 < 10 ? "0" : "", (idle / 60) % 60);
    }
    else
    {
	if (idle / 60)
	    sprintf(give, "%6d", idle / 60);
	else
	    give[0]=0;
    }

    return give;
}



/*
 * logintime()
 *
 * Returns the time given in a suitable format
 *
 * This routine was lifted from the original w program
 * by Larry Greenfield  (greenfie@gauss.rutgers.edu)
 * Copyright (c) 1993 Larry Greenfield
 *
 */
char *logintime(ut_time)

time_t ut_time;

{
    time_t curtime;
    struct tm *logintime, *curtm;
    int hour, am, curday, logday;
    static char give[20];
    static char *weekday[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
    static char *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
				"Aug", "Sep", "Oct", "Nov", "Dec" };

    curtime = time(NULL);
    curtm = localtime(&curtime);
    curday = curtm->tm_yday;
    logintime = localtime(&ut_time);
    hour = logintime->tm_hour;
    logday = logintime->tm_yday;
    am = (hour < 12);

    if (!am)
	hour -= 12;

    if (hour == 0)
	hour = 12;

/*
 * This is a newer behavior: it waits 12 hours and the next day, and then
 * goes to the 2nd time format. This should reduce confusion.
 * It then waits only 6 days (not till the last moment) to go the last
 * time format.
 */
    if ((curtime > (ut_time + (60 * 60 * 12))) && (logday != curday))
    {
	if (curtime > (ut_time + (60 * 60 * 24 * 6)))
	    sprintf(give, "%2d%3s%2d", logintime->tm_mday,
		month[logintime->tm_mon], (logintime->tm_year % 100));
	else
	    sprintf(give, "%*s%2d%s", 3, weekday[logintime->tm_wday],
		hour, am ? "am" : "pm");
    }
    else
	sprintf(give, "%2d:%02d%s", hour, logintime->tm_min, am ? "am" : "pm");

    return give;
}

