#!/usr/bin/perl -w
# psmax - Find processes left behind, not used by users online right now
# Max Baker <max@warped.org>
# 12/02/04

use Proc::ProcessTable;
use Sys::Utmp;
use POSIX;
use Getopt::Std;
use strict;

use vars qw/%users %procs %ut_type_map %proc_map $uid_min %args
            $DEBUG $All $Kill $Sleep $BadProcs $VERSION
           /;

$VERSION = 0.1;

getopts('ahdks:u:',\%args);

$DEBUG = $args{d} || 0;
$All   = $args{a} || 0;
$Kill  = $args{k} || 0;
$Sleep = defined $args{s} ? $args{s} : 100;
# Processes created by UIDs less than this will not be disturbed.
# Usually 500 or 1000
$uid_min = defined $args{u} ? $args{u} : 500;

die &usage if exists $args{h};

&make_uttype_map;
&parse_utmp;
&getprocs;
&showprocs;

print "Total Number of Procs shown: $BadProcs\n";

exit($BadProcs);

# collect the process statistics
sub getprocs {
    my $p = new Proc::ProcessTable;
    foreach my $proc (@{$p->table}){

        # Make map of processes by PID
        $proc_map{$proc->pid()}=$proc;
        
        # ignore ourselves
        next if ($proc->pid() == $$);
        
        # Sort/Save Procs by UID
        $procs{$proc->uid()}->{$proc->pid()} = $proc;
    }
}

# parse_utmp() - Sorts through the utmp entry to find out
#                who is online.
sub parse_utmp {
    my $u = new Sys::Utmp;
    print "UTMP Entries:\n" if $DEBUG;
    while (my $ut = $u->getutent()){
        my ($user,$id,$line,$pid,$t,$host,$time) = 
            ($ut->ut_user(),$ut->ut_id(),$ut->ut_line(),$ut->ut_pid,
             $ut->ut_type,$ut->ut_host,$ut->ut_time
            );
        my $type = $ut_type_map{$t};
        my $uid  = getpwnam($user) || '';
        my $age  = scalar(time) - $time;

        next unless $uid and $uid >= $uid_min;
        print "($uid) $user $pid $line $host $age $type\n" if $DEBUG;

        # Entries w/ type USER_PROCESS are logged in.
        $users{$uid}++ if $type eq 'USER_PROCESS'; 
    }
    print "\n" if $DEBUG;
    $u->endutent;
}

# make_uttype_map() - Makes reverse lookup of ut_type
sub make_uttype_map {
    foreach my $c (qw/ 
                   ACCOUNTING
                   BOOT_TIME
                   DEAD_PROCESS
                   EMPTY
                   INIT_PROCESS
                   LOGIN_PROCESS
                   NEW_TIME
                   OLD_TIME
                   RUN_LVL
                   USER_PROCESS
                    /
                  ){
        no strict 'refs';
        my $const = &{"Sys::Utmp::$c"};
        $ut_type_map{$const}=$c; 
    }
}


# showprocs() - Sort through resulting processes and display the
#               processes of people who are not logged in.
sub showprocs{
    print &hostname,"\n";
    print scalar localtime, "\n";
    print "Orphaned Process Report\n";
    print '-'x50 , "\n";

    $BadProcs = 0;
    foreach my $u (sort {$a <=> $b} keys %procs){
        my $name = getpwuid($u);
        my $u_procs = $procs{$u};
        my $logged_in = exists $users{$u};
        
        # Check for system users
        next unless $u >= $uid_min;

        # Checked if Logged in.
        next if ($logged_in and !$All);

        print "($u) $name ",
            $logged_in ? '' : '* ',
            scalar(keys %$u_procs)," procs\n";
        my $utime = 0;
        my $size  = 0;
        my $rss   = 0;
        foreach my $p (keys %$u_procs){
            $utime  += $u_procs->{$p}->utime();
            $size   += $u_procs->{$p}->size();
            $rss    += $u_procs->{$p}->rss();
        }
        printf "  Utime : %-.2f sec\n", ($utime/100);
        printf "  Mem   : %-.2f MB %-.2f MB Resident\n", ($size/(1024*1024)),
            ($rss/(1024*1024));
        $- -= 3;

        next unless $u >= $uid_min;
        foreach my $p (sort {$a <=> $b} keys %$u_procs){
            my $proc  = $u_procs->{$p};
            my $ppid  = $proc->ppid();
            my $sess  = $proc->sess();
            my $name  = $proc->fname();
            my $start = scalar localtime($proc->start());
            my $flag  = '';
            $flag .= 'orphan ' unless exists $proc_map{$ppid};
            $flag .= 'no_sess ' unless exists $proc_map{$sess};
            $flag .= 'session'  if $sess == $p;
            my $kill  = '';

            # Kill Process if user not logged in
            if ($Kill and !$logged_in){
                # Try a Hangup Signal first to be polite
                my $killed = $proc->kill(SIGHUP);

                select(undef,undef,undef,$Sleep/1000);

                # If HUP didn't do it, kill it.
                if (!$killed or $proc->kill(0)){
                    $proc->kill(SIGKILL);
                    select(undef,undef,undef,$Sleep/1000);
                    
                    # Check if kill did it.
                    if ($proc->kill(0)){
                        $kill = "SIGKILL failed";
                    } else {
                        $kill = "SIGKILL";
                    }
                } else {
                    $kill = "SIGHUP";
                }
            }

format STDOUT_TOP =
    PID   PPID  SESS  FILE                  START       FLAG     KILLED?
   ------------------------------------------------------------------
.


format STDOUT =
    @#### @#### @#### @<<<<<<<<<<<<<< @<<<<<<<<<<<<<<< @<<<<<<< @<<<<<<<<<<<<<<
    $p,   $ppid,$sess,$name,            $start,    $flag,   $kill
.
            
            write;
            $BadProcs++;

        }
    }
    print "\n * Denotes user is not currently logged in.\n" if $All;
}

sub hostname {
    my $h = $ENV{HOSTNAME} || `/bin/hostname` || 'Host Unknown';
    chomp($h);
    return $h;
}

sub usage {
    return <<"end_usage"
psmax - This utility is used to find and remove processes that
        were left behind by users that are not logged in.
        
        A summary of the CPU and MEM usage for each user is given.
    
        Consider this a Pure-Perl marriage of 'who' 'ps' and 'kill'.

    -d - Debug
    -u - Minimum UID of processes to disturb ($uid_min)
         Set to 0 for all processes
    -a - Show processes from everyone logged in
    -k - Kill processes from people not logged in.
    -s - Time to wait to let a process die ($Sleep ms)

Max Baker <max\@warped.org>  12/02/04
end_usage
}

=head1 NAME

psmax

=head1 AUTHOR

Max Baker <max@warped.org>

=head1 DESCRIPTION

This script is a pure-Perl marriage of ps,who, and kill.  It will look
for processes that were left behind by people who are not currently
logged into a machine.  

A report is given of CPU and Memory usage for each user, and the processes are
optionally killed.  Only user processes (UID X or greater) are touched.   A
SIGHUP is tried before a SIGKILL.

Run with C<-h> for a list of command-line arguments.

=head1 PREREQUISITES

 use Proc::ProcessTable;
 use Sys::Utmp;
 use POSIX;
 use Getopt::Std;
 use strict;

=head1 EXIT Status

The program exits with the number of "bad" processes seen.

=pod OSNAMES

linux posix bsd?

=pod SCRIPT CATEGORIES

Unix/System_administration

=cut