/*
 * w.c - the w(1) utility
 * show what logged in users are doing
 * 
 * Almost entirely rewritten from scratch by Charles Blake circa June 1996.
 * Some vestigal traces of the original may exist.  That was done in 1993 by
 * Larry Greenfield with some fixes by Michael K. Johnson.
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <utmp.h>
#include <fcntl.h>
#include <locale.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include "proc/version.h"
#include "proc/whattime.h"
#include "proc/readproc.h"
#include "proc/devname.h"
#include "proc/procps.h"
#include "proc/sysinfo.h"

static int ignoreuser = 0;	/* for '-u' */
static proc_t **procs;		/* our snapshot of the process table */

#define USERSZ (sizeof u->ut_user)

/*
 * This routine is careful since some programs leave utmp strings
 * unprintable.  Always outputs at least 16 chars padded with spaces
 * on the right if necessary.
 */
static void print_host(char *host, int len)
{
	char *last;
	int width = 0;

	/* FIXME: there should really be a way to configure this... */
	/* for now, we'll just limit it to the 16 that the libc5 version
	 * of utmp uses.
	 */
	if (len > 16)
		len = 16;
	last = host + len;
	for (; host < last; host++) {
		if (isprint(*host) && *host != ' ') {
			fputc(*host, stdout);
			++width;
		} else {
			break;
		}
	}
	if (!width) {		/* blank fields screw up parsers */
		fputc('-', stdout);
		++width;
	}
	/* if *any* unprintables(or blanks), replace rest of line with spaces */
	while (width < 16) {
		fputc(' ', stdout);
		++width;
	}
}

/*
 * print_time_ival7 - compact 7 char format for time intervals
 *
 * This probably belongs in libproc.
 */
static void print_time_ival7(time_t t, int centi_sec, FILE * fout)
{
	if ((long) t < (long) 0) {	/* system clock changed? */
		printf("   ?   ");
		return;
	}
	if (t >= 48 * 60 * 60)	/* > 2 days */
		fprintf(fout, " %2ludays", t / (24 * 60 * 60));
	else if (t >= 60 * 60)	/* > 1 hour */
		fprintf(fout, " %2lu:%02um", t / (60 * 60),
			(unsigned) ((t / 60) % 60));
	else if (t > 60)	/* > 1 minute */
		fprintf(fout, " %2lu:%02u ", t / 60, (unsigned) t % 60);
	else
		fprintf(fout, " %2lu.%02us", t, centi_sec);
}

/**** stat the device file to get an idle time */
static time_t idletime(char *tty)
{
	struct stat buf;

	if (stat(tty, &buf) != 0)
		return 0;
	return time(NULL) - buf.st_atime;
}

/***** 7 character formatted login time */
static void print_logintime(time_t logt, FILE * fout)
{
	char weekday[][4] = {
		"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
	char month[][4] = {
		"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
		"Aug", "Sep", "Oct", "Nov", "Dec" };
	time_t curt;
	struct tm *logtm, *curtm;
	int hour;
	char *merid;		/* meridian indicator */
	int today;

	curt = time(NULL);
	curtm = localtime(&curt);
	/* localtime returns a pointer to static memory */
	today = curtm->tm_yday;
	logtm = localtime(&logt);
	hour = logtm->tm_hour;
	merid = (hour < 12) ? "am" : "pm";
	if (hour >= 12)
		hour -= 12;
	if (hour == 0)
		hour = 12;
	if (curt - logt > 12 * 60 * 60 && logtm->tm_yday != today) {
		if (curt - logt > 6 * 24 * 60 * 60)
			fprintf(fout, " %2d%3s%02d", logtm->tm_mday,
				month[logtm->tm_mon], logtm->tm_year % 100);
		else
			fprintf(fout, " %3s%2d%s", weekday[logtm->tm_wday],
				hour, merid);
	} else {
		fprintf(fout, " %2d:%02d%s", hour, logtm->tm_min, merid);
	}
}

/*
 * This function scans the process table accumulating total cpu times for
 * any processes "associated" with this login session.  It also searches
 * for the "best" process to report as "(w)hat" the user for that login
 * session is doing currently.  This the essential core of 'w'.
 */
static proc_t *getproc(struct utmp *u, char *tty, unsigned long long *jcpu,
		       int *found_utpid)
{
	int line;
	proc_t **p, *best = NULL, *secondbest = NULL;

	line = tty_to_dev(tty);
	*jcpu = 0;
	*found_utpid = 0;
	for (p = procs; *p; p++) {
		if ((**p).pid == u->ut_pid) {
			*found_utpid = 1;
			best = *p;
		}
		if ((**p).tty == line) {
			(*jcpu) += (**p).utime + (**p).stime;

			secondbest = *p;

			if ((ignoreuser
			     || !strncmp((**p).euser, u->ut_user, USERSZ))
			    && ((**p).pid == (**p).tpgid
				&& (!best || (**p).pid > best->pid)))
				best = *p;

		}
	}

	return best ? best : secondbest;
}

static void showinfo(struct utmp *u, int formtype, int maxcmd, int show_from)
{
	unsigned long long jcpu, i;
	int ut_pid_found;
	char uname[USERSZ + 1] = "", tty[5 + sizeof u->ut_line + 1] = "/dev/";
	proc_t *best;

	for (i = 0; i < sizeof(u->ut_line); i++) /* clean up tty if garbled */
		if (isalnum(u->ut_line[i]) || (u->ut_line[i] == '/'))
			tty[i + 5] = u->ut_line[i];
		else
			tty[i + 5] = '\0';

	best = getproc(u, tty + 5, &jcpu, &ut_pid_found);

	/*
	 * just skip if stale utmp entry (i.e. login proc doesn't exist).
	 * If there is a desire a cmdline flag could be added to optionally
	 * show it with a prefix of (stale) in front of cmd or something
	 * like that.
	 */
	if (!ut_pid_found)
		return;

	strncpy(uname, u->ut_user, USERSZ);	/* force NUL term for printf */
	if (formtype) {
		printf("%-9.8s%-9.8s", uname, u->ut_line);
		if (show_from)
			print_host(u->ut_host, sizeof u->ut_host);
		print_logintime(u->ut_time, stdout);
		if (*u->ut_line == ':')	/* idle unknown for xdm logins */
			printf("   ?   ");
		else
			print_time_ival7(idletime(tty), 0, stdout);
		print_time_ival7(jcpu / Hertz, (jcpu % Hertz) * (100. / Hertz),
				 stdout);
		if (best) {
			unsigned long long pcpu = best->utime + best->stime;
			print_time_ival7(pcpu / Hertz,
					 (pcpu % Hertz) * (100. / Hertz),
					 stdout);
		} else
			printf("   ?   ");
	} else {
		printf("%-9.8s%-9.8s", u->ut_user, u->ut_line);
		if (show_from)
			print_host(u->ut_host, sizeof u->ut_host);
		if (*u->ut_line == ':')	/* idle unknown for xdm logins */
			printf("   ?   ");
		else
			print_time_ival7(idletime(tty), 0, stdout);
	}
	fputs("  ", stdout);
	if (best) {
		if (best->cmdline)
			print_strlist(stdout, best->cmdline, " ", maxcmd);
		else
			printf("%*.*s", -maxcmd, maxcmd, best->cmd);
	} else {
		printf("-");
	}
	fputc('\n', stdout);
}

int main(int argc, char **argv)
{
	char *user = NULL;
	struct utmp *u;
	struct winsize win;
	int header = 1, longform = 1, show_from = 1, args, maxcmd = 80, ch;

	setlocale(LC_ALL, "");
	for (args = 0; (ch = getopt(argc, argv, "hlusfV")) != EOF; args++)
		switch (ch) {
		case 'h':
			header = 0;
			break;
		case 'l':
			longform = 1;
			break;
		case 's':
			longform = 0;
			break;
		case 'f':
			show_from = 0;
			break;
		case 'V':
			display_version();
			exit(0);
		case 'u':
			ignoreuser = 1;
			break;
		default:
			printf("usage: w -hlsufV [user]\n"
			       "    -h    skip header\n"
			       "    -l    long listing (default)\n"
			       "    -s    short listing\n"
			       "    -u    ignore uid of processes\n"
			       "    -f    don't show FROM field\n"
			       "    -V    display version\n");
			exit(1);
		}

	if ((argv[optind]))
		user = (argv[optind]);

	if (ioctl(1, TIOCGWINSZ, &win) != -1 && win.ws_col > 0)
		maxcmd = win.ws_col;
	if (maxcmd < 71) {
		fprintf(stderr, "%d column window is too narrow\n", maxcmd);
		exit(1);
	}
	maxcmd -= 29 + (show_from ? 16 : 0) + (longform ? 20 : 0);
	if (maxcmd < 3)
		fprintf(stderr, "warning: screen width %d suboptimal.\n",
			win.ws_col);

	procs = readproctab(PROC_FILLCMD | PROC_FILLUSR | PROC_SKIPTHREADS);

	if (header) {		/* print uptime and headers */
		print_uptime();
		putchar('\n');
		printf("USER     TTY      ");
		if (show_from)
			printf("FROM            ");
		if (longform)
			printf("  LOGIN@   IDLE   JCPU   PCPU  WHAT\n");
		else
			printf("   IDLE  WHAT\n");
	}

	utmpname(UTMP_FILE);
	setutent();
	while ((u = getutent())) {
		if (u->ut_type == USER_PROCESS &&
		    (user ? !strncmp(u->ut_user, user, USERSZ) : *u->ut_user))
			showinfo(u, longform, maxcmd, show_from);
	}
	endutent();

	return 0;
}
