#  ProcessMonitor.pl
#  Example 5.2:
#  ----------------------------------------
#  From "Win32 Perl Scripting: Administrators Handbook" by Dave Roth
#  Published by New Riders Publishing.
#  ISBN # 1-57870-215-1
#
#  This script is a daemon that monitors processes and terminates 
#  a given process that exceeds its alloted running time.
#
print "From the book 'Win32 Perl Scripting: The Administrator's Handbook' by Dave Roth\n\n";


use Time::Local;
use Getopt::Long;
use Win32::Daemon;
use Win32::OLE qw( in );
use Win32::EventLog;
use Win32::EventLog::Message;
                                     
$SUCCESS = 0;

# How much time do we sleep between polling the service state?
$SERVICE_SLEEP_TIME = 1;

# The list of process names and seconds they are allowed to run
# Keys are the full names (must be lowercase) and values are the 
# number of seconds permitted to run.  A zero value indicates the
# process must be terminated as soon as possible.
%PROC_LIST = (
    "telnet.exe"    =>  120,
    "ftp.exe"       =>  0,
);

$SERVICE_ALIAS = "ProcMon";
$SERVICE_NAME = "Process Monitor";

$LOG_FILE_PATH = ( Win32::GetFullPathName( $0 ) =~ /^(.*)?\.[^.]*$/ )[0];
$LOG_FILE_PATH .= ".log";

%Config = (
   machine => ".",
   logfile => $LOG_FILE_PATH,
);

Configure( \%Config, @ARGV );
if( $Config{install} )
{
    InstallService();
    exit;
}
elsif( $Config{remove} )
{
    RemoveService();
    exit;
}
elsif( $Config{help} )
{
    Syntax();
    exit;
}

# This is the WMI moniker that will connect to a machine's 
# CIM (Common Information Model) repository
$CLASS = "winmgmts:{impersonationLevel=impersonate}!"
       . "//$Config{machine}/root/cimv2";


# Register our simple Event Log message table resource DLL with
# the System Event Log's $SERVICE_NAME source
Win32::EventLog::Message::RegisterSource( 'System', $SERVICE_NAME );

# Try to open a log file. We report what the service is doing and
# what process are terminated.
if( open( LOG, ">$Config{logfile}" ) )
{
    # Select the LOG filehandle...
    my $BackupHandle = select( LOG );
    # ...then turn on autoflush (no buffering)...
    $| = 1;
    # ...then restore the previous selected I/O handle
    select( $BackupHandle );
}

# Start the service...
if( ! Win32::Daemon::StartService() )
{
    ReportError( "Could not start the $0 service" );
    exit();
}

$PrevState = SERVICE_STARTING;
Write( "$SERVICE_NAME service is starting...\n" );
while( SERVICE_STOPPED != ( $State = Win32::Daemon::State() ) )
{
    if( SERVICE_START_PENDING == $State )
    {
        ReportInfo( "Starting $SERVICE_NAME service. "
                    . "Monitoring $Config{machine}" );
        # Initialization code:
        # Get the WMI (Microsoft's implementation of WBEM) interface
        if( $WMI = Win32::OLE->GetObject( $CLASS ) )
        {
            Win32::Daemon::State( SERVICE_RUNNING );
            ReportInfo( "$SERVICE_NAME service has started: "
                        . "Monitoring $Config{machine}" );
            $PrevState = SERVICE_RUNNING;
        }
        else
        {
            Win32::Daemon::State( SERVICE_STOPPED );
            ReportError( "$SERVICE_NAME service could not access "
                         . "WMI: Aborting." );
        }
    }
    elsif( SERVICE_PAUSE_PENDING == $State )
    {
        # "Pausing...";
        Win32::Daemon::State( SERVICE_PAUSED );
        ReportWarn( "$SERVICE_NAME service has paused." );
        $PrevState = SERVICE_PAUSED;
        next;
    }
    elsif( SERVICE_CONTINUE_PENDING == $State )
    {
        # "Resuming...";
        Win32::Daemon::State( SERVICE_RUNNING );
        ReportInfo( "$SERVICE_NAME service has resumed." );
        $PrevState = SERVICE_RUNNING;
        next;
    }
    elsif( SERVICE_STOP_PENDING == $State )
    {
        # "Stopping...";
        undef $WMI;
        Win32::Daemon::State( SERVICE_STOPPED );
        ReportWarn( "$SERVICE_NAME service is stopping." );
        $PrevState = SERVICE_STOPPED;
        next;
    }
    elsif( SERVICE_RUNNING == $State )
    {
        # The service is running as normal...
        # Get the collection of Win32_Process objects
        foreach my $Proc ( in( $WMI->InstancesOf( "Win32_Process" ) ) )
        {
            my $ProcName = $Proc->{Name};
            my $lcProcName = lc $ProcName;
            my $ProcCreation = $Proc->{CreationDate};
            
            if( ( defined $PROC_LIST{$lcProcName} ) 
                && ( defined $Proc->{CreationDate} ) )
            {
                my $ProcSeconds = DateInSeconds( $ProcCreation );
                if( $PROC_LIST{$lcProcName} <= ( time() - $ProcSeconds ) )
                {
                    if( $SUCCESS == $Proc->Terminate( 0 ) )
                    {
                        ReportWarn( "Killed $ProcName created on " 
                                    . localtime( $ProcSeconds ) );
                    }
                    else
                    {
                        ReportError( "Failed to Kill $ProcName created on " 
                                     . localtime( $ProcSeconds ) );
                    }
                }
            }
        }
    }
    else
    {
        # Got an unhandled control message. Set the state to
        # whatever the previous state was.
        Write( "Got odd state $State. Setting state to '$PrevState'" );
        Win32::Daemon::State( $PrevState );
    }
    sleep( $SERVICE_SLEEP_TIME );
}

sub DateInSeconds
{                                     
    my( $Date ) = @_;           
    my( @List ) = ($Date =~ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
    my( $Seconds ) = timelocal( $List[5], $List[4], $List[3],
                                $List[2], $List[1] - 1, $List[0] - 1900 );
    return( $Seconds );
}

sub ReportError
{
    my( $Message) = @_;
    return( Report( $Message, 
                    $SERVICE_NAME, 
                    EVENTLOG_ERROR_TYPE ) );
}

sub ReportWarn
{
    my( $Message ) = @_;
    return( Report( $Message, 
                    $SERVICE_NAME, 
                    EVENTLOG_WARNING_TYPE ) );
}

sub ReportInfo
{
    my( $Message) = @_;
    return( Report( $Message, 
                    $SERVICE_NAME, 
                    EVENTLOG_INFORMATION_TYPE ) );
}

sub Report
{
    my( $Message, $LogSource, $Type ) = @_;

    Write( "$Message\n" );
    if( my $EventLog = new Win32::EventLog( $LogSource ) )
    {
        $EventLog->Report(
            {
                Strings => $Message,
                EventID => 0,
                EventType => $Type,
                Category    => undef,
            }
        );
        $EventLog->Close();
    }
}
 
sub InstallService
{
    my $Service = GetService();
    
    if( "." ne $Config{machine} )
    {
       # Remove any slashes
       $Config{machine} =~ s#[\\/]##;
       $Service->{parameters} .= " -m \"$Config{machine}\"" 
    }
    $Service->{parameters} .= " -l \"$Config{logfile}\"";
    if( Win32::Daemon::CreateService( $Service ) )
    {
        print "The $Service->{display} was successfully installed.\n";
    }
    else
    {
        print "Failed to add the $Service->{display} service.\n";
        print "Error: " . GetError() . "\n";
    }
}

sub RemoveService
{
    my $Service = GetService();
    
    if( Win32::Daemon::DeleteService( $Service->{name} ) )
    {
        print "The $Service->{display} was successfully removed.\n";
    }
    else
    {
        print "Failed to remove the $Service->{display} service.\n";
        print "Error: " . GetError() . "\n";
    }
}

sub GetService
{
    my $ScriptPath = join( "", Win32::GetFullPathName( $0 ) );
    my %Hash = (
        name    => $SERVICE_ALIAS,
        display => $SERVICE_NAME,
        path    => $^X,
        user    => $Config{user},
        pwd     => $Config{password},
        description => "Monitors processes.",
        parameters => "\"$ScriptPath\"",
    );
    return( \%Hash );
}

sub GetError
{
    return( Win32::FormatMessage( Win32::Daemon::GetLastError() ) );
}

sub Write
{
    my( $Message ) = @_;
    $Message = "[" . scalar( localtime() ) . "] $Message";
    if( fileno( LOG ) )
    {
        print LOG $Message;
    }
}

sub Configure
{
    my( $Config, @Args ) = @_;
    my $Result;

    Getopt::Long::Configure( "prefix_pattern=(-|\/)" );
    $Result = GetOptions( $Config, 
                            qw(
                                install|i
                                remove|r
                                machine|m=s
                                logfile|l=s
                                user|u|a|account=s
                                password=s
                                help
                            )
                        );
    $Config->{help} = 1 if( ! $Result );
}

sub Syntax
{
    my( $Script ) = ( $0 =~ /([^\\]*?)$/ );
    my $Whitespace = " " x length( $Script );
    print << "EOT";

Syntax:
    $Script -install [-a Account][-p Password][-m Machine][-l Path]
    $Whitespace -remove
    $Whitespace -help
    
        -install...........Installs the service.
            -a.............Specifies what account the service runs under. 
                           Default: Local System
            -p.............Specifies the password the service uses.
            -m.............Specifies what machine to monitor.
                           Default: Local machine
            -l.............Specifies a log file path.
                           Default: $LOG_FILE_PATH
        -remove............Removes the service.
EOT
}
