File Coverage

blib/lib/POE/Component/ControlPort.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             # $Id: ControlPort.pm 266 2004-05-10 02:59:17Z sungo $
2             package POE::Component::ControlPort;
3              
4             =pod
5              
6             =head1 NAME
7              
8             POE::Component::ControlPort - network control port for POE applications
9              
10             =head1 SYNOPSIS
11              
12             use POE;
13             use Getopt::Long;
14            
15             use POE::Component::ControlPort;
16             use POE::Component::ControlPort::Command;
17              
18             my @commands = (
19             {
20             help_text => 'My command',
21             usage => 'my_command [ arg1, arg2 ]',
22             topic => 'custom',
23             name => 'my_command',
24             command => sub {
25             my %input = @_;
26            
27             local @ARGV = @{ $input{args} };
28             GetOptions( ... );
29             },
30             }
31             );
32              
33              
34             POE::Component::ControlPort->create(
35             local_address => '127.0.0.1',
36             local_port => '31337',
37            
38             # optional...
39             hostname => 'pie.pants.org',
40             appname => 'my perl app',
41              
42             commands => \@commands,
43              
44             poe_debug => 1,
45             );
46              
47             # other poe sessions or whatever ...
48              
49             POE::Kernel->run();
50            
51              
52             =head1 DESCRIPTION
53              
54             When building network applications, it is often helpful to have a
55             network accessible control and diagnostic interface. This module
56             provides such an interface for POE applications. By default, it provides
57             a fairly limited set of commands but is easily extended to provide
58             whatever command set you require. In addition, if
59             C version 1.018 or above is installed, a set
60             of POE debug commands will be loaded.
61              
62             =head1 GETTING STARTED
63              
64             The utility of a network accessible controlport is limited only by the commands
65             you allow access to. A controlport with only a status command isn't very useful.
66             Defining commands is easy.
67              
68             =head2 DEFINING COMMANDS
69              
70             my @commands = (
71             {
72             help_text => 'My command',
73             usage => 'my_command [ arg1, arg2 ]',
74             topic => 'custom',
75             name => 'my_command',
76             command => sub {
77             my %input = @_;
78            
79             local @ARGV = @{ $input{args} };
80             GetOptions( ... );
81             },
82             }
83             );
84              
85              
86             A command is defined by a hash of metadata and a subroutine reference. The
87             metadata helps to group commands into functional units as well as display help
88             and usage information for the confused user. The meat of a command, obviously,
89             is the subroutine reference which makes up the 'command' part of the metadata.
90              
91             The subroutine reference is executed every time a user issues the command name
92             that is assigned for it. Any text returned from the subroutine will be printed
93             out to the user in the control port. The subroutine's arguments are a hash of
94             data about the command invocation.
95              
96             =over 4
97              
98             =item * args
99              
100             This hash element is a list of arguments the user passed in to the command. It
101             is suggested that you assign this list to C< @ARGV > and use L and
102             friends to parse the arguments.
103              
104             =item * oob
105              
106             This hash element contains a hash of out of band data about the transaction. It
107             is populated with hostname, appname, client_addr, and client_port.
108              
109             =back
110              
111             =head2 LAUNCHING THE PORT
112              
113             POE::Component::ControlPort->create(
114             local_address => '127.0.0.1',
115             local_port => '31337',
116            
117             # optional...
118             hostname => 'pie.pants.org',
119             appname => 'my perl app',
120              
121             commands => \@commands,
122              
123             poe_debug => 1,
124             )
125              
126             The C method in the C namespace is used
127             to create and launch the control port. There are several parameters available
128             to C.
129              
130             =over 4
131              
132             =item * local_address
133              
134             Mandatory. The address on which the control port should listen.
135              
136             =item * local_port
137              
138             Mandatory. The port on which the control port should listen.
139              
140             =item * commands
141              
142             Optional (but boring if not provided). An array of command hashes. See
143             L above.
144              
145             =item * hostname
146              
147             Optional. Mostly for display in the control port itself. Will probably be used
148             for other things in the future.
149              
150             =item * appname
151              
152             Optional. The name of this application, defaulting to C. This is
153             used by TCPwrappers to determine if the connecting user is allowed to connect.
154             This is also used as the master alias for the control port session.
155              
156             =item * poe_debug
157              
158             Optional. Defaults to 0. If true, attempts to load commands from
159             L if said module is available and of the appropriate
160             version.
161              
162             =back
163              
164             =head2 SHUTTING DOWN
165              
166             The control port responds to a C event to the appname given during
167             control port creation. This event will cause the immediate shutdown of all
168             connections and the termination of the listener.
169              
170             =cut
171              
172             # General setup {{{
173 4     4   92863 use warnings;
  4         9  
  4         123  
174 4     4   19 use strict;
  4         7  
  4         118  
175              
176 4     4   5253 use Socket;
  4         18924  
  4         2356  
177 4         29 use POE qw(
178             Wheel::SocketFactory
179             Wheel::ReadWrite
180 4     4   11415 );
  4         200154  
181              
182 4     4   530073 use Sys::Hostname;
  4         11  
  4         218  
183 4     4   23 use File::Basename;
  4         8  
  4         399  
184              
185 4     4   2002 use Authen::Libwrap qw(hosts_ctl STRING_UNKNOWN);
  0            
  0            
186              
187             use Params::Validate qw(validate);
188              
189             use Carp;
190              
191             use POE::Component::ControlPort::DefaultCommands;
192             use POE::Component::ControlPort::Command;
193              
194             sub DEBUG () { 0 }
195              
196             our $VERSION = '1.'.sprintf "%04d", (qw($Rev: 266 $))[1];
197              
198             # }}}
199              
200              
201             sub create { #{{{
202             my $class = shift;
203              
204              
205             # Validate arguments {{{
206             warn "Validating arguments" if DEBUG;
207              
208             my %args = validate( @_, {
209             local_address => 1,
210             local_port => 1,
211             hostname => { optional => 1, default => hostname() },
212             appname => { optional => 1, default => basename($0) },
213             commands => { optional => 1, type => &Params::Validate::ARRAYREF },
214             poe_debug => { optional => 1, default => 1 },
215             } );
216             # }}}
217              
218              
219             # Register default commands #{{{
220             warn "Registering default commands" if DEBUG;
221            
222             foreach my $cmd ( @POE::Component::ControlPort::DefaultCommands::COMMANDS ) {
223             POE::Component::ControlPort::Command->register(%$cmd);
224             }
225             # }}}
226              
227            
228             # if available, register poe debug commands #{{{
229             if($args{poe_debug}) {
230             POE::Component::ControlPort::DefaultCommands->_add_poe_debug_commands();
231             }
232             # }}}
233              
234              
235             # Register user commands, if requested #{{{
236             warn "Registering user commands" if DEBUG;
237              
238             if($args{commands}) {
239             foreach my $cmd (@{ $args{commands} }) {
240             if(ref $cmd eq 'HASH') {
241             POE::Component::ControlPort::Command->register(%$cmd);
242             } else {
243             croak "Parameter 'commands' contains element which is not a hash ref";
244             }
245             }
246             }
247             # }}}
248            
249              
250             # Set the whole ball rolling #{{{
251             warn "Creating session" if DEBUG;
252            
253             my $self = POE::Session->create(
254             inline_states => {
255             _start => \&start,
256             _stop => \&stop,
257              
258             socket_connect => \&socket_connect,
259             socket_error => \&socket_error,
260              
261             client_connect => \&client_connect,
262             client_error => \&client_error,
263             client_input => \&client_input,
264              
265             'shutdown' => \&shutdown,
266              
267             },
268             heap => {
269             address => $args{local_address},
270             port => $args{local_port},
271             hostname => $args{hostname},
272             appname => $args{appname},
273             prompt => $args{appname}." [".$args{hostname}."]: ",
274             }
275             );
276            
277            
278             return $self;
279             # }}}
280            
281             } #}}}
282              
283             =begin devel
284              
285             =head2 start
286              
287             Get things rolling. Starts up a POE::Wheel::SocketFactory using the user
288             provided config info.
289              
290             =cut
291              
292             sub start { #{{{
293              
294             warn "Starting socketfactory" if DEBUG;
295              
296             $_[KERNEL]->alias_set($_[HEAP]->{appname});
297              
298             $_[HEAP]->{wheels}->{socketfactory} =
299             POE::Wheel::SocketFactory->new(
300             BindAddress => $_[HEAP]->{address},
301             BindPort => $_[HEAP]->{port},
302              
303             SuccessEvent => 'socket_connect',
304             FailureEvent => 'socket_error',
305              
306             Reuse => 'on',
307             );
308              
309             } #}}}
310              
311              
312             =head2 stop
313              
314             Mostly just a placeholder.
315              
316             =cut
317              
318             sub stop { #{{{
319             warn "Socketfactory stopping" if DEBUG;
320             } #}}}
321              
322              
323              
324             =head2 shutdown
325              
326             Forcibly shutdown the control port
327              
328             =cut
329              
330             sub shutdown { #{{{
331              
332             delete $_[HEAP]->{wheels}->{socketfactory};
333              
334             foreach my $wid (keys %{ $_[HEAP]->{wheels} }) {
335             my $data = $_[HEAP]->{wheels}->{$wid};
336             my $wheel = $data->{wheel};
337             delete $_[HEAP]->{wheels}->{$wid};
338             $wheel->shutdown_input();
339             $wheel->shutdown_output();
340             }
341             $_[KERNEL]->alias_remove( $_[HEAP]->{appname} );
342             } #}}}
343              
344              
345              
346              
347             =head2 socket_connect
348              
349             Well lookie here. Somebody wants to talk to us. Check their credentials
350             with Authen::Libwrap and if they're valid, set up the rest of the
351             connection with POE::Wheel::ReadWrite. Print out the welcome banner.
352              
353             =cut
354              
355              
356             sub socket_connect { #{{{
357              
358             my $handle = $_[ARG0];
359             my $client_addr = inet_ntoa($_[ARG1]);
360             my $client_port = $_[ARG2];
361              
362             if(hosts_ctl($_[HEAP]->{appname}, $handle)) {
363             warn "Got socket connection from $client_addr : $client_port" if DEBUG;
364            
365             my $wheel = POE::Wheel::ReadWrite->new(
366             Handle => $handle,
367            
368             InputEvent => 'client_input',
369             ErrorEvent => 'client_error',
370             );
371            
372             $_[HEAP]->{wheels}->{ $wheel->ID } = {
373             wheel => $wheel,
374             client_addr => $client_addr,
375             client_port => $client_port,
376             };
377              
378             my $time = localtime(time);
379             $wheel->put("Control port online: $time");
380              
381             $wheel->put($_[HEAP]->{prompt});
382              
383             } else {
384             warn"Control port connection from $client_addr : $client_port disallowed by Authen::LibWrap" if DEBUG;;
385             }
386             } #}}}
387              
388              
389              
390             =head2 socket_error
391              
392             Some problem happened while setting up the listener. carp about it and
393             try again in 2 seconds.
394              
395             =cut
396              
397             sub socket_error { #{{{
398             my ($errop, $errnum, $errstr) = @_[ARG0..ARG2];
399              
400             carp "ERROR: $errop : $errnum - $errstr. Could not create listener. Attempting to restart in 2 seconds";
401              
402             delete $_[HEAP]->{wheels};
403             $_[KERNEL]->delay('_start' => 2);
404              
405             } #}}}
406              
407              
408             =head2 client_error
409              
410             Error from a connected client. Probably just them logging out. Delete
411             their wheel and shut down the connection.
412              
413             =cut
414              
415             sub client_error { #{{{
416             my $wid = $_[ARG3];
417             my ($errop, $errnum, $errstr) = @_[ARG0 .. ARG2];
418            
419             my $data = $_[HEAP]->{wheels}->{$wid};
420            
421             if( ($errop eq 'read') && ($errnum eq '0') ) {
422             warn "Client disconnection by $data->{client_addr} : $data->{client_port}" if DEBUG;
423             } else {
424             warn "Client error from $data->{client_addr} : $data->{client_port}" if DEBUG;
425             }
426            
427             delete $_[HEAP]->{wheels}->{$wid};
428             } #}}}
429              
430              
431             =head2 client_input
432              
433             The user said something to us. If they said something useful,
434             run the command they asked for. Then give them the output from the
435             command.
436              
437             =cut
438              
439             sub client_input { #{{{
440             my $wid = $_[ARG1];
441             my $input = $_[ARG0];
442              
443             my $data = $_[HEAP]->{wheels}->{$wid};
444             my $wheel = $data->{wheel};
445              
446             my @args = split(/\s+/, $input);
447             my $cmd = shift @args;
448             my $txt;
449              
450             if($cmd) {
451             my $oob = { hostname => $_[HEAP]->{hostname},
452             appname => $_[HEAP]->{appname},
453             client_addr => $data->{client_addr},
454             client_port => $data->{client_port},
455             };
456            
457             warn "Got input from $data->{client_addr} : $data->{client_port}" if DEBUG;
458             my $txt = POE::Component::ControlPort::Command->run(
459             command => $cmd,
460             oob_data => $oob,
461             arguments => \@args,
462             );
463              
464             if(defined $txt) {
465             warn "Sending output to $data->{client_addr} : $data->{client_port}" if DEBUG;
466            
467             $wheel->put($txt);
468             }
469             }
470             $wheel->put("Done.");
471            
472             } # }}}
473              
474              
475             1;
476              
477             __END__