#  UpTimeMonitor.pl
#  Example 5.3:
#  ----------------------------------------
#  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 machines and triggers an alert
#  if the machine is down.
#
print "From the book 'Win32 Perl Scripting: The Administrator's Handbook' by Dave Roth\n\n";


use Net::Ping;
use Getopt::Long;
use Win32::Daemon;
use Win32::EventLog;
use Win32::EventLog::Message;
                                     
# How long to wait for a ping ($PING_TIMEOUT), how long to wait between
# ping attempts ($PING_INTERVAL) and how many pings to try before 
# concluding that a server is down ($PING_MAX_COUNT)
$PING_TIMEOUT = 5;
$PING_INTERVAL = 10;
$PING_MAX_COUNT = 5;

# How much time do we sleep between polling the service state?
$SERVICE_SLEEP_TIME = 5;

# The list of machines to monitor
@HOSTS = qw(
              127.0.0.1
              192.168.3.22
              www.roth.net
);

$SERVICE_ALIAS = "UpMon";
$SERVICE_NAME = "Server Uptime Monitor";
$LOG_FILE_PATH = ( Win32::GetFullPathName( $0 ) =~ /^(.*)?\.[^.]*$/ )[0];
$LOG_FILE_PATH .= ".log";

%Config = (
   logfile => $LOG_FILE_PATH,
);
Configure( \%Config, @ARGV );
if( $Config{install} )
{
    InstallService();
    exit;
}
elsif( $Config{remove} )
{
    RemoveService();
    exit;
}
elsif( $Config{help} )
{
    Syntax();
    exit;
}

# 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 the log file
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 );
}

# Initialize servers...
foreach my $Host ( @HOSTS )
{
    $Servers->{lc $Host}->{count} = $PING_MAX_COUNT;
}

# 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 )
    {
        # Initialization code
        if( $PingObject = Net::Ping->new( "icmp", $PING_TIMEOUT ) )
        {
            ReportInfo( "Monitoring hosts:\n    " 
                        . join( ", ", @HOSTS ) );
            Win32::Daemon::State( SERVICE_RUNNING );
            ReportInfo( "$SERVICE_NAME service has started." );
            $PrevState = SERVICE_RUNNING;
        }
        else
        {
            Win32::Daemon::State( SERVICE_STOPPED );
            ReportError( "$SERVICE_NAME service could not create "
                         . "a Ping object: 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...";
        $PingObject->close();
        undef $PingObject;
        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...
        if( time() > $NextPingTime )
        {
            PingServers( $PingObject, \%Servers, @HOSTS );
            $NextPingTime = time() + $PING_INTERVAL;
        }
    }
    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 );
    }
    CheckServers( \%Servers );
    sleep( $SERVICE_SLEEP_TIME );
}

sub PingServers
{
    my( $PingObject, $Servers, @Hosts ) = @_;

    return unless( defined $PingObject );

    foreach my $Host ( @HOSTS )
    {
        if( ! $PingObject->ping( $Host ) )
        {
            $Servers->{lc $Host}->{count}--;
        }
        else
        {
            $Servers->{lc $Host}->{count} = $PING_MAX_COUNT;
        }
    }
    return;
}

sub CheckServers
{
    my( $Servers ) = @_;

    foreach $Host ( keys %$Servers )
    {
        if( 0 >= $Servers->{lc $Host}->{count} )
        {
            ReportServerDown( $Host );
            $Servers->{lc $Host}->{count} = $PING_MAX_COUNT;
        }
    }
}

sub ReportServerDown
{
    my( $Host ) = @_;
    my $Error = "Server '$Host' is reported down at " . localtime();

    ReportError( $Error );
    Alert( $Host );
}

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, $Log, $Type ) = @_;

    Write( "$Message\n" );
    if( my $EventLog = new Win32::EventLog( $Log ) )
    {
        $EventLog->Report(
            {
                Strings => $Message,
                EventID => 0,
                EventType => $Type,
                Category    => undef,
            }
        );
        $EventLog->Close();
    }
}
 
sub InstallService
{
    my $Service = GetService();
    
    $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 remote machine's uptime.",
        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 Alert
{
    my( $Host ) = @_;
    # You could add code here to alert administrators via email,
    # pager, network message or some other means.
} 

sub Configure
{
    my( $Config, @Args ) = @_;
    my $Result;

    Getopt::Long::Configure( "prefix_pattern=(-|\/)" );
    $Result = GetOptions( $Config, 
                            qw(
                                install|i
                                remove|r
                                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 [-account Account][-password Password][-l Logfile]
    $Whitespace -remove
    $Whitespace -help
    
        -install...........Installs the service.
            -account.......Specifies what account the service runs under. 
                           Default: Local System
            -password......Specifies the password the service uses.
            -l.............Specifies a log file path.
                           Default: $LOG_FILE_PATH
            
        -remove............Removes the service.
EOT
}use Getopt::Long;
use Win32::Daemon;
use Win32::EventLog;
use Win32::EventLog::Message;
                                     
# How long to wait for a ping ($PING_TIMEOUT), how long to wait between
# ping attempts ($PING_INTERVAL) and how many pings to try before 
# concluding that a server is down ($PING_MAX_COUNT)
$PING_TIMEOUT = 5;
$PING_INTERVAL = 10;
$PING_MAX_COUNT = 5;

# How much time do we sleep between polling the service state?
$SERVICE_SLEEP_TIME = 5;

# The list of machines to monitor
@HOSTS = qw(
              127.0.0.1
              192.168.3.22
              www.roth.net
);

$SERVICE_ALIAS = "UpMon";
$SERVICE_NAME = "Server Uptime Monitor";
$LOG_FILE_PATH = ( Win32::GetFullPathName( $0 ) =~ /^(.*)?\.[^.]*$/ )[0];
$LOG_FILE_PATH .= ".log";

%Config = (
   logfile => $LOG_FILE_PATH,
);
Configure( \%Config, @ARGV );
if( $Config{install} )
{
    InstallService();
    exit;
}
elsif( $Config{remove} )
{
    RemoveService();
    exit;
}
elsif( $Config{help} )
{
    Syntax();
    exit;
}

# 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 the log file
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 );
}

# Initialize servers...
foreach my $Host ( @HOSTS )
{
    $Servers->{lc $Host}->{count} = $PING_MAX_COUNT;
}

# 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 )
    {
        # Initialization code
        if( $PingObject = Net::Ping->new( "icmp", $PING_TIMEOUT ) )
        {
            ReportInfo( "Monitoring hosts:\n    " 
                        . join( ", ", @HOSTS ) );
            Win32::Daemon::State( SERVICE_RUNNING );
            ReportInfo( "$SERVICE_NAME service has started." );
            $PrevState = SERVICE_RUNNING;
        }
        else
        {
            Win32::Daemon::State( SERVICE_STOPPED );
            ReportError( "$SERVICE_NAME service could not create "
                         . "a Ping object: 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...";
        $PingObject->close();
        undef $PingObject;
        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...
        if( time() > $NextPingTime )
        {
            PingServers( $PingObject, \%Servers, @HOSTS );
            $NextPingTime = time() + $PING_INTERVAL;
        }
    }
    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 );
    }
    CheckServers( \%Servers );
    sleep( $SERVICE_SLEEP_TIME );
}

sub PingServers
{
    my( $PingObject, $Servers, @Hosts ) = @_;

    return unless( defined $PingObject );

    foreach my $Host ( @HOSTS )
    {
        if( ! $PingObject->ping( $Host ) )
        {
            $Servers->{lc $Host}->{count}--;
        }
        else
        {
            $Servers->{lc $Host}->{count} = $PING_MAX_COUNT;
        }
    }
    return;
}

sub CheckServers
{
    my( $Servers ) = @_;

    foreach $Host ( keys %$Servers )
    {
        if( 0 >= $Servers->{lc $Host}->{count} )
        {
            ReportServerDown( $Host );
            $Servers->{lc $Host}->{count} = $PING_MAX_COUNT;
        }
    }
}

sub ReportServerDown
{
    my( $Host ) = @_;
    my $Error = "Server '$Host' is reported down at " . localtime();

    ReportError( $Error );
    Alert( $Host );
}

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, $Log, $Type ) = @_;

    Write( "$Message\n" );
    if( my $EventLog = new Win32::EventLog( $Log ) )
    {
        $EventLog->Report(
            {
                Strings => $Message,
                EventID => 0,
                EventType => $Type,
                Category    => undef,
            }
        );
        $EventLog->Close();
    }
}
 
sub InstallService
{
    my $Service = GetService();
    
    $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 remote machine's uptime.",
        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 Alert
{
    my( $Host ) = @_;
    # You could add code here to alert administrators via email,
    # pager, network message or some other means.
} 

sub Configure
{
    my( $Config, @Args ) = @_;
    my $Result;

    Getopt::Long::Configure( "prefix_pattern=(-|\/)" );
    $Result = GetOptions( $Config, 
                            qw(
                                install|i
                                remove|r
                                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 [-account Account][-password Password][-l Logfile]
    $Whitespace -remove
    $Whitespace -help
    
        -install...........Installs the service.
            -account.......Specifies account the service runs under. 
                           Default: Local System
            -password......Specifies the password the service uses.
            -l.............Specifies a log file path.
                           Default: $LOG_FILE_PATH
        -remove............Removes the service.
EOT
}
