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__