#!/usr/bin/perl -w
#
# proccheck.pl by Tikiman
#
# this script monitors the process table for runaway processes
#  and autokills those that consume too many resources.  This generally
#  occurs when a process that normally blocks gets stuck in a run
#  state (happens with bnc) or someone is running something
#  they shouldn't be (password cracked etc)  
#
# this was written for a freebsd box, I believe that the ps
#  syntax/output is compatible with linux, but probably not
#  for solaris, etc.

$maxcpu = "30";	    # max allowed cpu usage percent - may be decimal
$maxmem = "30";     # max allowed mem usage percent - may be decimal
		    # these numbers represent the maximum percentage
		    #  of resources a process my use before it
		    #  it may get killed. 

$cumcputime = "2"; # minimum number of CPU minutes the process has
		   #  to accumulate before it gets killed - this allows
		   #  relatively brief but CPU intensive processes
		   #  to continue (such as compiling)

$interval = 60; # time to sleep between process checks

$log = "killed.log"; # logfile (absolute or relative path) - program
	             #  output gets dumped here

$killproc = 1; # set to true if you want the processes killed, set
	       #  to 0 of you want the process just logged

$contact = "admin\@risingnet.net"; # contact given in the note to
				   #  process owner

@friends = qw(root bin ftp);  # usernames to ignore when checking
			      #  processes - these should be usernames
			      #  whose processes you don't want to 
			      #  bother

##### End config section #####

$kid = fork(); # fork to bg
if ($kid) {
	print "Process $kid sent to background\n";
	exit;
}

$ok{$_}++ foreach (@friends); # load friends into a hash

while() {
	checkProc(); # check processes
	sleep($interval); # sleep 
}

sub checkProc {
	open(PROC,"ps au|") or die "Open: $!\n"; # obtain processes
	<PROC>; # remove header line
	my @info;
	while(<PROC>) {
		my @tmp = split;
		($user,$pid,$cpu,$mem) = @tmp;
		next if $ok{$user};	
		push(@info, [ \@tmp, $pid, $cpu, $mem ]); # temp array
	}

	my @bycpu = reverse sort { $a->[2] <=> $b->[2] } @info; # sort 
	my @bymem = reverse sort { $a->[3] <=> $b->[3] } @info;

	while($tmp = shift(@bycpu)) { # shave off til we're out of lines
		$tmp->[2] > $maxcpu ? $bad{$tmp->[1]} = $tmp->[0] : last;
	}

	while($tmp = shift(@mem)) {
		$tmp->[3] > $maxmem ? $bad{$tmp->[1]} = $tmp->[0] : last;
	}

	open(LOG,">>$log") or die "Can't open log file: $!";
	select(LOG);
	while(($pid,$user) = each %bad) {
		($user,$pid,$mem,$cpu,undef,undef,undef,undef,undef,$time,@cpu)
			= @$user;

		$string = "Time - ". `date`. "Username - $user\nPID - $pid\n" .
		    	"Mem% - $mem CPU% - $cpu\n" .
			"Cumulative CPU time - $time\n" .
			"Command: ". join(' ',@cpu); # output

		print "$string\n\n";
	
		($min,$sec1,undef) = ($time =~ /(\d+):(\d+)\.(\d+)/);

		$tottime = $min + ($sec1 / 60);

		if(($killproc) and ($tottime >= $cumcputime)) {
			kill(9,$pid) and mailUser($user,$string); 
			# kill errant process
		}
	}	
	close(LOG);
	close(PROC);
}

sub mailUser {
	my($user,$string) = @_;
	$string = "Dear User:\n" . # changing this shouldn't be too hard
		  "The following process owned by you has " . 
		  "been killed for excess Memory and/or CPU usage:\n\n" .
		  "$string\n\n" . 
		  "Please email $contact if you " .
		  "have any questions.\n\n--Staff\n";

	$filename = "/tmp/mem.$$.tmp";
	open(TMP,">$filename");
	print TMP $string;
	close(TMP);
	# this creates a temporary file to mail out - there is probably
	# some kind of security hole in this?

	system("cat $filename | mail $user -s Process killed");

	unlink($filename);
}