/* $Copyright: $
 * Copyright (c) 1995 by Steve Baker
 * All Rights reserved
 *
 * This software is provided as is without any express or implied
 * warranties, including, without limitation, the implied warranties
 * of merchantability and fitness for a particular purpose.
 */
#include <utmp.h>
#include <sys/types.h>
#include <sys/time.h>
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

static char *version = "$Version: $ sac v1.0 (c) 1995 by Steve Baker $";

#define WTMP  "/etc/wtmp"

#define min(a,b)	((a) < (b) ? (a) : (b))
#define max(a,b)	((a) > (b) ? (a) : (b))
enum {
  TOTAL	  = 0x01,
  DAY     = 0x02,
  USER    = 0x03,
  AVERAGE = 0x10,
  HOUR    = 0x20
};
enum {FALSE=0,TRUE=1};

struct day *mkday();
struct usr *adduser(), *finduser();
void *malloc(), *realloc(), *malloc();

struct day {
  time_t start, stop;
  int day, month, year;
  time_t time;
  time_t h[24], logins;
  struct day *nxt;
} *days = NULL, *end=NULL;
/*
 * Keep a pointer at the end of the days list, since we'll usually be
 * adding crap to it, not to days before.  What 'o what do we do about
 * time changes aka time warps?
 */

struct usr {
  char user[9];
  time_t time;
  time_t h[24], logins;
  struct usr *nxt;
} *us;

struct user {
  char user[9];
  char line[12];
  time_t in;
  struct user *nxt;
} *usr;

char type = TOTAL, fix = FALSE;
int fd;
time_t total = 0;
int ndays = 0;

/*
 * sac [ -w wtmp ] [ -d ] [ -a ] [ -h ] [ -p ] [ users ]
 */
main(argc,argv)
int argc;
char **argv;
{
  char *file = WTMP;
  int i, j;

  for(i=1;i<argc;i++) {
    if (argv[i][0] == '-') {
      for(j=1;argv[i][j];j++) {
	switch (argv[i][j]) {
	  case 'w':
	    file = argv[i+1];
	    i++;
	    break;
	  case 'p':
	    type = USER | (type & 0xf0);
	    break;
	  case 'd':
	    type = DAY | (type & 0xf0);
	    break;
	  case 'a':
	    type |= AVERAGE;
	    break;
	  case 'h':
	    type |= HOUR;
	    break;
	  default:
	    fprintf(stderr,"usage: %s [-w wtmp] [-d] [-p] [-a] [-h] [userlist]\n", argv[0]);
	    exit(1);
	}
      }
    } else {
      adduser(argv[i]);
      fix = TRUE;
    }
  }

  fd = open(file,O_RDONLY);
  doit();
  close(fd);
}

doit()
{
  struct utmp u;

  while (read(fd,&u,sizeof(struct utmp)) == sizeof(struct utmp)) {
    checkday(u);  /* Do we have this day allocated? */
    /* Q: Why does the following bother me? */
    /* A: It may not handle all possible cases. Wtmp documentation sucks. */
    if (u.ut_line[0]) {
      if (u.ut_user[0]) {
	if (!strcmp("~",u.ut_line) && (!strcmp("reboot",u.ut_user) || !strcmp("shutdown",u.ut_user))) do_reboot(u);
	else if (u.ut_type == USER_PROCESS && strcmp("ftp",u.ut_line)) login(u);
	else if (u.ut_type == LOGIN_PROCESS) logout(u);
      } else logout(u);
    }
  }
  cleanup();
  report();
}

/*
 * Make sure the day that the wtmp entry corresponds to, exists in our
 * days list.  If day is less than the starting day by more than a day, or
 * greater than the last day by more than a day, allocate all the days in
 * between as well.
 */
checkday(u)
struct utmp u;
{
  struct day *d, *p;

  if (days == NULL) end = days = mkday(u.ut_time);
  else {
    if (u.ut_time < days->start) {
      p = d = mkday(u.ut_time);
      while (p->stop+1 < days->start) {
        p->nxt = mkday(p->stop+1);
	p = p->nxt;
      }
      p->nxt = days;
      days = d;
    } else if (u.ut_time > end->stop) {
      p = d = mkday(end->stop+1);
      while (p->stop < u.ut_time) {
        p->nxt = mkday(p->stop+1);
	p = p->nxt;
      }
      end->nxt = d;
      end = p;
    }
  }
}

/*
 * Makes a day entry.  We'll assume a day is exactly 24 hours or 86400
 * seconds long.  I don't know if this is the case or not.  I don't think
 * the time algorythm takes into account leap seconds.
 */
struct day *mkday(t)
time_t t;
{
  struct day *d;
  struct tm *tm;
  int i;

  d = malloc(sizeof(struct day));
  tm = localtime(&t);
  tm->tm_hour = tm->tm_min = tm->tm_sec = 0;

  d->start = mktime(tm);
  d->stop = d->start + 86399;
  d->nxt = NULL;
  d->day = tm->tm_mday;
  d->month = tm->tm_mon;
  d->year = tm->tm_year;
  d->logins = d->time = 0;
  for(i=0;i<24;i++) d->h[i] = 0;
  ndays++;
  return d;
}

login(u)
struct utmp u;
{
  struct user *q;

  /*
   * If we still have a login on this line, it's logged out for sure now!
   * Wtmp could be corrupted.
   */
  logout(u);

  q = malloc(sizeof(struct user));
  strncpy(q->line,u.ut_line,12);
  strncpy(q->user,u.ut_user,8);
  q->user[8] = 0;
  q->in = u.ut_time;
  q->nxt = usr;
  usr = q;
}

logout(u)
struct utmp u;
{
  struct user *p, *q, ux;

  for(p=q=usr;p;) {
    if (!strcmp(u.ut_line,p->line)) {
      release(p,u.ut_time);
      if (p == usr) {
	usr = p->nxt;
	free(p);
	p = q = usr;
      } else {
        q->nxt = p->nxt;
	free(p);
	p = q->nxt;
      }
      continue;
    }
    q = p;
    p = p->nxt;
  }
}

/*
 * logout everyone on reboots or crashes.
 */
do_reboot(u)
struct utmp u;
{
  struct user *p, *q;

  for(p=usr;p;) {
    release(p,u.ut_time);
    q = p;
    p=p->nxt;
    free(q);
  }
  usr = NULL;
}

/*
 * Apply login time for users who havn't logged out yet (or that the wtmp file
 * in particular indicates haven't logged out by the EOF) to the days that
 * are in days list.  Login time is not applied to days not listed in the
 * wtmp file (therefor all the days between the first and last wtmp entries).
 * The reason for this is that if you're inspecting an old wtmp file, the
 * wtmp may not indicate all logouts for the last day.  It is not possible
 * to know when the wtmp file really truly ends however, so we apply the
 * minimum of the current time or the ending time for the last day.
 */
cleanup()
{
  struct user *p;
  time_t t = time(0);

  for(p=usr;p;p=p->nxt)
    release(p,t);
}

/*
 * Release a login entry, applying the login time to the particular day
 * entries.
 * A user is logged in on a particular day when:
 *   in  >= start  &&  in  <= stop ||
 *   out >= start  &&  out <= stop ||
 *   in  <  start  &&  out >  stop
 */
release(u,t)
struct user *u;
time_t t;
{
  struct day *p;
  struct usr *up;
  signed long tx;
  int i;


  if ((signed long)(t - u->in) < 0) return;

  /* if we're logging usage / user then apply login time to user entry. */
  if ((type & 0x0f) == USER && (up = finduser(u->user))) {
    up->time += t - u->in;
    up->logins++;
    if ((type & HOUR) == HOUR) {
      if (u->in < end->start) {
	for(p=days;p;p=p->nxt) {
	  if (u->in >= p->start && u->in <= p->stop) apply_hours(u->in,t,p->start,up->h);
	  else if (t >= p->start && t <= p->stop) apply_hours(u->in,t,p->start,up->h);
	  else if (u->in < p->start && t > p->stop) for(i=0;i<24;i++) up->h[i] += 3600;
	}
      } else apply_hours(u->in,t,end->start,up->h);
    }
    return;
  }
  /* Total */
  if ((type & 0x0f) == TOTAL) {
    tx = t - u->in;
    total += max(0,tx);
    if ((type & HOUR) != HOUR) return;
  }
  /* Only DAYS left */
  if (u->in < end->start) {
    /* Ugh, it's probably yesterday, but we've got to start all the way at
       the beginning. */
    for(p=days;p;p=p->nxt) {
      if (u->in >= p->start && u->in <= p->stop) {
	p->time += (min(p->stop,t) - u->in) + 1;
	p->logins++;
	if ((type & HOUR) == HOUR) apply_hours(u->in,t,p->start,p->h);
      } else if (t >= p->start && t <= p->stop) {
	p->time += (t - max(p->start,u->in)) + 1;
	p->logins++;
	if ((type & HOUR) == HOUR) apply_hours(u->in,t,p->start,p->h);
      } else if (u->in < p->start && t > p->stop) {
	p->time += 86400;
	p->logins++;
	if ((type & HOUR) == HOUR)
	  for(i=0;i<24;i++) p->h[i] += 3600;
      }
    }
  } else {
    end->time += min(end->stop,t) - max(end->start,u->in);
    end->logins++;
    if ((type & HOUR) == HOUR) apply_hours(u->in,t,end->start,end->h);
  }
}

apply_hours(in,out,start,h)
time_t in, out, start, h[24];
{
  int i;
  time_t b, e;

  b = start;
  e = start + 3599;
  for(i=0;i<24;i++) {
    if (in >= b && in <= e) h[i] += (min(e,out) - in) + 1;
    else if (out >= b && out <= e) h[i] += (out - max(b,in)) + 1;
    else if (in < b && out > e) h[i] += 3600;

    b += 3600;
    e += 3600;
  }
}

struct usr *adduser(s)
char *s;
{
  struct usr *u;

  u = malloc(sizeof(struct usr));
  strncpy(u->user,s,8);
  u->user[8] = 0;
  u->time = 0;
  u->nxt = us;
  us = u;
  return us;
}

struct usr *finduser(s)
char *s;
{
  struct usr *u;

  for(u=us;u;u=u->nxt)
    if (!strcmp(s,u->user)) return u;
  if (!fix) return adduser(s);
  else return NULL;
}

report()
{
  static char *month[] = {
    "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"
  };
  struct day *d;
  struct usr *u;
  time_t h[24];
  int i;
  
  switch(type & 0x0f) {
    case TOTAL:
      printf("Total: %12.2f over %d days.\n", (float)total/3600, ndays);
      if ((type & AVERAGE) == AVERAGE) printf("Average: %10.2f / day.\n",((float)total/3600) / ndays);
      if ((type & HOUR) == HOUR) {
	for(i=0;i<24;i++) h[i] = 0;
	for(d=days;d;d=d->nxt)
	  for(i=0;i<24;i++) h[i] += d->h[i];
	print_hours(h,total);
      }
      break;
    case DAY:
      for(d=days;d;d=d->nxt) {
	printf("%s %2d  total %10.2f",month[d->month],d->day,(float)d->time/3600);
	if ((type & AVERAGE) == AVERAGE) {
	  if (d->logins)
	    printf("\t%5d logins, %10.2f hours / login.",d->logins,((float)d->time/3600)/d->logins);
	  else
	    printf("\t   no logins");
	}
	putchar('\n');
	if ((type & HOUR) == HOUR) print_hours(d->h,d->time);
      }
      break;
    case USER:
      for(u=us;u;u=u->nxt) {
	printf("\t%-8s %10.2f",u->user,(float)u->time/3600);
	if ((type & AVERAGE) == AVERAGE) {
	  if (u->logins)
	    printf("\t%5d logins, %10.2f hours / login.",u->logins,((float)u->time/3600) / u->logins);
	  else
	    printf("\t   no logins");
	}
	putchar('\n');
	if ((type & HOUR) == HOUR) print_hours(u->h,u->time);
      }
      break;
  }
}

print_hours(h,total)
time_t h[24], total;
{
  static char *bar = "########################################################################";
  int i, bl = strlen(bar);
  float p[24], scale, maxp;

  for(i=0;i<24;i++) {
    p[i] = (float)h[i] / (float)total;
    if (p[i] > maxp) maxp = p[i];
  }
  scale = (float)bl / maxp;

  for(i=0;i<24;i++) {
    printf("%02d-: %.*s\n",i,(int)(scale*p[i]),bar);
  }
}
