#!/usr/bin/perl

#
# Scan_PS - A program to look for CPU hogs and looping processes
#
# Created by: Ted Stefanik <ted@evi.com>
#             February, 1991
#
# Copyright 1991 by Ted Stefanik and Expert Views, Inc.  All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted, provided
# that the above copyright notice appear in all copies.
#
# This code is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY.  Ted Stefanik and Expert Views, Inc. disclaim all warranties with
# regard to this software, including all implied warranties of merchantability
# and fitness, and in no event shall be liable for any special, indirect or
# consequential damages or any damages whatsoever resulting from loss of use,
# data or profits arising out of or in connection with the use or performance
# of this software.
#


#
# First, set up our manifest constants
#
$Master = shift(@ARGV);      # First argument is "memories" directory

$AlertUnit = 10 * 60;        # 10 minutes

$Normal  = 0;                # Enumeration for type of process
$SysProc = 1;
$BadGuys = 2;

@Mult = (1, 4, 1);           # AlertUnit Multiplier for each type

%Init = ("sqlexec",  100*60, # Special processes - initial hog report time
         "isql",     50*60,
         "gawk",     100*60,
         "awk",      100*60,
         "perl",     100*60,
         "new2old",  100*60,
         "rtgenerate", 50*60,
         "newton",   50*60,
         "Xmfbpmax", 100*60,
         "Xcfbpmax", 100*60,
         "epoch",    50*60,
         "tee",      50*60);
%Step = ("sqlexec",  200*60, # Special processes - incremental hog report time
         "gawk",     200*60,
         "isql",     50*60,
         "awk",      200*60,
         "perl",     200*60,
         "new2old",  200*60,
         "rtgenerate", 50*60,
         "newton",   25*60,
         "Xmfbpmax", 100*60,
         "Xcfbpmax", 100*60,
         "epoch",    50*60,
         "tee",      50*60);

chop($hostname = `hostname`);
$histfile = "oldhogs" . ".$hostname";


#
# Second, read in the old hogs log
#
chdir "$Master/Hogs" || die "Can't cd to memories: $!\n";
if (open(OH, $histfile))
{
   &ScanPSList(*OH, *OldPS,*OldPIDs,*OldTimes,*OldTypes,*OldNames, *OldIdx);
}
   

#
# Next, read in the current system status
#
open(Ps, '/bin/ps auxww |') || die "scan_ps: can't run ps: $!\n";
&ScanPSList(*Ps, *NewPS,*NewPIDs,*NewTimes,*NewTypes,*NewNames, *NewIdx);


#
# Then, scan the current system status looking for hogs
#
for ($i = 1;  $i <= $#NewPIDs;  $i++)   # Element 0 is banner
{
   $oldTime = 0;

   if (defined($OldIdx{$NewPIDs[$i]}))
   {
      $oldTime = $OldTimes[$OldIdx{$NewPIDs[$i]}];
   }

   $elapse = $NewTimes[$i] - $oldTime;
   $incr = $AlertUnit * $Mult[$NewTypes[$i]];
   if ($oldTime == 0 && defined($Init{$NewNames[$i]}))
   {
      $incr = $Init{$NewNames[$i]};
   }
   if ($oldTime != 0 && defined($Step{$NewNames[$i]}))
   {
      $incr = $Step{$NewNames[$i]};
   }

   if ($elapse >= $incr)
   {
      push(@HogList, $i);
      push(@HogNorm, $i) if ($NewTypes[$i] == $Normal);
      push(@HogSys,  $i) if ($NewTypes[$i] == $SysProc);
      push(@HogBad,  $i) if ($NewTypes[$i] == $BadGuys);
   }
}


#
# Penultimately, print out the hog list
#
if ($#HogList != -1)
{
   print "System load:\n   ", `uptime`;
}
&PrintBad(*HogSys,
          "System processes.", "(We generally shouldn't have to kill these.)");
&PrintBad(*HogBad,
          "Previously known offenders.",
          "(If I had permission, I'd kill them automatically.)");
&PrintBad(*HogNorm,
          "Normal processes.",
"(If a particular program shows up here a lot, add it to the offender list.)");


#
# Lastly, save the current hogs as the "old hog log" for next time
#
open(OH, ">" . $histfile)  || die "scan_ps: can't write to history file: $!\n";
foreach $i (@HogList)
{
   print OH "$NewPS[$i]\n";
}
Old: foreach $i (@OldPIDs)
{
   next Old if (!defined($NewIdx{$i}));
   for $j (@HogList)
   {
      next Old if ($NewPIDs[$j] == $i);
   }
   print OH "$OldPS[$OldIdx{$i}]\n";
}
close(OH);


#
# Read a process status list
#
sub ScanPSList
{
   local(*fh, *lines, *pids, *times, *types, *names, *idx) = @_;
   local($i, $pid, $elapse, $type, $proc, $cmds, @toks, @name);

   for ($i = 0;  <fh>;  $i++)
   {
       chop;
       $lines[$i] = $_;
       &ScanPSEnt(*pid, *elapse, *type, *proc, $_);
       $pids[$i] = $pid;
       $times[$i] = $elapse;
       $types[$i] = $type;
       @toks = split(/[ \t]/o, $proc);
       @name = split(m|/|o, $toks[0]);
       $names[$i] = pop(@name);
       $idx{$pid} = $i;
   }
   close(fh);

   return(undef);
}


#
# Decode a single process status line
#
sub ScanPSEnt
{ 
   local(*pid, *elapse, *type, *proc, $line) = @_;
   local($user, $cpu, $mem, $sz, $rss, $tt, $stat, $time, $min, $sec);

   $user = substr($line,  0,  8);   # Use substr instead of split, because
   $pid  = substr($line,  9,  5);   #   fields can overflow and get stuck
   $cpu  = substr($line, 14,  5);   #   together; then split thinks that
   $mem  = substr($line, 19,  5);   #   two fields are a single field!
   $sz   = substr($line, 24,  5);
   $rss  = substr($line, 29,  5);
   $tt   = substr($line, 35,  2);
   $stat = substr($line, 38,  3);
   $time = substr($line, 41,  7);
   $proc = substr($line, 49);

   ($min, $sec) = split(/:/o, $time);
   $elapse  = $min * 60 + $sec;

   $type = $Normal;

   if ($proc eq "swapper"                                      ||
       $proc eq "pagedaemon"                                   ||
       $proc =~ m|^/etc/|o                                     ||
       $proc =~ m|^/usr/lib/|o                                 ||
       $proc =~ m|^/usr/local/gbin/X[mc]fbpmax|o               ||
       $proc eq "/usr/local/gbin/xdm"                          ||
       $proc eq "/usr/local/gbin/xdsxdm"                       ||
       $proc =~ /^-\w*:\w+(\.\w+)? \(xdm\)$/o                  ||
       $proc =~ /^- \w+\.[0-9]+ (console|tty\w+) \(getty\)$/o  ||
       $proc =~ /^rdump -/o                                    ||
       $proc eq "rwhod"                                        ||
       $proc eq "routed")
   {
      $type = $SysProc;
   }
   elsif ($proc =~ m|^-[a-z/]*sh \(c?sh\)$|o)
   {
      $type = $BadGuys;
   }

   return(undef);
}



#
# Print out a hogs table
#
sub PrintBad
{
   local(*bl, $msg1, $msg2) = @_;

   return(undef) if ($#bl == -1);

   print "\n\n$msg1\n   $msg2\n\n$NewPS[0]\n";

   foreach $i (@bl)
   {
      print "$NewPS[$i]\n";
   }

   return(undef);
}
