On Thu, 01 Mar 2001 23:15:44 -0800, Peter Scott wrote:

>I'm constructing a daemon to respond to user questions about various 
>services, e.g., is sendmail on such-and-such host working, is the web 
>server on another host working, etc; I just came across POE and it looks 
>ideal.
>
>What I'm wondering is, what class should I use for testing these 
>services?  I'm still trying to sort out Drivers from Wheels from Filters, 
>and what I want is the ability to fire off a session that will attempt to 
>connect to port 25 or 80 or whatever, then do a simple transaction in the 
>appropriate protocol.
>
>Because the whole point of the program is that those actions may take a 
>long time, I need to be able to get on with other stuff while I'm waiting 
>for them either to complete or timeout.  Again, an event-driven model looks 
>right.  So for writing something like this, what's the appropriate class to 
>start out with for the part that has to go off to this other 
>machine?  Another example would be, say, pinging port 110 and checking that 
>it answers a POP command.  The POE documentation appears mostly geared 
>towards how to listen for a connection rather than how to open one.


Here's one way to do it.  Mind you, this is just a client thing.  It
doesn't include the daemon bits you'll need to handle users' requests,
but most of it should be self-contained enough to work unmodified in
a server.  I hope you find it useful.

-- Rocco Caputo / [EMAIL PROTECTED] / poe.perl.org / poe.sourceforge.net

#!/usr/bin/perl -w

# This program is Copyright 2001 by Rocco Caputo.  All rights reserved.
# You may use, modify and/or distribute it under the same terms as Perl
# itself.

use strict;

use lib '/home/troc/perl/poe';

use POE qw( Wheel::SocketFactory Wheel::ReadWrite Driver::SysRW Filter::Line );

###############################################################################
# A generic service checker.

package Checker;

use strict;
use Carp qw(croak);

use POE::Kernel;  # Imports $poe_kernel.
use POE::Session; # Imports session parameter offsets, like HEAP.

# "Spawn" a checker.  This creates a POE session to connect to a
# service and check it according to a simple script.  The exact script
# is determined in Checker's subclasses.

sub spawn {
  my $package = shift;

  # Validate parameters.  Subclassed checkers should validate their
  # service-dependent parameters.

  my %args = @_;
  foreach my $param (qw(host port)) {
    croak "$package needs a '$param' parameter" unless exists $args{$param};
  }

  # Create a POE session to actually check the service.  This is the
  # equivalent of spawning a process or thread to do the work.  The
  # POE::Session constructor maps event names to their handlers.  In
  # this case, since we're subclassing Checker, we'll use package
  # states.  POE will invoke these in the usual Perl way,
  # C<Checker::Something->handler_start( ... )>, which means the @ISA
  # inheritance chain is followed if Checker::Something doesn't
  # implement &handler_start.

  POE::Session->create
    ( package_states =>
      [ $package =>
        { _start            => 'handler_start',
          connect_succeeded => 'handler_connect_success',
          connect_failed    => 'handler_connect_failure',
          got_input         => 'handler_input',
          got_timeout       => 'handler_timeout',
          got_error         => 'handler_error',
        },
      ],

     # Pass spawn's arguments to the _start event handler.  See the
     # notes for &handler_start next.
     args => [ %args ],
    );
}

# The _start handler is called immediately when a session is created.
# In this case, it's when a Checker subclass is spawned.  The spawn()
# method passes its paramters to the _start handler through
#
#   args => [ %args ]
#
# In the POE::Session->create() parameters.  See &spawn.
#
# These arguments are used to create a postback.  A postback is a
# plain code reference which, when called, posts a POE event.  It does
# other magic, some of which will be covered later.

sub handler_start {
  my $heap = $_[HEAP];
  my %args = @_[ARG0..$#_];

  # Store the parameters in this session's heap.  Each session has a
  # separate heap, so this Checker has different C<args> than all the
  # others.

  $heap->{args} = \%args;

  # Create a postback.  This is used to post a response back to the
  # parent session.

  $heap->{postback} = $_[SENDER]->postback( status => %args );

  # Start a socket factory.  This will establish a connection (or
  # not).  The important part is that it won't block; the connection
  # will occur (or not) in the background, and either a "success" or a
  # "failure" event will be emitted when the socket's status is known.

  $heap->{connector} =
    POE::Wheel::SocketFactory->new
      ( RemoteAddress => $heap->{args}->{host}, # Connect to this host.
        RemotePort    => $heap->{args}->{port}, # Connect to this port.
        SuccessState  => 'connect_succeeded',   # Emit this event on success.
        FailureState  => 'connect_failed',      # Emit this event on failure.
      );

  # Nothing more to do at the moment.  The session will resume
  # execution when either a "success" or "failure" event is generated.
}

# This handler is called when a socket factory successfully connects
# to a service.  Two things determine the cause/effect.  First,
# POE::Session->create() defines 'handler_connect_success' as the
# package method to handle the 'connect_succeeded' event.  Second, the
# POE::Wheel::SocketFactory created in &handler_start generates
# 'connect_succeeded' when a connection is established.

sub handler_connect_success {
  my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];

  # We no longer need the SocketFactory, so delete it.  This lets Perl
  # destroy that object, and its memory is released.

  delete $heap->{connector};

  # Set the protocol state.  This is an index into a subclass'
  # &expect_thing step.

  $heap->{step} = 0;

  # Create a read/write wheel to drive socket I/O.  It will generate a
  # 'got_input' event when the remote end sends us something, or a
  # 'got_error' event if an I/O error occurs on the socket.

  $heap->{interactor} =
    POE::Wheel::ReadWrite->new
      ( Handle     => $socket,                   # The socket handle to drive.
        Filter     => POE::Filter::Line->new(),  # Read and write as lines.
        Driver     => POE::Driver::SysRW->new(), # Use sysread and syswrite.
        InputState => 'got_input',   # The event to send for each input line.
        ErrorState => 'got_error',   # the event to send when errors occur.
      );

  # And so we don't sit here indefinitely, let's set up a timeout.
  # The delay() method's parameters are the event to generate
  # (got_timeout) and the number of seconds hence to generate it.

  $kernel->delay( got_timeout => 60 );
}

# This handler is called if a connection fails.  We post a message
# back to the master session indicating the failure and delete the
# socket factory.  The session will have nothing to do without its
# socket factory (the I/O hasn't been set up yet because the socket
# wasn't established), so POE will stop this session.

sub handler_connect_failure {
  my ($heap, $operation, $errnum, $errstr) = @_[HEAP, ARG0, ARG1, ARG2];

  # Post a notice back to the parent session.

  $heap->{postback}->( 'fail', 'connect', $operation, $errnum, $errstr );

  # Destroy the socket factory, which implies that the session will
  # also be destroyed.  Note: If a session has something else to do
  # besides wait on a socket factory, it might not go away just
  # because this was deleted.

  delete $heap->{connector};

}

# This handler is called whenever a checker's ReadWrite wheel has
# received an entire line of input.  ARG0 contains the line.  This
# handler passes some preprocessed parameters to another package
# method, &expect_thing, which is overridden in a subclass.  For
# example, the POP3 checker overrides &expect_thing to probe a POP3
# server and verify that it twitches properly.

sub handler_input {
  my ($object, $kernel, $heap, $line) = @_[OBJECT, KERNEL, HEAP, ARG0];

  # Reset the input timeout for another minute hence.

  $kernel->delay( got_timeout => 60 );

  # Pass the pre-digested parameters to a subclass for validation.

  $object->expect_thing
    ( $heap->{args},       # The checker's creation parameters.
      $heap->{interactor}, # A reference to the ReadWrite wheel, for put().
      $heap->{step},       # The script's current step number.
      $line                # The line of input received from the server.
    );
}

# Something has failed to respond as expected.  Take the failure
# details from the caller, and pass them to the checker's controlling
# session.  Destroy the connector and interactor (just to be sure),
# and stop the input timeout.  This destruction and deactivation
# ensures that the session no longer has anything to do.  POE will
# stop it because it's idle.

sub fail {
  my $self = shift;

  # We use this "back door" to get the current checker's heap.  Don't
  # worry, it's all documented public interface.

  my $heap = $poe_kernel->get_active_session()->get_heap();

  # Post the failure to the controlling session.

  $heap->{postback}->( 'fail', 'step', @_ );

  # Release resources, ensuring that POE will stop this session.

  delete $heap->{connector};
  delete $heap->{interactor};
  $poe_kernel->delay( 'got_timeout' );
}

# Something has responded as expected.  Move to the next step if no
# parameters are given.  Otherwise, if parameters are present, pass
# them to the controlling session and flag the checker as "passed".

sub pass {
  my $self = shift;

  # Once again, get the current session's heap without requiring it to
  # be passed in.

  my $heap = $poe_kernel->get_active_session()->get_heap();

  # If parameters are given, pass them to the controlling session, and
  # flag this checker as "passed".  A "passed" checker will not fail
  # if the remote end of its socket closes.

  if (@_) {
    $heap->{postback}->( 'pass', 'step', @_ );
    $heap->{passed} = 1;
  }

  # Move to the next step in the &expect_thing script.

  $heap->{step}++;
}

# This handler is called whenever the ReadWrite wheel (and thus the
# server being checked) has been idle for too long.  It prevents a
# checker from hanging indefinitely while waiting for a server to
# respond.  All it does is fail.

sub handler_timeout {
  my $self = $_[OBJECT];
  $self->fail( 'timed out' );
}

# This handler is called whenever the ReadWrite wheel detects an error
# on his socket handle.  If the checker hasn't been flagged as
# "passed" (see &pass above), then the error will cause the checker to
# fail.

sub handler_error {
  my ($self, $kernel, $heap, $operation, $errnum, $errstr) =
    @_[OBJECT, KERNEL, HEAP, ARG0, ARG1, ARG2];

  # Fail unless we've already passed the final test.

  $self->fail( 'i/o', $operation, $errnum, $errstr ) unless $heap->{passed};
}

###############################################################################
# Subclass the Checker to check POP3 servers.

package Checker::POP3;
@Checker::POP3::ISA = qw( Checker );

use POE::Session;

# This is the expect script.  It's given some information about the current state of 
affairs: the
sub expect_thing {
  my ($self, $args, $remote, $step, $input) = @_;

  # Step 0: Validate pop3d banner, and send the login ID.

  if ($step == 0) {

    # Got +OK; send the USER command, and move to the next step.
    if ($input =~ /^\+OK /) {
      $remote->put( 'USER ' . $args->{user} );
      $self->pass();
    }

    # Got something else.  Fail.
    else {
      $self->fail( 'pop3 bad banner', $input );
    }
    return;
  }

  # Step 1: Wait for login acknowledgement, and send password.

  if ($step == 1) {

    # Got +OK; send the PASS command, and move to the next step.
    if ($input =~ /^\+OK /) {
      $remote->put( 'PASS ' . $args->{pass} );
      $self->pass();
    }

    # Got something else.  Fail.
    else {
      $self->fail( 'pop3 bad user', $input );
    }
    return;
  }

  # Step 2: Wait for password acknowledgement, and quit.

  if ($step == 2) {

    # Got +OK; send a QUIT command, and move to the next step.
    if ($input =~ /^\+OK /) {
      $remote->put( 'QUIT' );
      $self->pass();
    }

    # Got something else.  Fail.
    else {
      $self->fail( 'pop3 bad pass', $input );
    }
    return;
  }

  # Step 3: Wait for quit acknowledgement, and be done.

  if ($step == 3) {

    # Got +OK; pass this test with a message, indicating that it's the
    # last test to pass.  This will flag the checker as "passed" so
    # that it won't fail when the server closes its end of the socket.

    if ($input =~ /^\+OK /) {
      $self->pass( 'ok' );
    }

    # Got something else.  Fail.
    else {
      $self->fail( 'pop3 bad quit', $input );
    }
    return;
  }

  $self->fail( 'pop3 bad step', $step, $input );
}

###############################################################################
# Create a controlling session.  This will spawn one or more Checker
# subclasses, with parameters to guide them as they check their
# services.  The checkers will all run in parallel, passing and/or
# failing asynchronously.
#
# The controlling session will wait for all the checkers to complete,
# and then it will exit.  It exits because it has nothing else to do
# once the checkers have stopped.  Since it's the last session to go,
# it "turns off the lights".  That is, POE itself stops when its last
# session does.
#
# This session might be one that handles commands in a daemon.  It
# might spawn new checkers in response to requests and pass the status
# responses back to the client who requested things be checked.

package main;

POE::Session->create
  ( inline_states =>
    {

     # The controlling session's _start state spawns all the checkers
     # at once.  Each checker may have different parameters, possibly
     # gathered from /etc/passwd, the command line paramters (@ARGV)
     # or other places.

     _start => sub {

       # Spawn a pop3 checker for this user.

       Checker::POP3->spawn
         ( host => 'mail.netrus.net',
           port => '110',
           user => 'nobody',
           pass => 'nothing',
         );

       # Spawn a second one, just for the heck of it.  This
       # demonstrates that they both run in parallel.

       Checker::POP3->spawn
         ( host => 'mail2.netrus.net',
           port => '110',
           user => 'nobody',
           pass => 'nothing',
         );
     },

     # Wait for status messages, and simply log them.

     status => sub {
       my %args = @{$_[ARG0]};
       my ($status, $category, @etc) = @{$_[ARG1]};
       print( "status: ",
              "host($args{host}) login($args{user}) = ",
              "$status/$category = (@etc)\n"
            );
     },
    }
  );

# Finally, we start POE::Kernel's main loop.  This will run until the
# last session stops.  In this program, that last session is the
# controlling one created just above.

$poe_kernel->run();
exit 0;

__END__


Reply via email to