File Coverage

blib/lib/POE/Quickie.pm
Criterion Covered Total %
statement 159 185 85.9
branch 64 84 76.1
condition 13 24 54.1
subroutine 28 31 90.3
pod 9 9 100.0
total 273 333 81.9


line stmt bran cond sub pod time code
1             package POE::Quickie;
2             BEGIN {
3 10     10   1660202 $POE::Quickie::AUTHORITY = 'cpan:HINRIK';
4             }
5             BEGIN {
6 10     10   172 $POE::Quickie::VERSION = '0.18';
7             }
8              
9 10     10   86 use strict;
  10         22  
  10         328  
10 10     10   57 use warnings FATAL => 'all';
  10         26  
  10         466  
11 10     10   52 use Carp 'croak';
  10         17  
  10         520  
12 10     10   906 use POE;
  10         53510  
  10         60  
13 10     10   135484 use POE::Filter::Stream;
  10         23790  
  10         292  
14 10     10   12213 use POE::Wheel::Run;
  10         264813  
  10         488  
15              
16             require Exporter;
17 10     10   118 use base 'Exporter';
  10         22  
  10         32509  
18             our @EXPORT = qw(quickie_run quickie quickie_merged quickie_tee quickie_tee_merged);
19             our @EXPORT_OK = @EXPORT;
20             our %EXPORT_TAGS = (ALL => [@EXPORT]);
21              
22             our %OBJECTS;
23              
24             sub new {
25 15     15 1 1108 my ($package, %args) = @_;
26              
27 15         98 my $parent_id = $poe_kernel->get_active_session->ID;
28 15 100       201 if (my $self = $OBJECTS{$parent_id}) {
29 6         19 return $self;
30             }
31              
32 9         30 my $self = bless \%args, $package;
33 9         77 $self->{parent_id} = $parent_id;
34 9         26 $OBJECTS{$parent_id} = $self;
35              
36 9         33 return $self;
37             }
38              
39             sub _create_session {
40 13     13   30 my ($self) = @_;
41              
42 13 50       1475 POE::Session->create(
    50          
    50          
43             object_states => [
44             $self => [qw(
45             _start
46             _stop
47             _exception
48             _create_wheel
49             _child_signal
50             _child_timeout
51             _child_stdin
52             _child_stdout
53             _child_stderr
54             _killall
55             )],
56             ],
57             options => {
58             ($self->{debug} ? (debug => 1) : ()),
59             ($self->{default} ? (default => 1) : ()),
60             ($self->{trace} ? (trace => 1) : ()),
61             },
62             );
63              
64 13         2139 return;
65             }
66              
67             sub _start {
68 13     13   4103 my ($kernel, $session, $self) = @_[KERNEL, SESSION, OBJECT];
69              
70 13         50 my $session_id = $session->ID;
71 13         72 $self->{session_id} = $session_id;
72 13         74 $kernel->sig(DIE => '_exception');
73 13         491 return;
74             }
75              
76             sub _stop {
77 13     13   7639 my $self = $_[OBJECT];
78 13         65 delete $self->{session_id};
79 13         57 return;
80             }
81              
82             sub run {
83 14     14 1 1047 my ($self, %args) = @_;
84 14 100       74 $self = POE::Quickie->new() if ref $self ne 'POE::Quickie';
85              
86 14 50       79 croak 'Program parameter not supplied' if !defined $args{Program};
87              
88 14 50 33     88 if ($args{AltFork} && ref $args{Program}) {
89 0         0 croak 'Program must be a string when AltFork is enabled';
90             }
91              
92 14 50 33     64 if ($args{AltFork} && $^O eq 'Win32') {
93 0         0 croak 'AltFork does not currently work on Win32';
94             }
95              
96 14 100       99 $self->_create_session() if !defined $self->{session_id};
97              
98 14         211 my ($exception, $wheel)
99             = $poe_kernel->call($self->{session_id}, '_create_wheel', \%args);
100              
101             # propagate possible exception from POE::Wheel::Run->new()
102 14 100       1241 croak $exception if $exception;
103              
104 13         148 return $wheel->PID;
105             }
106              
107             sub _create_wheel {
108 14     14   939 my ($kernel, $self, $args) = @_[KERNEL, OBJECT, ARG0];
109              
110 14         50 my %data;
111 14         52 for my $arg (qw(AltFork Timeout Input Program Context ProgramArgs
112             StdoutEvent StderrEvent ExitEvent ResultEvent Tee Merged)) {
113 168 100       390 next if !exists $args->{$arg};
114 43         170 $data{$arg} = delete $args->{$arg};
115             }
116              
117 14 50       62 if ($data{AltFork}) {
118 0         0 my @inc = map { +'-I' => $_ } @INC;
  0         0  
119 0         0 $data{Program} = [$^X, @inc, '-e', $data{Program}];
120             }
121              
122 14         31 my $wheel;
123 14         137 eval {
124 14 100       174 $wheel = POE::Wheel::Run->new(
    50          
125             StdinFilter => POE::Filter::Stream->new(),
126             StdinEvent => '_child_stdin',
127             StdoutEvent => '_child_stdout',
128             StderrEvent => '_child_stderr',
129             Program => $data{Program},
130             (defined $data{ProgramArgs}
131             ? (ProgramArgs => $data{ProgramArgs})
132             : ()
133             ),
134             ($^O ne 'Win32'
135             ? (CloseOnCall => 1)
136             : ()
137             ),
138             %$args,
139             );
140             };
141              
142 14 100       95986 if ($@) {
143 1         4 chomp $@;
144 1         8 return $@;
145             }
146              
147 13         285 $data{obj} = $wheel;
148 13         393 $data{extra_args} = $args;
149 13         466 $self->{wheels}{$wheel->ID} = \%data;
150              
151 13 100       185 if (defined $data{Input}) {
152 1         17 $wheel->put($data{Input});
153             }
154             else {
155 12         377 $wheel->shutdown_stdin();
156             }
157              
158 13 100       3906 if ($data{Timeout}) {
159 1         6 $data{alrm} =
160             $kernel->delay_set('_child_timeout', $data{Timeout}, $wheel->ID);
161             }
162              
163 13         806 $kernel->sig_child($wheel->PID, '_child_signal');
164              
165 13         15216 return (undef, $wheel);
166             }
167              
168             sub _exception {
169 0     0   0 my ($kernel, $self, $ex) = @_[KERNEL, OBJECT, ARG1];
170 0         0 chomp $ex->{error_str};
171 0         0 warn __PACKAGE__.": Event $ex->{event} in session "
172             .$ex->{dest_session}->ID." raised exception:\n $ex->{error_str}\n";
173 0         0 $kernel->sig_handled();
174 0         0 return;
175             }
176              
177             sub _child_signal {
178 13     13   34936 my ($kernel, $self, $pid, $status) = @_[KERNEL, OBJECT, ARG1, ARG2];
179 13         102 my $id = $self->_pid_to_id($pid);
180              
181 13         1266 my $data = $self->{wheels}{$id};
182              
183 13         46 my $s = $status >> 8;
184 13 50 66     158 if ($s != 0 && !exists $data->{ExitEvent}
      33        
185             && !exists $data->{ResultEvent}) {
186 0         0 warn "Child $pid exited with nonzero status $s\n";
187             }
188              
189 13 100       85 $kernel->alarm_remove($data->{alrm}) if $data->{Timeout};
190 13 100       93 if ($data->{lazy}) {
191 8         114 $self->{lazy}{$id} = {
192             merged => $data->{merged},
193             stdout => $data->{stdout},
194             stderr => $data->{stderr},
195             status => $status,
196             }
197             }
198 13         45 delete $self->{wheels}{$id};
199              
200 13 100       65 if (defined $data->{ExitEvent}) {
201 2 50       23 $kernel->call(
202             $self->{parent_id},
203             $data->{ExitEvent},
204             $status,
205             $pid,
206             (defined $data->{Context}
207             ? $data->{Context}
208             : ()),
209             );
210             }
211              
212 13 100       2882 if (defined $data->{ResultEvent}) {
213 2 100       31 $kernel->call(
214             $self->{parent_id},
215             $data->{ResultEvent},
216             $pid,
217             $data->{stdout},
218             $data->{stderr},
219             $data->{merged},
220             $status,
221             (defined $data->{Context}
222             ? $data->{Context}
223             : ()),
224             );
225             }
226              
227 13         17985 return;
228             }
229              
230             sub _child_timeout {
231 1     1   3000658 my ($self, $id) = @_[OBJECT, ARG0];
232 1         19 $self->{wheels}{$id}{obj}->kill();
233 1         48 return;
234             }
235              
236             sub _child_stdin {
237 1     1   772 my ($self, $id) = @_[OBJECT, ARG0];
238 1         19 $self->{wheels}{$id}{obj}->shutdown_stdin();
239 1         185 return;
240             }
241              
242             sub _child_stdout {
243 10     10   18529973 my ($kernel, $self, $output, $id) = @_[KERNEL, OBJECT, ARG0, ARG1];
244              
245 10         38 my $data = $self->{wheels}{$id};
246              
247 10 100 100     228 if ($data->{lazy} || defined $data->{ResultEvent}) {
    50          
    50          
248 8         38 push @{ $data->{merged} }, $output;
  8         58  
249 8         16 push @{ $data->{stdout} }, $output;
  8         46  
250              
251 8 100       105 if ($data->{lazy}{Tee}) {
252 2         60 print $output, "\n";
253             }
254             }
255             elsif (!exists $data->{StdoutEvent}) {
256 0         0 print "$output\n";
257             }
258             elsif (defined (my $event = $data->{StdoutEvent})) {
259 2         19 my $context = $data->{Context};
260 2 50       23 $kernel->call(
261             $self->{parent_id},
262             $event,
263             $output,
264             $data->{obj}->PID,
265             (defined $context ? $context : ()),
266             );
267             }
268              
269 10         8011 return;
270             }
271              
272             sub _child_stderr {
273 6     6   15720613 my ($kernel, $self, $error, $id) = @_[KERNEL, OBJECT, ARG0, ARG1];
274              
275 6         35 my $data = $self->{wheels}{$id};
276              
277 6 100 100     158 if ($data->{lazy} || defined $data->{ResultEvent}) {
    50          
    100          
278 4         22 push @{ $data->{merged} }, $error;
  4         41  
279 4         11 push @{ $data->{stderr} }, $error;
  4         42  
280              
281 4 100       39 if ($data->{lazy}{Tee}) {
282 2 100       206 $data->{lazy}{Merged}
283             ? print $error, "\n"
284             : warn $error, "\n";
285             }
286             }
287             elsif (!exists $data->{StderrEvent}) {
288 0         0 warn "$error\n";
289             }
290             elsif (defined (my $event = $data->{StderrEvent})) {
291 1         14 my $context = $data->{Context};
292 1 50       7 $kernel->call(
293             $self->{parent_id},
294             $event,
295             $error,
296             $data->{obj}->PID,
297             (defined $context ? $context : ()),
298             );
299             }
300              
301 6         2180 return;
302             }
303              
304             sub _pid_to_id {
305 19     19   49 my ($self, $pid) = @_;
306              
307 19         49 for my $id (keys %{ $self->{wheels} }) {
  19         456  
308 19 50       152 return $id if $self->{wheels}{$id}{obj}->PID == $pid;
309             }
310              
311 0         0 return;
312             }
313              
314             sub killall {
315 0     0 1 0 my $self = shift;
316 0 0       0 $self = POE::Quickie->new() if ref $self ne 'POE::Quickie';
317 0         0 $poe_kernel->call($self->{session_id}, '_killall', @_);
318 0         0 return;
319             }
320              
321             sub _killall {
322 0     0   0 my ($kernel, $self, $signal) = @_[KERNEL, OBJECT, ARG0];
323              
324 0         0 $kernel->alarm_remove_all();
325              
326 0         0 for my $id (keys %{ $self->{wheels}}) {
  0         0  
327 0         0 $self->{wheels}{$id}{obj}->kill($signal);
328             }
329              
330 0         0 return;
331             }
332              
333             sub processes {
334 1     1 1 2102 my ($self) = @_;
335 1 50       10 $self = POE::Quickie->new() if ref $self ne 'POE::Quickie';
336              
337 1         3 my %wheels;
338 1         2 for my $id (keys %{ $self->{wheels} }) {
  1         11  
339 1         10 my $pid = $self->{wheels}{$id}{obj}->PID;
340 1         8 $wheels{$pid} = $self->{wheels}{$id}{Context};
341             }
342              
343 1         5 return \%wheels;
344             }
345              
346             sub _lazy_run {
347 6     6   24 my ($self, %args) = @_;
348              
349 6         12 my %run_args;
350 6 50 33     11 if (@{ $args{RunArgs} } == 1 &&
  6   33     135  
351             (!ref $args{RunArgs}[0] || ref ($args{RunArgs}[0]) =~ /^(?:ARRAY|CODE)$/)) {
352 6         22 $run_args{Program} = $args{RunArgs}[0];
353             }
354             else {
355 0         0 %run_args = @{ $args{RunArgs} };
  0         0  
356             }
357              
358 6 100       62 my $pid = $self->run(
    100          
359             %run_args,
360             ExitEvent => undef,
361             ($args{Tee} ? () : (StderrEvent => undef)),
362             ($args{Tee} ? () : (StdoutEvent => undef)),
363             );
364              
365 6         200 my $id = $self->_pid_to_id($pid);
366 6         217 $self->{wheels}{$id}{lazy} = {
367             Tee => $args{Tee},
368             Merged => $args{Merged},
369             };
370              
371 6         201 my $parent_id = $poe_kernel->get_active_session->ID;
372 6         86 $poe_kernel->refcount_increment($parent_id, __PACKAGE__);
373 6         764 $poe_kernel->run_one_timeslice() while $self->{wheels}{$id};
374 6         1567 $poe_kernel->refcount_decrement($parent_id, __PACKAGE__);
375              
376 6         259 my $data = delete $self->{lazy}{$id};
377 6 100       72 return $data->{merged}, $data->{status} if $args{Merged};
378 4         92 return $data->{stdout}, $data->{stderr}, $data->{status};
379             }
380              
381             sub quickie_run {
382 2     2 1 634 my %args = @_;
383 2         17 my $self = POE::Quickie->new();
384 2         14 return $self->run(%args);
385             }
386              
387             sub quickie {
388 3     3 1 2400 my @args = @_;
389 3         26 my $self = POE::Quickie->new();
390              
391 3         16 return $self->_lazy_run(
392             RunArgs => \@args
393             );
394             }
395              
396             sub quickie_tee {
397 1     1 1 16004 my @args = @_;
398 1         16 my $self = POE::Quickie->new();
399 1         6 return $self->_lazy_run(
400             RunArgs => \@args,
401             Tee => 1,
402             );
403             }
404              
405             sub quickie_merged {
406 1     1 1 5358 my @args = @_;
407 1         16 my $self = POE::Quickie->new();
408              
409 1         8 return $self->_lazy_run(
410             RunArgs => \@args,
411             Merged => 1,
412             );
413             }
414              
415             sub quickie_tee_merged {
416 1     1 1 4554 my @args = @_;
417 1         18 my $self = POE::Quickie->new();
418              
419 1         7 return $self->_lazy_run(
420             RunArgs => \@args,
421             Tee => 1,
422             Merged => 1,
423             );
424             }
425              
426             1;
427              
428             =encoding utf8
429              
430             =head1 NAME
431              
432             POE::Quickie - A lazy way to wrap blocking code and programs
433              
434             =head1 SYNOPSIS
435              
436             use POE::Quickie;
437              
438             sub event_handler {
439             # the "I'll wait until it's finished" approach, which will block your
440             # session until the command has finished
441             my ($stdout, $stderr, $exit_status) = quickie('foo.pl');
442             print $stdout;
443              
444             # the "keep me posted" approach, which will give you each line of output
445             # as it is appears
446             quickie_run(
447             Program => ['foo.pl', 'bar'],
448             StdoutEvent => 'stdout',
449             Context => 'remember this',
450             );
451              
452             # the "get back to me when it's done" approach, which will notify you
453             # of the entire output when the command has finished
454             quickie_run(
455             Program => ['foo.pl', 'bar'],
456             Context => 'remember this',
457             ResultEvent => 'result',
458             );
459             }
460              
461             sub stdout {
462             my ($output, $context) = @_[ARG0, ARG1];
463             print "got output: '$output' in the context of '$context'\n";
464             }
465              
466             sub result {
467             my ($pid, $stdout, $stderr, $merged, $status, $context) = @_[ARG0..$#_];
468             print "got all this output in the context of '$context':\n";
469             print "$_\n" for @$stdout;
470             }
471              
472             =head1 DESCRIPTION
473              
474             If you need nonblocking access to an external program, or want to execute
475             some blocking code in a separate process, but you don't want to write a
476             wrapper module or some L boilerplate code,
477             then POE::Quickie can help. You just specify what you're interested in
478             (stdout, stderr, and/or exit code), and POE::Quickie will handle the rest in
479             a sensible way.
480              
481             It has some convenience features, such as killing processes after a timeout,
482             and storing process-specific context information which will be delivered with
483             every event.
484              
485             There is also an even lazier API which suspends the execution of your event
486             handler and gives control back to POE while your task is running, the same
487             way L does. This is provided by the
488             L|/FUNCTIONS> functions which are exported by default.
489              
490             =head1 METHODS
491              
492             =head2 C
493              
494             Constructs a POE::Quickie object. You only need to do this if you want to
495             specify any of the parameters below, since a POE::Quickie object will be
496             constructed automatically when it is needed. The rest of the methods can
497             be called on the object (C<< $object->run() >>) or as class methods
498             (C<< POE::Quickie->run() >>). You can safely let the object go out of scope;
499             POE::Quickie will continue to run your processes until they finish.
500              
501             Takes 3 optional parameters: B<'debug'>, B<'default'>, and B<'trace'>. These
502             will be passed to the object's L constructor. See
503             its documentation for details.
504              
505             =head2 C
506              
507             This method spawns a new child process. It returns its process id.
508              
509             You can either call it with a single argument (string, arrayref, or coderef),
510             which will used as the B<'Program'> argument, or you can supply the following
511             key-value pairs:
512              
513             B<'Program'> (required), will be passed to directly to
514             L's constructor.
515              
516             B<'ProgramArgs'> (optional), will be passed directly to
517             L's constructor.
518              
519             B<'Input'> (optional), a string containing the input to the process. This
520             string, if provided, will be sent immediately to the child, and its stdin
521             will then be shut down. B no processing will be done on the data
522             before it is sent. For instance, if you are executing a program which expects
523             line-based input, be sure to end your input with a newline.
524              
525             B<'StdoutEvent'> (optional), the event for delivering lines from the
526             process' STDOUT. If you don't supply this, they will be printed to the main
527             process's STDOUT. To explicitly ignore them, set this to C.
528              
529             B<'StderrEvent'> (optional), the event for delivering lines from the
530             process' STDERR. If you don't supply this, they will be printed to the main
531             process' STDERR. To explicitly ignore them, set this to C.
532              
533             B<'ExitEvent'> (optional), the event to be called when the process has exited.
534             If you don't supply this, a warning indicating the exit code will be printed
535             if it is nonzero. To explicitly ignore it, set this to C.
536              
537             B<'ResultEvent'> (optional), like 'ExitEvent', but it will also contain all
538             the stdout/stderr generated by the child process.
539              
540             B<'Context'> (optional), a variable which will be sent back to you with every
541             event. If you pass a reference, that same reference will be delivered back
542             to you later (not a copy), so you can update it as you see fit.
543              
544             B<'Timeout'> (optional), a timeout in seconds after which the process will
545             be forcibly L if it is still running. There is
546             no timeout by default.
547              
548             B<'AltFork'> (optional), if true, a new instance of the active Perl
549             interpreter (L|perlvar>) will be launched with B<'Program'> (which
550             must be a string) as the code argument (L|perlrun>), and the current
551             L|perlvar> passed as include arguments (L|perlrun>). Default
552             is false.
553              
554             All other arguments will be passed to POE::Wheel::Run's
555             L|POE::Wheel::Run/new> method. Useful if you want to specify the
556             input/output filters and such.
557              
558             =head2 C
559              
560             This L all processes which POE::Quickie is
561             managing for your session. Takes one optional argument, a signal name (e.g.
562             B<'SIGTERM'>).
563              
564             =head2 C
565              
566             Returns a hash reference of all the currently running processes. The key
567             is the process id, and the value is the context variable, if any.
568              
569             =head1 OUTPUT
570              
571             The following events might get sent to your session. The names correspond
572             to the options to L|/run>.
573              
574             =head2 StdoutEvent
575              
576             =over 4
577              
578             =item C: the chunk of STDOUT generated by the process
579              
580             =item C: the process id of the child process
581              
582             =item C: the context variable, if any
583              
584             =back
585              
586             =head2 StderrEvent
587              
588             =over 4
589              
590             =item C: the chunk of STDERR generated by the process
591              
592             =item C: the process id of the child process
593              
594             =item C: the context variable, if any
595              
596             =back
597              
598             =head2 ExitEvent
599              
600             =over 4
601              
602             =item C: the exit code (L|perlvar>) of the child process
603              
604             =item C: the process id of the child process
605              
606             =item C: the context variable, if any
607              
608             =head2 ResultEvent
609              
610             =over 4
611              
612             =item C: the process id of the child process
613              
614             =item C: an array of every chunk of stdout generated by the child process
615              
616             =item C: an array of every chunk of stderr generated by the child process
617              
618             =item C: an array of the interleaved stdout and stderr chunks
619              
620             =item C: the exit code (L|perlvar>) of the child process
621              
622             =item C: the context variable, if any
623              
624             =back
625              
626             =back
627              
628             =head1 FUNCTIONS
629              
630             =head2 C
631              
632             Equivalent to C<< POE::Quickie->run() >>, provided as a convenience.
633              
634             =head2 Blocking functions
635              
636             The following functions are modeled after the ones provided by
637             L. They will not return until the executed
638             process has exited. However,
639             L|POE::Kernel/run_one_timeslice> in POE::Kernel will be
640             called in the meantime, so the rest of your application will continue to run.
641              
642             They all take the same arguments as the L|/run> method, except for the
643             B<'*Event'> and B<'Context'> arguments.
644              
645             B Since these functions block, you must be careful not to call them in
646             event handlers which were executed with C<< $poe_kernel->call() >> by other
647             sessions, so you don't hold them up. A simple way to avoid that is to
648             C or C a new event to your session and do it from there.
649              
650             =head3 C
651              
652             Returns 3 values: the stdout, stderr, and exit code (L|perlvar>) of the
653             child process.
654              
655             =head3 C
656              
657             Returns 3 values: an array of all stdout chunks, an array of all stderr
658             chunks, and the exit code (L|perlvar>) of the child process. In addition,
659             it will echo the stdout/stderr to your process' stdout/stderr.
660              
661             =head3 C
662              
663             Returns 2 values: an array of interleaved stdout and stderr chunks, and the
664             exit code (L|perlvar>) of the child process. Beware that stdout and
665             stderr in the merged result are not guaranteed to be properly ordered due to
666             buffering.
667              
668             =head3 C
669              
670             Returns 2 values: an array of interleaved stdout and stderr chunks, and the
671             exit code (L|perlvar>) of the child process. In addition, it will echo
672             the merged stdout & stderr to your process' stdout. Beware that stdout and
673             stderr in the merged result are not guaranteed to be properly ordered due to
674             buffering.
675              
676             =head1 AUTHOR
677              
678             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
679              
680             =head1 LICENSE AND COPYRIGHT
681              
682             Copyright 2010-2011 Hinrik Ern SigurEsson
683              
684             This program is free software, you can redistribute it and/or modify
685             it under the same terms as Perl itself.
686              
687             =cut