[net.sources] sa - a simple accounting system

richmon@astrovax.UUCP (Michael Richmond) (08/29/85)

I wrote the following package for my AT&T 7300, but it should work
on just about any UNIX system with only minor modifications. It's
not very sophisticated, keeping track only of time, memory and I/O
operations on a per-user basis, but it's small and pretty easy to use.
The .doc file is formatted with the -man macros.

As always, let me know if there are bugs.

Michael Richmond			Princeton University, Astrophysics

{allegra,akgua,burl,cbosgd,decvax,ihnp4,noao,princeton,vax135}!astrovax!richmon


# This is a shell archive.  Remove anything before this line,
# then unpack it by saving it in a file and typing "sh file".
#
# Wrapped by richmon on Wed Aug 28 22:53:02 EDT 1985
# Contents:  README makefile acct.week accton.c sa.c sa.doc
 
echo x - README
sed 's/^@//' > "README" <<'@//E*O*F README//'
# To install accounting on your system, do the following:
#
#	0. su to super-user (and then to adm if you are cautious).
#
#	1. make a directory /usr/adm, owned by adm. unpack this archive
#	   inside it and type 'make' to produce the executable programs.
#
#	2. change the variable CLERK in 'acct.week' to be the person in charge
#	   of things (i.e. your login).
#
#	3. put the executables 'sa', 'accton' and the shell script
#	   'acct.week' into /usr/adm.
#
#	4. make the owner and group of 'sa' and 'acct.week' adm.
#
#	5. make the owner of 'accton' root, but the group adm.
#
#	6. change modes to
#
#		sa			755
#		acct.week	744
#		accton		4750
#
#	7. put the following line in /usr/lib/crontab (without the '#')
#
#		0 5 * * 2 /bin/su adm -c "/usr/adm/acct.week > /dev/null"
#
#	8. put the following lines in /etc/rc (again without the '#')
#	   if you desire to have the accounting process start up 
#	   automatically on system boot:
#
#	if test -x /usr/adm/accton 
#	then 
#		/usr/adm/accton /usr/adm/acctfile 1> /dev/null 2> /usr/adm/acct.err
#	fi
#
#	9. to start the accounting without reboot, su to super-user and type 
#
#			/usr/adm/accton /usr/adm/acctfile
#
#	   to halt it, su to super-user and type
#
#			/usr/adm/accton
#
#		
@//E*O*F README//
chmod u=rw,g=r,o=r README
 
echo x - makefile
sed 's/^@//' > "makefile" <<'@//E*O*F makefile//'
all: accton sa

accton: accton.c
	cc -o accton accton.c

sa: sa.c
	cc -o sa sa.c

@//E*O*F makefile//
chmod u=rw,g=r,o=r makefile
 
echo x - acct.week
sed 's/^@//' > "acct.week" <<'@//E*O*F acct.week//'
# this is a shell script which should be run once a week or so
#   via cron. first, it crunches the information contained in
#   the current accounting file into a more succint one which
#   contains the per-user stats and has a name indicating when
#   (i.e. which week) it was made. 
# next, it merges the current week's total with the those in the
#	file TOTALS, which will contain the sum of all per-user stats
#	since the time accounting is first started up (or the file
#	is removed).
# then it empties out the old accounting file and starts up the 
#	accounting process again.
# the presence of the file 'acct.err' indicates that something went 
#   wrong and needs to be fixed.

# the person in charge of accounting
CLERK=richmon

ACCTFILE=/usr/adm/acctfile
WEEKFILE=/usr/adm/week.`date | awk '{ print $2$3 }'`
TOTALS=/usr/adm/totfile

/bin/rm -f acct.err

#	create the file containing last week's stats
/usr/adm/sa > $WEEKFILE 2> acct.err
chmod 744 $WEEKFILE

#	merge this week's with the totals. if the file $TOTALS does
#	not exist, it will be created (hence the need to chmod).
/usr/adm/sa -m $TOTALS
chmod 744 $TOTALS

#	stop the accounting, empty the accounting file and restart
#	accounting. allow others to see, but not change, the file.
/usr/adm/accton 1> /dev/null 2>> acct.err
cp /dev/null $ACCTFILE
chmod 744 $ACCTFILE
/usr/adm/accton $ACCTFILE 1> /dev/null 2>> acct.err

if test -s acct.err
then
# there was an error. send mail to some responsible person.
	/bin/mail $CLERK < acct.err
else
	/bin/rm -f acct.err 2> /dev/null
fi
@//E*O*F acct.week//
chmod u=rwx,g=r,o=r acct.week
 
echo x - accton.c
sed 's/^@//' > "accton.c" <<'@//E*O*F accton.c//'
#include <stdio.h>
#include <errno.h>

	/* start (or end) the system accounting routine. the should be called
	   from /etc/rc with an argument file (such as /usr/adm/acctfile)
	   so that whenever the system boots, accounting
	   is enabled. all ouput goes into the (EXISTING) file
	   named as the argument. if called with no argument, 
	   accounting is turned off.

	   error messages are sent to the standard error - hence this should
	   be redirected to some logical place in the /etc/rc entry. */


main(argc, argv)
int argc;
char *argv[];
{

	int i, fd;

		
	if (argc < 2) {

		/* turn it off if possible */

		if (acct((char *) 0) < 0) {
			perror("Can't turn off accounting");
			exit(-1);
		}
		else {
			printf("Accounting disabled\n");
			exit(0);
		}
	}

	/* first create the file if it doesn't exist, since it is an error
	   to call acct() on a non-existent file. */

	if (access(argv[1]) < 0) {
		if (errno == ENOTDIR) {
			if ((fd = creat(argv[1], 033)) < 0) {
				perror("Can't create accounting file");
				exit(-1);
			}
			else
				close(fd);
		}
		else {
			perror("Can't create accounting file");
			exit(-1);
		}
	}	
		
	/* now try to make the acct() call */

	if (acct(argv[1]) < 0) {
		if (errno == EBUSY) {
			fprintf(stderr, "Accounting already in process\n");
			exit(-1);
		}
		else {
			perror("Can't start accounting");
			exit(-1);
		}
	}
	else
		printf("Accounting started\n");
}


@//E*O*F accton.c//
chmod u=rw,g=r,o=r accton.c
 
echo x - sa.c
sed 's/^@//' > "sa.c" <<'@//E*O*F sa.c//'
#include <stdio.h>
#include <fcntl.h>
#include <sys/acct.h>

	/*
	    purpose: go through the entries in /usr/adm/acctfile and sum
	  everything up by user, producing as output the total stats
	  per user, sorted by uid, to the standard output.

	    this should probably be run once every week or so, since
	  /usr/adm/acctfile can get really big quickly. a good idea
	  is to have it run, put the output somewhere sensible
	  (say /usr/adm/acct.week or something) and wipe the old
	  file clean. then the accounting process can be restarted.
	  a small shell script should do the trick.

	    it would also be nice to have some sort of merger program
	  to take the results from each week's dump and combine them
	  into a cumulative file.
	*/

#define MAXUSERS  1000
#define LINE_SIZE 75

struct acct my_acct;

struct s_stats {
		int uid;
		long utime;
		long stime;
		long etime;
		long mem;
		long io;
		long rw;
} s_array[MAXUSERS];

FILE *old_fp, *temp_fp;

char *tempfile;
char old_buf[100];		/* holds a line from the old accounting file, if any */

char deflt[]     = "/usr/adm/acctfile";
char progname[] = "sa";
char header[]   = "uid      user       sys         elapse        mem         io         rw";


main(argc, argv)
int argc;
char *argv[];
{
	int m_flag, err_flag, fd, nbytes;
	char *file, *old_file;

	file = deflt;
	m_flag = err_flag = 0;
	switch (argc) {
	case 1:
		break;
	case 2:
		file = argv[1];
		break;
	case 3:	
		if (strcmp(argv[1], "-m") == 0) {
			m_flag++;
			old_file = argv[2];
		}
		else
			err_flag = 1;
		break;
	case 4:
		if (strcmp(argv[1], "-m") == 0) {
			m_flag++;
			old_file = argv[2];
			file = argv[3];
		}
		else
			err_flag = 1;
		break;
	default:
		err_flag = 1;
		break;
	}

	if (err_flag) {
		fprintf(stderr, "usage: sa [ -m mergefile ] [ acctfile ]\n");
		exit(-1);
	}
		
	nbytes = sizeof(struct acct);

	if ((fd = open(file, O_RDONLY)) < 0) {
		perror(progname);
		exit(-1);
	}

	while (read(fd, (char *) &my_acct, nbytes) == nbytes) {
		process();
	}

	if (m_flag)
		merge(old_file);
	else
		stats();

}


	/* add the information in 'my_acct' to the appropriate
	   user's statistics. right now all we keep track of is

		user time,
		system time
		elapsed time
		memory usage
		characters transferred
		blocks read or written

	*/

process()
{
	int i;
	long expand();

	s_array[(i = my_acct.ac_uid)].uid = i;
	s_array[i].utime += expand(my_acct.ac_utime);
	s_array[i].stime += expand(my_acct.ac_stime);
	s_array[i].etime += expand(my_acct.ac_etime);
	s_array[i].mem   += expand(my_acct.ac_mem);
	s_array[i].io    += expand(my_acct.ac_io);
	s_array[i].rw    += expand(my_acct.ac_rw);

}


	/* given a number in the form of a comp_t (defined in
	   /usr/include/sys/acct.h), return a long value which
	   is the closest integer. */

long
expand(t)
comp_t t;
{
	long nt;
	unsigned int tt;

	/* use the top three bits as an exponent, base 8 */
	tt = t >> 13; 

	/* take the bottom 13 bits ... */
	nt = t & 017777;

	/* and shift three bits left (into a long, so we have plenty to use)
	   for tt times */
	while (tt != 0) {
		tt--;
		nt <<= 3;
	}
	return(nt);
}

	/* once all the ACCT structs have been read and added up, print
	   out a line of information by user for any uid with non-zero
	   time. */


	/* return the next uid with non-zero accounting stats, or 
	   MAXUSERS if there are no more. */

next_new()
{
	static int i;

	while ((s_array[i].utime == 0) && (i < MAXUSERS))
		i++;
	return(i++);
}

	/* convert an s_struct into a nice format for printing and return a 
	   pointer to the string. */

char *
to_str(s)
struct s_stats *s;
{
	char *to_time(), *p, *malloc();

	p = malloc(LINE_SIZE);
	sprintf(p, "%-4d  %-10s %-10s %-10s %10ld %10ld %10ld",
			s->uid, to_time(s->utime), to_time(s->stime), 
			to_time(s->etime), 
			s->mem, s->io, s->rw);
	return(p);
}


	/* convert a time from CPU ticks (which on this machine are 60/sec,
	   but may vary on others - adjust to suit) to hours, minutes,
	   seconds. return in a string with form HHHH:MM:SS . note that
	   strange things happen if hours > 9999. */

#define SEC  60			/* may be 100 on some machines */
#define MIN  3600		/* so that these two would be 6000 */
#define HOUR 216000		/* and 360000, respectively */

char *
to_time(ticks)
long ticks;
{
	char *malloc(), *p;
	int hour, min, sec;

	ticks -= (hour = (ticks / HOUR)) * HOUR;
	ticks -= (min = (ticks / MIN)) * MIN;
	sec = ticks / SEC;

	p = malloc(12);
	sprintf(p, "%4d:%02d:%02d", hour, min, sec);
	return(p);
}
	

	/* convert from a time in the form HHHH:MM:SS to a number of CPU clock
	   ticks. the inverse of 'to_time()'. */

long
to_ticks(time)
char *time;
{
	int hour, min, sec;

	sscanf(time, "%d:%d:%d", &hour, &min, &sec);
	return(hour * HOUR + min * MIN + sec * SEC);
}


	/* return the uid from the next line in the old accounting file,
	   or MAXUSERS if at EOF. */

next_old()
{
	int uid;

	if (fgets(old_buf, 100, old_fp) == NULL)
		return(MAXUSERS);
	if (sscanf(old_buf, "%d", &uid) < 1)
		return(MAXUSERS);
	return(uid);
}

	/* process the next line from an old accounting file, putting the
	   information from it into the structure passed as a parameter.
	   return 0 if okay, 1 if end-of-file. */

parse_old(s)
struct s_stats *s;
{
	char t1[12], t2[12], t3[12];
	int uid;
	long to_ticks();

	if (sscanf(old_buf, "%d %s %s %s %ld %ld %ld", &(s->uid), t1, t2, t3,
			&(s->mem), &(s->io), &(s->rw)) < 7) 
				return(1);

	s->utime = to_ticks(t1);
	s->stime = to_ticks(t2);
	s->etime = to_ticks(t3);
	return(0);
}

	/* return a pointer to a string containing the formatted sum of a line
	   from the old accounting file and the entry in the new table for the
	   same uid. */

char *
merge_line(uid)
int uid;
{
	char *to_str();
	struct s_stats s;

	/* if a line from the old file is invalid, just put in the new line */
	if (parse_old(&s))
		return(to_str(&s_array[uid]));
	
	s.utime += s_array[uid].utime;
	s.stime += s_array[uid].stime;
	s.etime += s_array[uid].etime;
	s.mem += s_array[uid].mem;
	s.io += s_array[uid].io;
	s.rw += s_array[uid].rw;
	return(to_str(&s));
}

	
	/* print the given message and die, removing any temporary files */

die(s1, s2, s3, s4, s5)
char *s1, *s2, *s3, *s4, *s5;
{
	fprintf(stderr, "sa: %s %s %s %s %s\n", s1, s2, s3, s4, s5);
	unlink(tempfile);
	exit(-1);
}

	/* print the statistics of the new accounting file only onto
	   the standard output. */

stats()
{
	char *to_str();
	int i;

	fprintf(stdout, "%s\n", header);
	for (i = 0; i < MAXUSERS; i++)
		if (s_array[i].utime > 0)
			fprintf(stdout, "%s\n", to_str(&s_array[i]));
}

	/* merge the current accounting file information with that it
	   other one, placing the result in the other. return 0 if
	   everything okay, 1 (and keep all files in original
	   condition) if some problem occurs. */

merge(old)
char *old;
{
	char *mktemp(), *merge_line();
	int i, old_f, new_f;
	struct s_stats s;
	
	tempfile = mktemp("tempXXXXXX");
	if ((temp_fp = fopen(tempfile, "w+")) < 0) {
		fprintf(stderr, "can't open temporary file %s\n", tempfile);
		return(1);
	}
	if ((old_fp = fopen(old, "r")) == NULL) { 
		fprintf(stderr, "can't open old accounting file %s\n", old); 
		return(1);
	}

	/* remove the header from the old file */
	fgets(old_buf, 100, old_fp);
	fprintf(temp_fp, "%s\n", header);

	old_f = next_old();
	new_f = next_new();
	while ((old_f < MAXUSERS) || (new_f < MAXUSERS)) {
		if (old_f == new_f) {
			fprintf(temp_fp, "%s\n", merge_line(old_f));
			old_f = next_old();
			new_f = next_new();
		}
		else if (new_f < old_f) {
			fprintf(temp_fp, "%s\n", to_str(&s_array[new_f]));
			new_f = next_new();
		}
		else {
			fprintf(temp_fp, "%s", old_buf);
			old_f = next_old();
		}
	}


	if (unlink(old)) {	
		fprintf("can't overwrite old file %s\n", old);
		return(1);
	}
	link(tempfile, old);
	unlink(tempfile);
	return(0);
}
	
@//E*O*F sa.c//
chmod u=rw,g=r,o=r sa.c
 
echo x - sa.doc
sed 's/^@//' > "sa.doc" <<'@//E*O*F sa.doc//'
@.TH sa 1
@.SH NAME
@.PP
sa - system accounting program
@.SH SYNOPSIS
@.IP
sa [ -m mergefile ] [ acctfile ]
@.SH DESCRIPTION
@.PP
@.I Sa
takes data from 
@.I acctfile,
a file created by 
@.IR acct (2),
to create a summary of system use on a per-login basis.
The output is a list, in ascending uid order, of uid, 
user, system and elapsed times (in form HH:MM:SS),
memory usage, character and block I/O usage (in bytes), preceded by a
one-line header. Only uids with non-zero user times are listed.
@.PP
If the -m flag is given,
nothing is sent to the standard output.
Instead, the statistics are summed
with those in the file
@.I mergefile,
and the results placed in it.
@.I Mergefile will be created if not present and filled with the
current statistics.
@.PP
If no
@.I acctfile
is given,
@.I /usr/adm/acctfile
is used in default.
@.SH OPERATION
@.PP
An accounting file can be created by a program which calls
@.IR acct (2);
if the shell script
@.I /etc/rc
calls such a program, it will be executed automatically every
time the system is started. However, the effective user ID
of the calling process must be super-user.
@.SH "SEE ALSO"
@.PP
@.IR acct (2), acct(4)
@//E*O*F sa.doc//
chmod u=rw,g=r,o=r sa.doc
 
exit 0