File Coverage

blib/lib/POE/Component/CPANPLUS/YACSmoke.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package POE::Component::CPANPLUS::YACSmoke;
2              
3 1     1   29942 use strict;
  1         2  
  1         34  
4 1     1   5 use warnings;
  1         1  
  1         29  
5 1     1   533 use POE qw(Wheel::Run);
  0            
  0            
6             use Storable;
7             use Digest::MD5 qw(md5_hex);
8             use vars qw($VERSION);
9              
10             $VERSION = '1.62';
11              
12             my $GOT_KILLFAM;
13             my $GOT_PTY;
14              
15             BEGIN {
16             $GOT_KILLFAM = 0;
17             eval {
18             require Proc::ProcessTable;
19             $GOT_KILLFAM = 1;
20             };
21             $GOT_PTY = 0;
22             eval {
23             require IO::Pty;
24             $GOT_PTY = 1;
25             };
26             }
27              
28             sub spawn {
29             my $package = shift;
30             my %opts = @_;
31             $opts{lc $_} = delete $opts{$_} for keys %opts;
32             my $options = delete $opts{options};
33              
34             if ( $^O eq 'MSWin32' ) {
35             eval { require Win32; };
36             if ($@) { die "Win32 but failed to load:\n$@" }
37             eval { require Win32::Job; };
38             if ($@) { die "Win32::Job but failed to load:\n$@" }
39             eval { require Win32::Process; };
40             if ($@) { die "Win32::Process but failed to load:\n$@" }
41             }
42              
43             my $self = bless \%opts, $package;
44             $self->{session_id} = POE::Session->create(
45             object_states => [
46             $self => { shutdown => '_shutdown',
47             submit => '_command',
48             push => '_command',
49             unshift => '_command',
50             recent => '_command',
51             check => '_command',
52             indices => '_command',
53             author => '_command',
54             flush => '_command',
55             'package' => '_command',
56             },
57             $self => [ qw(_start _spawn_wheel _wheel_error _wheel_closed _wheel_stdout _wheel_stderr _wheel_idle _wheel_kill _sig_child _sig_handle) ],
58             ],
59             heap => $self,
60             ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
61             )->ID();
62             return $self;
63             }
64              
65             sub session_id {
66             return $_[0]->{session_id};
67             }
68              
69             sub pending_jobs {
70             return @{ $_[0]->{job_queue} };
71             }
72              
73             sub current_job {
74             my $self = shift;
75             return unless $self->{_current_job};
76             my $item = Storable::dclone( $self->{_current_job} );
77             return $item;
78             }
79              
80             sub current_log {
81             my $self = shift;
82             return unless $self->{_wheel_log};
83             my $item = Storable::dclone( $self->{_wheel_log} );
84             return $item;
85             }
86              
87             sub pause_queue {
88             my $self = shift;
89             $self->{paused} = 1;
90             }
91              
92             sub resume_queue {
93             my $self = shift;
94             my $pause = delete $self->{paused};
95             $poe_kernel->post( $self->{session_id}, '_spawn_wheel' ) if $pause;
96             }
97              
98             sub paused {
99             return $_[0]->{paused};
100             }
101              
102             sub statistics {
103             my $self = shift;
104             my @stats;
105             push @stats, $self->{stats}->{$_} for qw(started totaljobs avg_run min_run max_run);
106             return @stats if wantarray;
107             return \@stats;
108             }
109              
110             sub shutdown {
111             my $self = shift;
112             $poe_kernel->post( $self->{session_id}, 'shutdown' );
113             }
114              
115             sub _start {
116             my ($kernel,$self) = @_[KERNEL,OBJECT];
117             $kernel->sig( 'HUP', '_sig_handle' );
118             $self->{session_id} = $_[SESSION]->ID();
119             if ( $self->{alias} ) {
120             $kernel->alias_set( $self->{alias} );
121             } else {
122             $kernel->refcount_increment( $self->{session_id} => __PACKAGE__ );
123             }
124             $self->{job_queue} = [ ];
125             $self->{idle} = 600 unless $self->{idle};
126             $self->{timeout} = 3600 unless $self->{timeout};
127             $self->{stats} = {
128             started => time(),
129             totaljobs => 0,
130             avg_run => 0,
131             min_run => 0,
132             max_run => 0,
133             _sum => 0,
134             };
135             $ENV{APPDATA} = $self->{appdata} if $self->{appdata};
136             undef;
137             }
138              
139             sub _sig_handle {
140             $poe_kernel->sig_handled();
141             }
142              
143             sub _shutdown {
144             my ($kernel,$self) = @_[KERNEL,OBJECT];
145             $kernel->sig( 'HUP' );
146             $kernel->sig( 'KILL' );
147             $kernel->alias_remove( $_ ) for $kernel->alias_list();
148             $kernel->refcount_decrement( $self->{session_id} => __PACKAGE__ ) unless $self->{alias};
149             $kernel->refcount_decrement( $_->{session}, __PACKAGE__ ) for @{ $self->{job_queue} };
150             $self->{_shutdown} = 1;
151             undef;
152             }
153              
154             sub _command {
155             my ($kernel,$self,$state,$sender) = @_[KERNEL,OBJECT,STATE,SENDER];
156             return if $self->{_shutdown};
157             my $args;
158             if ( ref( $_[ARG0] ) eq 'HASH' ) {
159             $args = { %{ $_[ARG0] } };
160             } else {
161             $args = { @_[ARG0..$#_] };
162             }
163              
164             $state = 'push' if $state eq 'submit';
165              
166             $args->{lc $_} = delete $args->{$_} for grep { $_ !~ /^_/ } keys %{ $args };
167              
168             my $ref = $args->{session} ? $kernel->alias_resolve( $args->{session} ) : $sender;
169             $args->{session} = $ref->ID();
170              
171             if ( !$args->{module} and $state !~ /^(recent|check|indices|package|author|flush)$/i ) {
172             warn "No 'module' specified for $state";
173             return;
174             }
175              
176             unless ( $args->{event} ) {
177             warn "No 'event' specified for $state";
178             return;
179             }
180              
181             if ( $state =~ /^(package|author)$/ and !$args->{search} ) {
182             warn "No 'search' criteria specified for $state";
183             return;
184             }
185              
186             $args->{submitted} = time();
187              
188             if ( $state eq 'recent' ) {
189             if ( $^O eq 'MSWin32' ) {
190             $args->{program} = \&_recent_modules;
191             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X ];
192             }
193             else {
194             my $perl = $args->{perl} || $self->{perl} || $^X;
195             my $code = 'my $smoke = CPANPLUS::YACSmoke->new(); print "$_\n" for $smoke->_download_list();';
196             $args->{program} = [ $perl, '-MCPANPLUS::YACSmoke', '-e', $code ];
197             }
198             }
199             elsif ( $state eq 'check' ) {
200             if ( $^O eq 'MSWin32' ) {
201             $args->{program} = \&_check_yacsmoke;
202             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X ];
203             }
204             else {
205             my $perl = $args->{perl} || $self->{perl} || $^X;
206             $args->{program} = [ $perl, '-MCPANPLUS::YACSmoke', '-e', 1 ];
207             }
208             $args->{debug} = 1;
209             }
210             elsif ( $state eq 'indices' ) {
211             $args->{prioritise} = 0 unless $args->{prioritise};
212             if ( $^O eq 'MSWin32' ) {
213             $args->{program} = \&_reload_indices;
214             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X ];
215             }
216             else {
217             my $perl = $args->{perl} || $self->{perl} || $^X;
218             my $code = 'CPANPLUS::Backend->new()->reload_indices( update_source => 1 );';
219             $args->{program} = [ $perl, '-MCPANPLUS::Backend', '-e', $code ];
220             }
221             }
222             elsif ( $state eq 'author' ) {
223             if ( $^O eq 'MSWin32' ) {
224             $args->{program} = \&_author_search;
225             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X, $args->{type} || 'cpanid', $args->{search} ];
226             }
227             else {
228             my $perl = $args->{perl} || $self->{perl} || $^X;
229             my $code = 'my $type = shift; my $search = shift; my $cb = CPANPLUS::Backend->new(); my %mods = map { $_->package() => 1 } map { $_->modules() } $cb->search( type => $type, allow => [ qr/$search/ ], verbose => 0 ); print qq{$_\n} for sort keys %mods;';
230             $args->{program} = [ $perl, '-MCPANPLUS::Backend', '-e', $code, $args->{type} || 'cpanid', $args->{search} ];
231             }
232             }
233             elsif ( $state eq 'package' ) {
234             if ( $^O eq 'MSWin32' ) {
235             $args->{program} = \&_package_search;
236             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X, $args->{type} || 'package', $args->{search} ];
237             }
238             else {
239             my $perl = $args->{perl} || $self->{perl} || $^X;
240             my $code = 'my $type = shift; my $search = shift; my $cb = CPANPLUS::Backend->new(); my %mods = map { $_->package() => 1 } $cb->search( type => $type, allow => [ qr/$search/ ], verbose => 0 ); print qq{$_\n} for sort keys %mods;';
241             $args->{program} = [ $perl, '-MCPANPLUS::Backend', '-e', $code, $args->{type} || 'package', $args->{search} ];
242             }
243             }
244             elsif ( $state eq 'flush' ) {
245             $args->{prioritise} = 0 unless $args->{prioritise};
246             if ( $^O eq 'MSWin32' ) {
247             $args->{program} = \&_flush;
248             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X, ( $args->{type} and $args->{type} eq 'all' ? 'all' : 'old' ) ];
249             }
250             else {
251             my $perl = $args->{perl} || $self->{perl} || $^X;
252             my $code = 'my $type = shift; my $smoke = CPANPLUS::YACSmoke->new(); $smoke->flush($type) if $smoke->can("flush");';
253             $args->{program} = [ $perl, '-MCPANPLUS::YACSmoke', '-e', $code, ( $args->{type} and $args->{type} eq 'all' ? 'all' : 'old' ) ];
254             }
255             }
256             else {
257             if ( $^O eq 'MSWin32' ) {
258             $args->{program} = \&_test_module;
259             $args->{program_args} = [ $args->{perl} || $self->{perl} || $^X, $args->{module} ];
260             }
261             else {
262             my $perl = $args->{perl} || $self->{perl} || $^X;
263             my $code = 'my $module = shift; my $smoke = CPANPLUS::YACSmoke->new(); $smoke->test($module);';
264             $args->{program} = [ $perl, '-MCPANPLUS::YACSmoke', '-e', $code, $args->{module} ];
265             }
266             }
267              
268             $kernel->refcount_increment( $args->{session}, __PACKAGE__ );
269              
270             $args->{cmd} = $state;
271              
272             if ( $state eq 'unshift' or $state eq 'recent' or $args->{prioritise} ) {
273             unshift @{ $self->{job_queue} }, $args;
274             }
275             else {
276             push @{ $self->{job_queue} }, $args;
277             }
278              
279             $kernel->yield( '_spawn_wheel' );
280              
281             undef;
282             }
283              
284             sub _sig_child {
285             my ($kernel,$self,$thing,$pid,$status) = @_[KERNEL,OBJECT,ARG0..ARG2];
286             push @{ $self->{_wheel_log} }, "$thing $pid $status";
287             warn "$thing $pid $status\n" if $self->{debug};
288             $kernel->delay( '_wheel_idle' );
289             delete $self->{_digests};
290             delete $self->{_loop_detect};
291             my $job = delete $self->{_current_job};
292             $job->{status} = $status;
293             my $log = delete $self->{_wheel_log};
294             if ( $job->{cmd} eq 'recent' ) {
295             pop @{ $log };
296             s/\x0D$// for @{ $log };
297             $job->{recent} = $log;
298             }
299             elsif ( $job->{cmd} =~ /^(package|author)$/ ) {
300             pop @{ $log };
301             s/\x0D$// for @{ $log };
302             @{ $job->{results} } = grep { $_ !~ /^\[/ } @{ $log };
303             }
304             else {
305             $job->{log} = $log;
306             }
307             $job->{end_time} = time();
308             unless ( $self->{debug} ) {
309             delete $job->{program};
310             delete $job->{program_args};
311             }
312             # Stats
313             my $run_time = $job->{end_time} - $job->{start_time};
314             $self->{stats}->{max_run} = $run_time if $run_time > $self->{stats}->{max_run};
315             $self->{stats}->{min_run} = $run_time if $self->{stats}->{min_run} == 0;
316             $self->{stats}->{min_run} = $run_time if $run_time < $self->{stats}->{min_run};
317             $self->{stats}->{_sum} += $run_time;
318             $self->{stats}->{totaljobs}++;
319             $self->{stats}->{avg_run} = $self->{stats}->{_sum} / $self->{stats}->{totaljobs};
320             $self->{debug} = delete $job->{global_debug};
321             #$ENV{APPDATA} = delete $job->{backup_env} if $job->{appdata};
322             $kernel->post( $job->{session}, $job->{event}, $job );
323             $kernel->refcount_decrement( $job->{session}, __PACKAGE__ );
324             $kernel->yield( '_spawn_wheel' );
325             $kernel->sig_handled();
326             }
327              
328             sub _spawn_wheel {
329             my ($kernel,$self) = @_[KERNEL,OBJECT];
330             return if $self->{wheel};
331             return if $self->{_shutdown};
332             return if $self->{paused};
333             my $job = shift @{ $self->{job_queue} };
334             return unless $job;
335             my $backup_env;
336             if ( $job->{appdata} ) {
337             $backup_env = $ENV{APPDATA};
338             $ENV{APPDATA} = $job->{appdata};
339             }
340             $self->{wheel} = POE::Wheel::Run->new(
341             Program => $job->{program},
342             ProgramArgs => $job->{program_args},
343             StdoutEvent => '_wheel_stdout',
344             StderrEvent => '_wheel_stderr',
345             ErrorEvent => '_wheel_error',
346             CloseEvent => '_wheel_close',
347             ( $GOT_PTY and !$self->{no_pty} ? ( Conduit => 'pty-pipe' ) : () ),
348             );
349             if ( $job->{appdata} ) {
350             delete $ENV{APPDATA};
351             $ENV{APPDATA} = $backup_env if $backup_env;
352             }
353             unless ( $self->{wheel} ) {
354             warn "Couldn\'t spawn a wheel for $job->{module}\n";
355             $kernel->refcount_decrement( $job->{session}, __PACKAGE__ );
356             return;
357             }
358             if ( defined $job->{debug} ) {
359             $job->{global_debug} = delete $self->{debug};
360             $self->{debug} = $job->{debug};
361             }
362             $self->{_wheel_log} = [ ];
363             $self->{_digests} = { };
364             $self->{_loop_detect} = 0;
365             $self->{_current_job} = $job;
366             $job->{PID} = $self->{wheel}->PID();
367             $job->{start_time} = time();
368             $kernel->sig_child( $job->{PID}, '_sig_child' );
369             $kernel->delay( '_wheel_idle', 60 ) unless $job->{cmd} eq 'indices';
370             undef;
371             }
372              
373             sub _wheel_error {
374             $poe_kernel->delay( '_wheel_idle' );
375             delete $_[OBJECT]->{wheel};
376             undef;
377             }
378              
379             sub _wheel_closed {
380             $poe_kernel->delay( '_wheel_idle' );
381             delete $_[OBJECT]->{wheel};
382             undef;
383             }
384              
385             sub _wheel_stdout {
386             my ($self, $input, $wheel_id) = @_[OBJECT, ARG0, ARG1];
387             $self->{_wheel_time} = time();
388             push @{ $self->{_wheel_log} }, $input;
389             warn $input, "\n" if $self->{debug};
390             if ( $self->_detect_loop( $input ) ) {
391             $self->{_current_job}->{excess_kill} = 1;
392             $poe_kernel->yield( '_wheel_kill', 'Killing current run CPAN::Shell loop detected' );
393             return;
394             }
395             undef;
396             }
397              
398             sub _wheel_stderr {
399             my ($self, $input, $wheel_id) = @_[OBJECT, ARG0, ARG1];
400             $self->{_wheel_time} = time();
401             if ( $^O eq 'MSWin32' and !$self->{_current_job}->{GRP_PID} and my ($pid) = $input =~ /(\d+)/ ) {
402             $self->{_current_job}->{GRP_PID} = $pid;
403             warn "Grp PID: $pid\n" if $self->{debug};
404             return;
405             }
406             push @{ $self->{_wheel_log} }, $input unless $self->{_current_job}->{cmd} eq 'recent';
407             warn $input, "\n" if $self->{debug};
408             if ( $self->_detect_loop( $input ) ) {
409             $self->{_current_job}->{excess_kill} = 1;
410             $poe_kernel->yield( '_wheel_kill', 'Killing current run CPAN::Shell loop detected' );
411             return;
412             }
413             undef;
414             }
415              
416             sub _detect_loop {
417             my $self = shift;
418             my $input = shift || return;
419             return if $self->{_loop_detect};
420             my $digest = md5_hex( $input );
421             $self->{_digests}->{ $digest }++;
422             return unless ++$self->{_digests}->{ $digest } > 300;
423             return $self->{_loop_detect} = 1;
424             }
425              
426             sub _wheel_idle {
427             my ($kernel,$self) = @_[KERNEL,OBJECT];
428             my $now = time();
429             if ( $now - $self->{_wheel_time} >= $self->{idle} ) {
430             $self->{_current_job}->{idle_kill} = 1;
431             $kernel->yield( '_wheel_kill', 'Killing current run due to excessive idle' );
432             return;
433             }
434             if ( $now - $self->{_current_job}->{start_time} >= $self->{timeout} ) {
435             $self->{_current_job}->{excess_kill} = 1;
436             $kernel->yield( '_wheel_kill', 'Killing current run due to excessive run-time' );
437             return;
438             }
439             $kernel->delay( '_wheel_idle', 60 );
440             return;
441             }
442              
443             sub _wheel_kill {
444             my ($kernel,$self,$reason) = @_[KERNEL,OBJECT,ARG0];
445             push @{ $self->{_wheel_log} }, $reason;
446             warn $reason, "\n" if $self->{debug};
447             if ( $^O eq 'MSWin32' and $self->{wheel} ) {
448             my $grp_pid = $self->{_current_job}->{GRP_PID};
449             return unless $grp_pid;
450             warn Win32::FormatMessage( Win32::GetLastError() )
451             unless Win32::Process::KillProcess( $grp_pid, 0 );
452             }
453             else {
454             if ( !$self->{no_grp_kill} ) {
455             if ( $^O eq 'solaris' ) {
456             kill( 9, '-' . $self->{wheel}->PID() ) if $self->{wheel};
457             }
458             else {
459             $self->{wheel}->kill(-9) if $self->{wheel};
460             }
461             }
462             elsif ( $GOT_KILLFAM ) {
463             _kill_family( 9, $self->{wheel}->PID() ) if $self->{wheel};
464             }
465             else {
466             $self->{wheel}->kill(9) if $self->{wheel};
467             }
468             }
469             return;
470             }
471              
472             sub _check_yacsmoke {
473             my $perl = shift;
474             my $cmdline = $perl . q{ -MCPANPLUS::YACSmoke -e 1};
475             my $job = Win32::Job->new()
476             or die Win32::FormatMessage( Win32::GetLastError() );
477             my $pid = $job->spawn( $perl, $cmdline )
478             or die Win32::FormatMessage( Win32::GetLastError() );
479             warn $pid, "\n";
480             my $ok = $job->watch( sub { 0 }, 60 );
481             my $hashref = $job->status();
482             return $hashref->{$pid}->{exitcode};
483             }
484              
485             sub _test_module {
486             my $perl = shift;
487             my $module = shift;
488             my $cmdline = $perl . ' -MCPANPLUS::YACSmoke -e "my $module = shift; my $smoke = CPANPLUS::YACSmoke->new(); $smoke->test($module);" ' . $module;
489             my $job = Win32::Job->new()
490             or die Win32::FormatMessage( Win32::GetLastError() );
491             my $pid = $job->spawn( $perl, $cmdline )
492             or die Win32::FormatMessage( Win32::GetLastError() );
493             warn $pid, "\n";
494             my $ok = $job->watch( sub { 0 }, 60 );
495             my $hashref = $job->status();
496             return $hashref->{$pid}->{exitcode};
497             }
498              
499             sub _flush {
500             my $perl = shift;
501             my $type = shift;
502             my $cmdline = $perl . ' -MCPANPLUS::YACSmoke -e "my $type = shift; my $smoke = CPANPLUS::YACSmoke->new(); $smoke->flush($type) if $smoke->can(q{flush});" ' . $type;
503             my $job = Win32::Job->new()
504             or die Win32::FormatMessage( Win32::GetLastError() );
505             my $pid = $job->spawn( $perl, $cmdline )
506             or die Win32::FormatMessage( Win32::GetLastError() );
507             warn $pid, "\n";
508             my $ok = $job->watch( sub { 0 }, 60 );
509             my $hashref = $job->status();
510             return $hashref->{$pid}->{exitcode};
511             }
512             sub _recent_modules {
513             my $perl = shift;
514             my $cmdline = $perl . ' -MCPANPLUS::YACSmoke -e "my $smoke = CPANPLUS::YACSmoke->new();print qq{$_\n} for $smoke->{plugin}->download_list();"';
515             my $job = Win32::Job->new()
516             or die Win32::FormatMessage( Win32::GetLastError() );
517             my $pid = $job->spawn( $perl, $cmdline )
518             or die Win32::FormatMessage( Win32::GetLastError() );
519             warn $pid, "\n";
520             my $ok = $job->watch( sub { 0 }, 60 );
521             my $hashref = $job->status();
522             return $hashref->{$pid}->{exitcode};
523             }
524              
525             sub _reload_indices {
526             my $perl = shift;
527             my $cmdline = $perl . ' -MCPANPLUS::Backend -e "CPANPLUS::Backend->new()->reload_indices( update_source => 1 );"';
528             my $job = Win32::Job->new()
529             or die Win32::FormatMessage( Win32::GetLastError() );
530             my $pid = $job->spawn( $perl, $cmdline )
531             or die Win32::FormatMessage( Win32::GetLastError() );
532             warn $pid, "\n";
533             my $ok = $job->watch( sub { 0 }, 60 );
534             my $hashref = $job->status();
535             return $hashref->{$pid}->{exitcode};
536             }
537              
538             sub _author_search {
539             my $perl = shift;
540             my $type = shift;
541             my $search = shift;
542             my $cmdline = $perl . ' -MCPANPLUS::YACSmoke -e "my $type = shift; my $search = shift; my $cb = CPANPLUS::Backend->new(); my %mods = map { $_->package() => 1 } map { $_->modules() } $cb->search( type => $type, allow => [ qr/$search/ ], [ verbose => 0 ] ); print qq{$_\n} for sort keys %mods;" ' . $type . " " . $search;
543             my $job = Win32::Job->new()
544             or die Win32::FormatMessage( Win32::GetLastError() );
545             my $pid = $job->spawn( $perl, $cmdline )
546             or die Win32::FormatMessage( Win32::GetLastError() );
547             warn $pid, "\n";
548             my $ok = $job->watch( sub { 0 }, 60 );
549             my $hashref = $job->status();
550             return $hashref->{$pid}->{exitcode};
551             }
552              
553             sub _package_search {
554             my $perl = shift;
555             my $type = shift;
556             my $search = shift;
557             my $cmdline = $perl . ' -MCPANPLUS::YACSmoke -e "my $type = shift; my $search = shift; my $cb = CPANPLUS::Backend->new(); my %mods = map { $_->package() => 1 } $cb->search( type => $type, allow => [ qr/$search/ ], [ verbose => 0 ] ); print qq{$_\n} for sort keys %mods;" ' . $type . " " . $search;
558             my $job = Win32::Job->new()
559             or die Win32::FormatMessage( Win32::GetLastError() );
560             my $pid = $job->spawn( $perl, $cmdline )
561             or die Win32::FormatMessage( Win32::GetLastError() );
562             warn $pid, "\n";
563             my $ok = $job->watch( sub { 0 }, 60 );
564             my $hashref = $job->status();
565             return $hashref->{$pid}->{exitcode};
566             }
567              
568             sub _kill_family {
569             my ($signal, @pids) = @_;
570             my $pt = Proc::ProcessTable->new;
571             my (@procs) = @{$pt->table};
572             my (@kids) = _get_pids( \@procs, @pids );
573             @pids = (@pids, @kids);
574             kill $signal, reverse @pids;
575             }
576              
577             sub _get_pids {
578             my($procs, @kids) = @_;
579             my @pids;
580             foreach my $kid (@kids) {
581             foreach my $proc (@$procs) {
582             if ($proc->ppid == $kid) {
583             my $pid = $proc->pid;
584             push @pids, $pid, _get_pids( $procs, $pid );
585             }
586             }
587             }
588             @pids;
589             }
590              
591             1;
592             __END__