#!/usr/bin/perl -w

use strict;
use warnings;
use English;
use Term::ReadLine ();
use Proc::ProcessTable ();
use Config::General ();
use Text::Wrap qw(wrap);
use POSIX ();

use vars qw($VERSION $SELF);

$OUTPUT_AUTOFLUSH = 1;
($SELF = $PROGRAM_NAME) =~ s|^.*/||;
$VERSION = sprintf('%d.%02d', q$Revision: 1.4 $ =~ /(\d+)/g);
$Text::Wrap::columns = 77;

# Create Term::Realine object
my $term = new Term::ReadLine $SELF;


# Start header of config output
my $str = sprintf("# Generated by %s, by %s@%s\n", $SELF,
				(getpwuid($EFFECTIVE_USER_ID))[0], (POSIX::uname())[1]
			);
$str .= sprintf("# Created at %s\n\n",scalar localtime);

$str .= "# Please read through your configuration file before using it in production!\n";
$str .= "Disabled True\n\n";


# Print some information
print <<EOT;
psmon-config will ask you a series of questions to help you quickly generate a
valid configuration file. It is reccomended that you then review (and edit) the
resulting configuration file in a text editor of your choice before allowing it
to be used in a production environment.
EOT


# Get global directives
my $directives = get_directives();
for my $directive (keys(%{$directives})) {
	(my $help = $directives->{$directive}->[1]) =~ s/\s\s+/ /g;
	printf("\n%-13s%s\n",'Directive:',$directive);
	print wrap('Description: ','             ',"$help\n");
	printf("%-13s%s\n",'Defaults to:',$directives->{$directive}->[0]) if $directives->{$directive}->[0];

	my $prompt = "Specify $directive? [".($directives->{$directive}->[0] ? 'y/N' : 'Y/n')."]: ";
	$_ = $term->readline($prompt);
	redo unless /^\s*(y|n)?\s*$/i;

	$str .= sprintf("# %-13s%s\n",'Directive:',$directive);
	$str .= wrap('# Description: ','#              ',"$help\n");
	$str .= sprintf("# %-13s%s\n",'Defaults to:',$directives->{$directive}->[0]) if $directives->{$directive}->[0];

	if ($_ =~ /y/ || (!length($directives->{$directive}->[0]) && $_ !~ /n/)) {
		$str .= sprintf("%s %s\n\n",$directive,get_value('Value?: '));
	} else {
		$str .= "\n";
	}
}


# Print some information about process scope directives
print <<EOT;

psmon-config.pl will now scan your process table for background daemon
processes. You can then select which of these processes you wish to monitor,
in order to ensure they are always running and/or do not exceed specified
resource limits.

EOT


# Scan the process table
print "Scanning your process table ... ";
my $daemons = {};
my $p = new Proc::ProcessTable( 'cache_ttys' => 1 );
for my $process (@{$p->table}) {
	unless ($process->{ttynum} || $process->{ttydev}) {
		my ($executable) = $process->{cmndline} =~ /^\s*(\S+)/;
		if (-f $executable) {
			my $instances = 1;
			if (exists $daemons->{$process->{cmndline}}) {
				$instances += $daemons->{$process->{cmndline}}->{instances};
			}
			$daemons->{$process->{cmndline}} = $process;
			$daemons->{$process->{cmndline}}->{instances} = $instances;
		}
	}
}
print "done\n";


# Get process scope directive information
$directives = get_process_scope_directives();
for my $process (values %{$daemons}) {
	print "\n";
	print "Process name: $process->{fname}\n";
	print "Command line: $process->{cmndline}\n";
	print "Working UID : $process->{uid}\n";
	print "Instances   : $process->{instances}\n";

	$_ = $term->readline("Would you like to monitor '$process->{fname}'? [y/N]: ");
	redo unless /^\s*(y|n)?\s*$/i;
	next unless /y/i;

	$str .= "# Process $process->{fname} added by psmon-config\n";
	$str .= "<Process $process->{fname}>\n";

	for my $directive (keys(%{$directives})) {
		(my $help = $directives->{$directive}->[1]) =~ s/\s\s+/ /g;
		printf("\n%-13s%s\n",'Directive:',$directive);
		print wrap('Description: ','             ',"$help\n");
		printf("%-13s%s\n",'Defaults to:',$directives->{$directive}->[0]) if $directives->{$directive}->[0];

		my $prompt = "Specify $directive? [".($directives->{$directive}->[0] ? 'y/N' : 'Y/n')."]: ";
		$_ = $term->readline($prompt);
		redo unless /^\s*(y|n)?\s*$/i;

		if ($_ =~ /y/ || (!length($directives->{$directive}->[0]) && $_ !~ /n/)) {
			$str .= sprintf("\t%s %s\n",$directive,get_value('Value?: '));
		}
	}

	$str .= "</Process>\n\n";
}


# Add this again for those who just do not pay attention
$str .= "# You need to remove BOTH of these 'Disabled' directives before using this\n";
$str .= "# configuration file. Please make sure you have read and understood everything\n";
$str .= "# in this file before using it in a live production environment!\n";
$str .= "Disabled True\n\n";


# Print the configuration
open(FH,">psmon-config.conf") || die "Unable to open file handle FH for file 'psmon-config.conf': $!";
print FH "$str\n";
close(FH) || warn "Unable to close file handle FH for file 'psmon-config.conf': $!";
print "\n\n";
print "Configuration file written to ./psmon-config.conf\n\n";


# Subroutines
sub get_value {
	my $prompt = shift || 'Value?: ';

	my $retval = '';
	until ($retval) {
		$_ = $term->readline($prompt);
		chomp;
		redo unless /\S+/;
		confirm: {
			print "You entered: $_\n";
			my $confirm = $term->readline('Is this correct? [Y/n]: ');
			redo confirm unless $confirm =~ /^\s*(y|n)?\s*$/i;
			$retval = $_ if $confirm !~ /n/i;
		}
	}

	return $retval;
}

sub read_config {
	my $config_file = shift;

	unless (-f $config_file && -r $config_file) {
		return undef;
	}

	my $conf = new Config::General(
			-ConfigFile				=> $config_file,
			-LowerCaseNames			=> 1,
			-UseApacheInclude		=> 1,
			-IncludeRelative		=> 1,
			-MergeDuplicateBlocks	=> 1,
			-AllowMultiOptions		=> 1,
			-MergeDuplicateOptions	=> 1,
			-AutoTrue				=> 1,
		);

	return $conf->getall;
}

sub get_process_scope_directives {
	my $directives = {
		SpawnCmd		=> [ ('', 'Defines the full command line to be executed in order
							to respawn a dead process.') ],
		KillCmd			=> [ ('*Undefined*', 'Defines the full command line to be executed in order
							to gracefully shutdown or kill a rogue process. If the command
							returns a boolean true exit status then it is assumed that the
							command failed to execute sucessfully. If no KillCmd is specified
							or the command fails, the process will be killed by sending a
							SIGKILL signal with the standard kill() function.') ],
		AdminEmail		=> [ ('*Undefined*', 'Defines the email address where
							notification emails should be sent to. An entry in the process
							 scope which will take priority over a global declaration.') ],
		PIDFile			=> [ ('', 'Defines the full path and filename of a file created by
							a process which contain it\'s main parent process ID.') ],
		TTL				=> [ ('*Undefined*', 'Defines a maximum time to live (in seconds) of a process.
							The process will be killed once it has been running longer than
							this value, and it\'s process ID isn\'t contained in the defined pidfile.') ],
		PctCpu			=> [ ('*Undefined*', 'Defines a maximum allowable percentage of CPU time a
							process may use. The process will be killed once it\'s CPU
							usage exceeds this threshold and it\'s process ID isn\'t
							contained in the defined pidfile.') ],
		PctMem			=> [ ('*Undefined*', 'Defines a maximum allowable percentage of total system
							memory a process may use. The process will be killed once it\'s
							memory usage exceeds this threshold and it\'s process ID isn\'t
							contained in the defined pidfile.') ],
		Instances		=> [ ('*Undefined*', 'Defines a maximum number of instances of a process which
							may run. The process will be killed once there are more than
							this number of occurances running, and it\'s process ID isn\'t
							contained in the defined pid file.') ],
		NoEmailOnKill	=> [ ('False', 'Accepts a boolean value of True or False. Surpresses
							process killing notification emails for this process scope.') ],
		NoEmailOnSpawn	=> [ ('False', 'Accepts a boolean value of True or False. Surpresses
							process spawning notification emails for this process scope.') ],
		NoEmail			=> [ ('False', 'Accepts a boolean value of True or False. Surpresses
							all notification emails for this process scope.') ],
		};
	return $directives;
}

sub get_directives {
	my $directives = {
		Facility		=> [ ('LOG_DAEMON', 'Defines which syslog facility to log to. Refer
							to your syslogd and/or operating system documentation for a
							list of valid facilities.') ],
		LogLevel		=> [ ('LOG_NOTICE', 'Defines the loglevel priority that notifications
							to syslog will be marked as. Refer to your operating system\'s
							kernel.h documentation for a list of valid priorities.') ],
		AdminEmail		=> [ ('root@localhost', 'Defines the email address where
							notification emails should be sent to. May be also be used in a
							Process scope which will take priority over a global declaration.') ],
		NotifyEmailFrom	=> [ (sprintf('%s@%s',(getpwuid($EFFECTIVE_USER_ID))[0],(POSIX::uname())[1]),
							'Defines the email address that notification email should be
							addresses from.') ],
		SMTPHost		=> [ ('localhost', 'Defines the IP address or hostname of the SMTP
							server to used to send email notifications.') ],
		SMTPTimeout		=> [ ('20', 'Defines the timeout in seconds to be used during SMTP
							connections.') ],
		SendmailCMD		=> [ ('/usr/sbin/sendmail -t', 'Defines the sendmail command to
							use to send notification emails if there is a failure with the
							SMTP connection to the host defined by smtphost.') ],
		Frequency		=> [ ('60', 'Defines the frequency (in seconds) of process table queries.') ],
		LastSafePID		=> [ ('100', 'When defined, psmon will never attempt to kill a
							process ID which is numerically less than or equal to the value
							defined by lastsafepid. It should be noted that psmon will never
							attempt to kill itself, or a process ID less than or equal to 1.') ],
		NeverKillPID	=> [ ('1', 'Accepts a space delimited list of PIDs which will never
							be killed.') ],
		NeverKillProcessName	=> [ ('kswapd kupdated mdrecoveryd pageout sched init', 'Accepts a space
							delimited list of process names which will never be killed. ') ],
		ProtectSafePIDsQuietly	=> [ ('Off', 'Accepts a boolean value of On or Off.
							Surpresses all notifications of preserved process IDs when
							used in conjunction with the lastsafepid directive.') ],
		};
	return $directives;
}

