/* vi: set sw=4 ts=4: */
/*
 * (sysvinit like) last implementation
 *
 * Copyright (C) 2008 by Patricia Muscalu <patricia.muscalu@axis.com>
 *
 * Licensed under the GPLv2 or later, see the file LICENSE in this tarball.
 */

#include "libbb.h"
#include <utmp.h>

/* NB: ut_name and ut_user are the same field, use only one name (ut_user)
 * to reduce confusion */

#ifndef SHUTDOWN_TIME
#  define SHUTDOWN_TIME 254
#endif

#define HEADER_FORMAT     "%-8.8s %-12.12s %-*.*s %-16.16s %-7.7s %s\n"
#define HEADER_LINE       "USER", "TTY", \
	INET_ADDRSTRLEN, INET_ADDRSTRLEN, "HOST", "LOGIN", "  TIME", ""
#define HEADER_LINE_WIDE  "USER", "TTY", \
	INET6_ADDRSTRLEN, INET6_ADDRSTRLEN, "HOST", "LOGIN", "  TIME", ""

enum {
	NORMAL,
	LOGGED,
	DOWN,
	REBOOT,
	CRASH,
	GONE
};

enum {
	LAST_OPT_W = (1 << 0),  /* -W wide            */
	LAST_OPT_f = (1 << 1),  /* -f input file      */
	LAST_OPT_H = (1 << 2),  /* -H header          */
};

#define show_wide (option_mask32 & LAST_OPT_W)

static void show_entry(struct utmp *ut, int state, time_t dur_secs)
{
	unsigned days, hours, mins;
	char duration[32];
	char login_time[17];
	char logout_time[8];
	const char *logout_str;
	const char *duration_str;
	time_t tmp;

	/* manpages say ut_tv.tv_sec *is* time_t,
	 * but some systems have it wrong */
	tmp = ut->ut_tv.tv_sec;
	safe_strncpy(login_time, ctime(&tmp), 17);
	snprintf(logout_time, 8, "- %s", ctime(&dur_secs) + 11);

	dur_secs = MAX(dur_secs - (time_t)ut->ut_tv.tv_sec, (time_t)0);
	/* unsigned int is easier to divide than time_t (which may be signed long) */
	mins = dur_secs / 60;
	days = mins / (24*60);
	mins = mins % (24*60);
	hours = mins / 60;
	mins = mins % 60;

//	if (days) {
		sprintf(duration, "(%u+%02u:%02u)", days, hours, mins);
//	} else {
//		sprintf(duration, " (%02u:%02u)", hours, mins);
//	}

	logout_str = logout_time;
	duration_str = duration;
	switch (state) {
	case NORMAL:
		break;
	case LOGGED:
		logout_str = "  still";
		duration_str = "logged in";
		break;
	case DOWN:
		logout_str = "- down ";
		break;
	case REBOOT:
		break;
	case CRASH:
		logout_str = "- crash";
		break;
	case GONE:
		logout_str = "   gone";
		duration_str = "- no logout";
		break;
	}

	printf(HEADER_FORMAT,
		   ut->ut_user,
		   ut->ut_line,
		   show_wide ? INET6_ADDRSTRLEN : INET_ADDRSTRLEN,
		   show_wide ? INET6_ADDRSTRLEN : INET_ADDRSTRLEN,
		   ut->ut_host,
		   login_time,
		   logout_str,
		   duration_str);
}

static int get_ut_type(struct utmp *ut)
{
	if (ut->ut_line[0] == '~') {
		if (strcmp(ut->ut_user, "shutdown") == 0) {
			return SHUTDOWN_TIME;
		}
		if (strcmp(ut->ut_user, "reboot") == 0) {
			return BOOT_TIME;
		}
		if (strcmp(ut->ut_user, "runlevel") == 0) {
			return RUN_LVL;
		}
		return ut->ut_type;
	}

	if (ut->ut_user[0] == 0) {
		return DEAD_PROCESS;
	}

	if ((ut->ut_type != DEAD_PROCESS)
	 && (strcmp(ut->ut_user, "LOGIN") != 0)
	 && ut->ut_user[0]
	 && ut->ut_line[0]
	) {
		ut->ut_type = USER_PROCESS;
	}

	if (strcmp(ut->ut_user, "date") == 0) {
		if (ut->ut_line[0] == '|') {
			return OLD_TIME;
		}
		if (ut->ut_line[0] == '{') {
			return NEW_TIME;
		}
	}
	return ut->ut_type;
}

static int is_runlevel_shutdown(struct utmp *ut)
{
	if (((ut->ut_pid & 255) == '0') || ((ut->ut_pid & 255) == '6')) {
		return 1;
	}

	return 0;
}

int last_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int last_main(int argc UNUSED_PARAM, char **argv)
{
	struct utmp ut;
	const char *filename = _PATH_WTMP;
	llist_t *zlist;
	off_t pos;
	time_t start_time;
	time_t boot_time;
	time_t down_time;
	int file;
	unsigned opt;
	smallint going_down;
	smallint boot_down;

	opt = getopt32(argv, "Wf:" /* "H" */, &filename);
#ifdef BUT_UTIL_LINUX_LAST_HAS_NO_SUCH_OPT
	if (opt & LAST_OPT_H) {
		/* Print header line */
		if (opt & LAST_OPT_W) {
			printf(HEADER_FORMAT, HEADER_LINE_WIDE);
		} else {
			printf(HEADER_FORMAT, HEADER_LINE);
		}
	}
#endif

	file = xopen(filename, O_RDONLY);
	{
		/* in case the file is empty... */
		struct stat st;
		fstat(file, &st);
		start_time = st.st_ctime;
	}

	time(&down_time);
	going_down = 0;
	boot_down = NORMAL; /* 0 */
	zlist = NULL;
	boot_time = 0;
	/* get file size, rounding down to last full record */
	pos = xlseek(file, 0, SEEK_END) / sizeof(ut) * sizeof(ut);
	for (;;) {
		pos -= (off_t)sizeof(ut);
		if (pos < 0) {
			/* Beyond the beginning of the file boundary =>
			 * the whole file has been read. */
			break;
		}
		xlseek(file, pos, SEEK_SET);
		xread(file, &ut, sizeof(ut));
		/* rewritten by each record, eventially will have
		 * first record's ut_tv.tv_sec: */
		start_time = ut.ut_tv.tv_sec;

		switch (get_ut_type(&ut)) {
		case SHUTDOWN_TIME:
			down_time = ut.ut_tv.tv_sec;
			boot_down = DOWN;
			going_down = 1;
			break;
		case RUN_LVL:
			if (is_runlevel_shutdown(&ut)) {
				down_time = ut.ut_tv.tv_sec;
				going_down = 1;
				boot_down = DOWN;
			}
			break;
		case BOOT_TIME:
			strcpy(ut.ut_line, "system boot");
			show_entry(&ut, REBOOT, down_time);
			boot_down = CRASH;
			going_down = 1;
			break;
		case DEAD_PROCESS:
			if (!ut.ut_line[0]) {
				break;
			}
			/* add_entry */
			llist_add_to(&zlist, memcpy(xmalloc(sizeof(ut)), &ut, sizeof(ut)));
			break;
		case USER_PROCESS: {
			int show;

			if (!ut.ut_line[0]) {
				break;
			}
			/* find_entry */
			show = 1;
			{
				llist_t *el, *next;
				for (el = zlist; el; el = next) {
					struct utmp *up = (struct utmp *)el->data;
					next = el->link;
					if (strncmp(up->ut_line, ut.ut_line, UT_LINESIZE) == 0) {
						if (show) {
							show_entry(&ut, NORMAL, up->ut_tv.tv_sec);
							show = 0;
						}
						llist_unlink(&zlist, el);
						free(el->data);
						free(el);
					}
				}
			}

			if (show) {
				int state = boot_down;

				if (boot_time == 0) {
					state = LOGGED;
					/* Check if the process is alive */
					if ((ut.ut_pid > 0)
					 && (kill(ut.ut_pid, 0) != 0)
					 && (errno == ESRCH)) {
						state = GONE;
					}
				}
				show_entry(&ut, state, boot_time);
			}
			/* add_entry */
			llist_add_to(&zlist, memcpy(xmalloc(sizeof(ut)), &ut, sizeof(ut)));
			break;
		}
		}

		if (going_down) {
			boot_time = ut.ut_tv.tv_sec;
			llist_free(zlist, free);
			zlist = NULL;
			going_down = 0;
		}
	}

	if (ENABLE_FEATURE_CLEAN_UP) {
		llist_free(zlist, free);
	}

	printf("\nwtmp begins %s", ctime(&start_time));

	if (ENABLE_FEATURE_CLEAN_UP)
		close(file);
	fflush_stdout_and_exit(EXIT_SUCCESS);
}
