#!/usr/local/bin/perl
# 
# fixproc [-min n] [-max n] [-check | -kill | -restart | -exist | -fix] proc ...
# 
# fixproc exit code:
# 	0	ok
# 	1	check failed
# 	2	cannot restart
# 	3	cannot kill
# 	4	fix failed	if fix is defined as kill or restart, then
# 				cannot kill or cannot restart is return instead
# 	10	fixproc error
# 
#
# Fixes a process named "proc" by performing the specified action.  The
# actions can be check, kill, restart, exist, or fix.  The action is specified
# on the command line or is read from a default database, which describes
# the default action to take for each process.  The database format and
# the meaning of each action are described below.
# 
# database format
# ---------------
# 
# name	foo			required
# cmd	/a/b/name args		required
# min	number			optional, defaults to 1
# max	number			optional, defaults to 1
# 
# check	{null, exist, shell}	optional, defaults to exist if not defined
# [shell command		shell commands needed only if check=shell
#  ...
#  shell command
#  end_shell]			keyword end_shell marks end of shell commands
# fix	{kill, restart, shell}	required
# [shell command			shell commands needed only if fix=shell
#  ...
#  shell command
#  end_shell]			keyword end_shell marks end of shell commands
# 
# Blank lines and lines beginning with "#" are ignored.
# 
#
# Example:
# 
# name	test1
# cmd	nice /home/kong/z/test1 > /dev/null &
# max	2
# fix	shell
# 	xterm&
# 	nice /home/kong/z/test1 > /dev/null &
# 	end_shell
# 
# 
# actions
# -------
# There are 5 possible actions:  kill, restart, fix, exist, check.  Fix is
# defined to be the kill action, the restart action, or a series of shell
# commands.  Check is optionally defined in the database.  If check is not
# defined, it defaults to exist.
# 
# If the action is specified on the cmd line, it is executed regardless of
# check.  The commands executed for each action type is as follow:
# 
#   switch action:
# 	kill:
# 	  kill process, wait 5 seconds, kill -9 if still exist
# 	  if still exist
# 	    return "cannot kill"
# 	  else
# 	    return "ok"
# 
# 	restart:	
# 	  execute kill
# 	  if kill returned "cannot kill"
# 	    return "cannot kill"
# 	  restart by issuing cmd to shell
# 	  if check defined
# 	    execute check
# 	    if check succeeds
# 	      return "ok"
# 	    else
# 	      return "cannot restart"
# 
# 	fix:
# 	  if fix=kill
# 	    execute kill
# 	  else if fix=restart
# 	    execute restart
# 	  else
# 	    execute shell commands
# 	    execute check
# 
# 	check:
# 	  if check defined as null
# 	    return "fixproc error"
# 	  else
# 	    execute check
# 	    if check succeeds
# 	      return (execute exist)
# 	    return "check failed"
# 
# 	exist:
# 	  if proc exists in ps && (min <= num. of processes <= max)
# 	    return "ok"
# 	  else
# 	    return "check failed"
# 
# 
# If the action is not specified on the cmd line, the default action is the
# fix action defined in the database.  Fix is only executed if check fails:
# 
# 	if fix defined
# 	  if check is not defined as null
# 	    execute check
# 	    if check succeeds
# 	      return "ok"
# 	  execute action defined for fix  
# 	else
# 	  return "fixproc error"
# 
# 
# If proc is not specified on the command line, return "fixproc error." 
# Multiple proc's can be defined on the cmd line.   When an error occurs
# when multiple proc's are specified, the first error encountered halts the
# script.
# 
# For check shell scripts, any non-zero exit code means the check has failed.
#
#
# Timothy Kong		3/1995

use File::Temp qw(tempfile);

$database_file = '/usr/local/etc/fixproc.conf';

$debug = 0;			# specify debug level using -dN
				# currently defined: -d1

$no_error = 0;
$check_failed_error = 1;
$cannot_restart_error = 2;
$cannot_kill_error = 3;
$cannot_fix_error = 4;
$fixproc_error = 10;

$min = 1;
$max = 1;
$cmd_line_action = '';
%min = ();
%max = ();
%cmd = ();
%check = ();
%fix = ();
$shell_lines = ();
@proc_list = ();

$shell_header = "#!/bin/sh\n";
$shell_end_marker = 'shell_end_marker';

open(command, "/bin/ps -p $$ |") || die "$0: can't run ps command\n";
if (split(' ', <command>) > 4) {
        $ps_opts = 'ax';
} else {
	$ps_opts = '-e';
}
close command;

&read_args();
&read_database();
# &dump_database();		# debug only

# change the default min. and max. number of processes allowed
if ($min != 1)
  {
    for $name ( keys (%min) )
      {
	$min{$name} = $min;
      }
  }
if ($max != 1)
  {
    for $name ( keys (%max) )
      {
	$max{$name} = $max;
      }
  }
    
# work on one process at a time
for $proc ( @proc_list )
  {
    $error_code = &work_on_proc ($proc);

############# uncomment next line when fully working ############
#    exit $error_code if ($error_code);

    die "error_code = $error_code\n" if ($error_code);
  }


# create an executable shell script file
sub create_sh_script
{
  local ($file) = pop (@_);
  local ($fh) = pop (@_);
  local ($i) = pop (@_);

  printf (STDERR "create_sh_script\n") if ($debug > 0);

  $! = $fixproc_error;
  while ( $shell_lines[$i] ne $shell_end_marker )
    {
      printf ($fh "%s", $shell_lines[$i]);
      $i++;
    }
  close ($fh);
  chmod 0755, $file;
}


sub do_fix
{
  local ($proc) = pop(@_);

  printf (STDERR "do_fix\n") if ($debug > 0);

  if ($fix{$proc} eq '')
    {
      $! = $fixproc_error;
      die "$0: internal error 4\n";
    }
  if ($fix{$proc} eq 'kill')
    {
      return &do_kill ($proc);
    }
  elsif ($fix{$proc} eq 'restart')
    {
      return &do_restart ($proc);
    }
  else
    {
      # it must be "shell", so execute the shell script defined in database
      local ($tmpfh, $tmpfile) = tempfile("fix_XXXXXXXX", DIR => "/tmp");

      &create_sh_script ($fix{$proc}, $tmpfh, $tmpfile);

      	# return code is number divided by 256
      $error_code = (system "$tmpfile") / 256;
      unlink($tmpfile);
      return ($fix_failed_error) if ($error_code != 0);
        # sleep needed here?
      return &do_exist ($proc);
    }
}


sub do_check
{
  local ($proc) = pop(@_);

  printf (STDERR "do_check\n") if ($debug > 0);

  if ($check{$proc} eq '')
    {
      $! = $fixproc_error;
      die "$0: internal error 2\n";
    }

  if ($check{$proc} ne 'exist')
    {
      # if not "exist", then it must be "shell", so execute the shell script
      # defined in database

      local ($tmpfh, $tmpfile) = tempfile("check_XXXXXXXX", DIR => "/tmp");

      &create_sh_script ($fix{$proc}, $tmpfh, $tmpfile);

      	# return code is number divided by 256
      $error_code = (system "$tmpfile") / 256;
      unlink($tmpfile);
      return ($check_failed_error) if ($error_code != 0);

      # check passed, continue
    }
  return &do_exist ($proc);
}


sub do_exist
{
  local ($proc) = pop(@_);

  printf (STDERR "do_exist\n") if ($debug > 0);

  # do ps, check to see if min <= no. of processes <= max
  $! = $fixproc_error;
  open (COMMAND, "/bin/ps -e | /bin/grep $proc | /bin/wc -l |")
    || die "$0: can't run ps-grep-wc command\n";
  $proc_count = <COMMAND>;
  if (($proc_count < $min{$proc}) || ($proc_count > $max{$proc}))
    {
      return $check_failed_error;
    }
  return $no_error;
}


sub do_kill
{
  local ($proc) = pop(@_);
  local ($second_kill_needed);

  printf (STDERR "do_kill\n") if ($debug > 0);

  # first try kill
  $! = $fixproc_error;
  open (COMMAND, "/bin/ps $ps_opts |")
    || die "$0: can't run ps-grep-awk command\n";
  while (<COMMAND>)
    {
     if /$proc/ {
      # match the first field of ps $ps_opts
      $! = $fixproc_error;
      /^\s*(\d+)\s/ || die "$0: can't match ps $ps_opts output\n";
      kill 15, $1;
     }
    }
  close COMMAND;

  # if process still exist, try kill -9
  sleep 2;
  $! = $fixproc_error;
  open (COMMAND, "/bin/ps $ps_opts |")
    || die "$0: can't run ps-grep-awk command\n";
  $second_kill_needed = 0;
  while (<COMMAND>)
    {
     if /$proc/ {
      # match the first field of ps $ps_opts
      $! = $fixproc_error;
      /^\s*(\d+)\s/ || die "$0: can't match ps $ps_opts output\n";
      kill 9, $1;
      $second_kill_needed = 1;
    }
  close COMMAND;
  return ($no_error) if ($second_kill_needed == 0);

  # see if kill -9 worked
  sleep 2;
  $! = $fixproc_error;
  open (COMMAND, "/bin/ps $ps_opts |")
    || die "$0: can't run ps-grep-awk command\n";
  while (<COMMAND>)
    {				# a process still exist, return error
      return $cannot_kill_error if /$proc/;
    }
  close COMMAND;
  return $no_error;		# good, all dead
}


sub do_restart
{
  local ($proc) = pop(@_);
  local ($error_code);

  printf (STDERR "do_restart\n") if ($debug > 0);

  $error_code = &do_kill ($proc);
  return $error_code if ($error_code != $no_error);
  die "$0: internal error 3\n" if ($cmd{$proc} eq '');
  system "$cmd{$proc}";
  # sleep needed here?
  if ($check{$proc} ne 'null')
    {
      return $no_error if (&do_check($proc) == $no_error);
      return $cannot_restart_error;
    }
}


sub work_on_proc
{
  local ($proc) = pop(@_);
  local ($error_code);

  printf (STDERR "work_on_proc\n") if ($debug > 0);

  if ($cmd_line_action eq '')
    {
      # perform action from database

      if ($check{$proc} ne 'null')
	{
	  $error_code = &do_check ($proc);
	  if ($error_code != $check_failed_error)
	    {
	      return $error_code;
	    }
	}
      return &do_fix ($proc);
    }
  else
    {
      # perform action from command line

      $error_code = $no_error;
      if ($cmd_line_action eq 'kill')
	{
	  $error_code = &do_kill ($proc);
	}
      elsif ($cmd_line_action eq 'restart')
	{
	  $error_code = &do_restart ($proc);
	}
      elsif ($cmd_line_action eq 'fix')
	{
	  $error_code = &do_fix ($proc);
	}
      elsif ($cmd_line_action eq 'check')
	{
	  if ( $check{$proc} eq 'null' )
	    {
	      exit $fixproc_error;
	    }
	  $error_code = &do_check ($proc);
	}
      elsif ($cmd_line_action eq 'exist')
	{
	  $error_code = &do_exist ($proc);
	}
      else
	{
	  $! = $fixproc_error;
	  die "$0: internal error 1\n";
	}
    }
}


sub dump_database
{
  local ($name);

  for $name (keys(%cmd))
    {
      printf ("name\t%s\n", $name);
      printf ("cmd\t%s\n", $cmd{$name});
      printf ("min\t%s\n", $min{$name});
      printf ("max\t%s\n", $max{$name});
      if ( $check{$name} =~ /[0-9]+/ )
	{
	  printf ("check\tshell\n");
	  $i = $check{$name};
	  while ( $shell_lines[$i] ne $shell_end_marker )
	    {
	      printf ("%s", $shell_lines[$i]);
	      $i++;
	    }
	}
      else
	{
	  printf ("check\t%s\n", $check{$name});
	}
      if ( $fix{$name} =~ /[0-9]+/ )
	{
	  printf ("fix\tshell\n");
	  $i = $fix{$name};
	  while ( $shell_lines[$i] ne $shell_end_marker )
	    {
	      printf ("%s", $shell_lines[$i]);
	      $i++;
	    }
	}
      else
	{
	  printf ("fix\t%s\n", $fix{$name});
	}
      printf ("\n");
    }
}


sub read_database
{
  local ($in_check_shell_lines) = 0;
  local ($in_fix_shell_lines) = 0;
  local ($name) = '';
  local ($str1);
  local ($str2);

  $! = $fixproc_error;
  open (DB, $database_file) || die 'cannot open database file $database_file\n';
  while (<DB>)
    {
      if ((! /\S/) || (/^[ \t]*#.*$/))
	{
		# ignore blank lines or lines beginning with "#"
	}
      elsif ($in_check_shell_lines)
	{
	  if ( /^\s*end_shell\s*$/ )
	    {
	      $in_check_shell_lines = 0;
	      push (@shell_lines, $shell_end_marker);
	    }
	  else
	    {
	      push (@shell_lines, $_);
	    }
	}
      elsif ($in_fix_shell_lines)
	{
	  if ( /^\s*end_shell\s*$/ )
	    {
	      $in_fix_shell_lines = 0;
	      push (@shell_lines, $shell_end_marker);
	    }
	  else
	    {
	      push (@shell_lines, $_);
	    }
	}
      else
	{
	  if ( ! /^\s*(\S+)\s+(\S.*)\s*$/ )
	    {
	      $! = $fixproc_error;
	      die "$0: syntax error in database\n$_";
	    }
	  $str1 = $1;
	  $str2 = $2;
	  if ($str1 eq 'name')
	    {
	      &finish_db_entry($name);
	      $name = $str2;
	    }
	  elsif ($str1 eq 'cmd')
	    {
	      $! = $fixproc_error;
	      die "$0: cmd specified before name in database\n$_\n"
	        if ($name eq '');
	      die "$0: cmd specified multiple times for $name in database\n"
		if ($cmd{$name} ne '');
	      $cmd{$name} = $str2;
	    }
	  elsif ($str1 eq 'min')
	    {
	      $! = $fixproc_error;
	      die "$0: min specified before name in database\n$_\n"
	        if ($name eq '');
	      die "$0: min specified multiple times in database\n$_\n"
		if ($min{$name} ne '');
	      die "$0: non-numeric min value in database\n$_\n"
		if ( ! ($str2 =~ /[0-9]+/ ));
	      $min{$name} = $str2;
	    }
	  elsif ($str1 eq 'max')
	    {
	      $! = $fixproc_error;
	      die "$0: max specified before name in database\n$_\n"
	        if ($name eq '');
	      die "$0: max specified multiple times in database\n$_\n"
		if ($max{$name} ne '');
	      die "$0: non-numeric max value in database\n$_\n"
		if ( ! ($str2 =~ /[0-9]+/ ));
	      $max{$name} = $str2;
	    }
	  elsif ($str1 eq 'check')
	    {
	      $! = $fixproc_error;
	      die "$0: check specified before name in database\n$_\n"
	        if ($name eq '');
	      die "$0: check specified multiple times in database\n$_\n"
		if ($check{$name} ne '');
	      if ( $str2 eq 'shell' )
		{
		  # if $check{$name} is a number, it is a pointer into
		  # $shell_lines[] where the shell commands are kept
		  $shell_lines[$#shell_lines+1] = $shell_header;
		  $check{$name} = $#shell_lines;
		  $in_check_shell_lines = 1;
		}
	      else
		{
		  $check{$name} = $str2;
		}
	    }
	  elsif ($str1 eq 'fix')
	    {
	      $! = $fixproc_error;
	      die "$0: fix specified before name in database\n$_\n"
	        if ($name eq '');
	      die "$0: fix specified multiple times in database\n$_\n"
		if ($fix{$name} ne '');
	      if ( $str2 eq 'shell' )
		{
		  # if $fix{$name} is a number, it is a pointer into
		  # $shell_lines[] where the shell commands are kept
		  $shell_lines[$#shell_lines+1] = $shell_header;
		  $fix{$name} = $#shell_lines;
		  $in_fix_shell_lines = 1;
		}
	      else
		{
		  $fix{$name} = $str2;
		}
	    }
	}
    }
  &finish_db_entry($name);
}


sub finish_db_entry
{
  local ($name) = pop(@_);

  if ($name ne '')
    {
      $! = $fixproc_error;
      die "$0: fix not defined for $name in database\n"
	if ($fix{$name} eq '');
      die "$0: cmd not defined for $name in database\n"
	if ($cmd{$name} eq '');
      $check{$name} = 'exist' if ($check{$name} eq '');
      $max{$name} = 1 if ($max{$name} eq '');
      $min{$name} = 1 if ($min{$name} eq '');
    }
}


sub read_args
{
  local ($i) = 0;
  local ($arg);
  local ($action_arg_count) = 0;

  while ( $i <= $#ARGV )
    {
      $arg = $ARGV[$i];
      if (($arg eq '-min') || ($arg eq '-max'))
	{
	  if (($i == $#ARGV - 1) || ($ARGV[$i+1] =~ /\D/))  # \D is non-numeric
	    {
	      $! = $fixproc_error;
	      die "$0: numeric arg missing after -min or -max\n";
	    }
	  if ($arg eq '-min')
	    {
	      $min = $ARGV[$i+1];
	    }
	  else
	    {
	      $max = $ARGV[$i+1];
	    }
	  $i += 2;
	}
      elsif ($arg eq '-kill')
	{
	  $cmd_line_action = 'kill';
	  $action_arg_count++;
	  $i++;
	}
      elsif ($arg eq '-check')
	{
	  $cmd_line_action = 'check';
	  $action_arg_count++;
	  $i++;
	}
      elsif ($arg eq '-restart')
	{
	  $cmd_line_action = 'restart';
	  $action_arg_count++;
	  $i++;
	}
      elsif ($arg eq '-exist')
	{
	  $cmd_line_action = 'exist';
	  $action_arg_count++;
	  $i++;
	}
      elsif ($arg eq '-fix')
	{
	  $cmd_line_action = 'fix';
	  $action_arg_count++;
	  $i++;
	}
      elsif ($arg =~ /-d(\d)$/)
	{
	  $debug = $1;
	  $i++;
	}
      elsif ($arg =~ /^-/)
	{
	  $! = $fixproc_error;
	  die "$0: unknown switch $arg\n";
	}
      else
	{
	  push (@proc_list, $arg);
	  $i++;
	}
    }
    $! = $fixproc_error;
    die "$0: no process specified\n" if ($#proc_list == -1);
    die "$0: more than one action specified\n" if ($action_arg_count > 1);
  }

