/*
 * prunewtmp -- prune the wtmp file:
 * all entries older than specified age are removed to oldfile
 */

static char rcsid[] = "@(#) $Id: prunelog.c,v 1.2 2001/06/16 16:10:33 davidn Exp $  Tellurian";

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <utmp.h>

static struct id_entry {
	struct utmp utmp;
	struct id_entry *next;
} *ids = 0, *free_ids = 0;

static time_t cutoff;
static int wtmp, owtmp, nwtmp;

#ifdef DEBUG
static showutmp(char *msg, struct utmp *ut)
{
	printf("%s ", msg);
	switch (ut->ut_type) {
	case UT_UNKNOWN: printf("UT_UNKNOWN "); break;
	case RUN_LVL: printf("RUN_LVL	 "); break;
	case BOOT_TIME: printf("BOOT_TIME "); break;
	case NEW_TIME: printf("NEW_TIME "); break;
	case OLD_TIME: printf("OLD_TIME "); break;
	case INIT_PROCESS: printf("INIT_PROCESS "); break;
	case LOGIN_PROCESS: printf("LOGIN_PROCESS "); break;
	case USER_PROCESS: printf("USER_PROCESS "); break;
	case DEAD_PROCESS: printf("DEAD_PROCESS "); break;
	default: printf("type %d ", ut->ut_type);
	}
	printf("pid %d, line %.*s ", ut->ut_pid, UT_LINESIZE, ut->ut_line);
	printf("id %.4s %.15s ", ut->ut_id, ctime(&ut->ut_time) + 4);
	printf("user %.*s\n", UT_NAMESIZE, ut->ut_user);
}
#endif

static void discard(struct utmp *ut)
{
	struct id_entry **id, *nxt;
	struct utmp_chain **u;

	for (id = &ids; *id; id = &(*id)->next)
		if (strncmp((*id)->utmp.ut_id, ut->ut_id, 4) == 0)
			break;
	
	if (*id)
	{
		/* free the id entry */
		nxt = (*id)->next;
		(*id)->next = free_ids;
		free_ids = *id;
		*id = nxt;
	}
}

static void remember(struct utmp *ut)
{
	struct id_entry **id;
	struct utmp_chain **u;

	/* try to find existing chain for this id */
	for (id = &ids; *id; id = &(*id)->next)
		if (strncmp((*id)->utmp.ut_id, ut->ut_id, 4) == 0)
			break;
	
	/* doesn't exist so create one */
	if (!*id)
	{
		if (free_ids)
		{
			*id = free_ids;
			free_ids = free_ids->next;
		}
		else if ((*id = malloc(sizeof(struct id_entry))) == 0)
		{
			perror("malloc id");
			exit(3);
		}
		(*id)->next = 0;
	}

	/* save the latest utmp entry */
	memcpy(&(*id)->utmp, ut, sizeof(struct utmp));
}

/* add end records to old file and start records to new file */
static void switch_sessions(void)
{
	while (ids)
	{
		short type = ids->utmp.ut_type;
		char line = *ids->utmp.ut_line;
		char user = *ids->utmp.ut_user;
		ids->utmp.ut_time = cutoff;
		*ids->utmp.ut_line = 0;
		*ids->utmp.ut_user = 0;
		ids->utmp.ut_type = INIT_PROCESS;
#ifdef DEBUG
		showutmp("new", &ids->utmp);
#endif
		write(nwtmp, &ids->utmp, sizeof(struct utmp));
		*ids->utmp.ut_line = line;
		ids->utmp.ut_type = LOGIN_PROCESS;
#ifdef DEBUG
		showutmp("new", &ids->utmp);
#endif
		write(nwtmp, &ids->utmp, sizeof(struct utmp));
		if (type == USER_PROCESS)
		{
#ifdef DEBUG
			showutmp("old", &ids->utmp);
#endif
			write(owtmp, &ids->utmp, sizeof(struct utmp));
			ids->utmp.ut_type = DEAD_PROCESS;
#ifdef DEBUG
			showutmp("old", &ids->utmp);
#endif
			write(owtmp, &ids->utmp, sizeof(struct utmp));

			*ids->utmp.ut_user = user;
			ids->utmp.ut_type = USER_PROCESS;
#ifdef DEBUG
			showutmp("new", &ids->utmp);
#endif
			write(nwtmp, &ids->utmp, sizeof(struct utmp));
		}
		ids = ids->next;
	}
}

main(int argc, char *argv[])
{
	struct stat junk;
	struct utmp ut, cur_boot_time, cur_run_level;
	char *wtmpname, *oldname, tmpname[100], *age;
	int xfer, got_boot_time = 0, got_run_level = 0;

	if (argc != 4)
	{
		fprintf(stderr, "usage: prunewtmp wtmpfile prunefile {age[s|m|h|d|w]}\n");
		exit(1);
	}

	wtmpname = argv[1];
	oldname = argv[2];
	age = argv[3];

	cutoff = time(0);
	while (*age)
	{
		long l = strtoul(age, &age, 10);
		switch (*age)
		{
		default:
			fprintf(stderr, "%s: %c is not a valid age factor; try one of s, m, h, d or w\n", argv[3], *age);
			exit(1);
		case 'w': l *= 7;
		case 'd': l *= 24;
		case 'h': l *= 60;
		case 'm': l *= 60;
		case 's': age++;
		case '\0': cutoff -= l;
		}
	}

	wtmp = open(wtmpname, O_RDONLY);
	if (wtmp == -1)
	{
		perror(wtmpname);
		exit(2);
	}

	owtmp = open(oldname, O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (owtmp == -1)
	{
		perror(oldname);
		exit(2);
	}

	nwtmp = 0;
	do
		sprintf(tmpname, "%s.%d.%d", wtmpname, getpid(), nwtmp++);
	while (stat(tmpname, &junk) == 0);
	nwtmp = open(tmpname, O_EXCL | O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (nwtmp == -1)
	{
		perror(tmpname);
		unlink(oldname);
		exit(2);
	}

	while ((xfer = read(wtmp, &ut, sizeof ut)) == sizeof ut)
	{
		if (ut.ut_time > cutoff)
			break;
#ifdef DEBUG
		showutmp("old", &ut);
#endif
		write(owtmp, &ut, sizeof ut);

		switch (ut.ut_type) {
		case RUN_LVL:
			got_run_level = 1;
			memcpy(&cur_run_level, &ut, sizeof ut);
			break;

		case BOOT_TIME:
			got_boot_time = 1;
			memcpy(&cur_boot_time, &ut, sizeof ut);
			break;

		case INIT_PROCESS:
			discard(&ut);
			/* fall through */
		case LOGIN_PROCESS:
		case USER_PROCESS:
			remember(&ut);
			break;

		case DEAD_PROCESS:
			discard(&ut);
			break;
		}
	}

	if (got_boot_time)
	{
#ifdef DEBUG
		showutmp("new", &ut);
#endif
		write(nwtmp, &cur_boot_time, sizeof cur_boot_time);
	}

	if (got_run_level)
	{
#ifdef DEBUG
		showutmp("new", &ut);
#endif
		write(nwtmp, &cur_run_level, sizeof cur_run_level);
	}

	switch_sessions();
	while (xfer == sizeof ut)
	{
#ifdef DEBUG
		showutmp("new", &ut);
#endif
		write(nwtmp, &ut, sizeof ut);
		xfer = read(wtmp, &ut, sizeof ut);
	}

	if (xfer > 0)
		fprintf(stderr, "%s: short record\n", wtmpname);
	if (rename(tmpname, wtmpname) == -1)
	{
		perror("rename");
		unlink(tmpname);
		unlink(oldname);
		exit(3);
	}

	return 0;
}
