File Coverage

blib/lib/POE/Component/SmokeBox/Backend.pm
Criterion Covered Total %
statement 242 297 81.4
branch 94 156 60.2
condition 22 60 36.6
subroutine 31 36 86.1
pod 7 7 100.0
total 396 556 71.2


line stmt bran cond sub pod time code
1             package POE::Component::SmokeBox::Backend;
2             $POE::Component::SmokeBox::Backend::VERSION = '0.54';
3             #ABSTRACT: smoker backend to POE::Component::SmokeBox
4              
5 44     44   190414 use strict;
  44         98  
  44         1207  
6 44     44   210 use warnings;
  44         87  
  44         997  
7 44     44   195 use Carp;
  44         80  
  44         2520  
8 44     44   25580 use Storable;
  44         128203  
  44         2450  
9 44     44   29530 use File::Temp ();
  44         393922  
  44         1261  
10 44     44   304 use File::Path qw[rmtree];
  44         124  
  44         1879  
11 44     44   253 use File::Spec;
  44         87  
  44         937  
12 44     44   709 use POSIX qw( O_CREAT O_RDWR O_RDONLY ); # for SDBM_File
  44         6487  
  44         311  
13 44     44   21833 use SDBM_File;
  44         20075  
  44         1911  
14 44     44   789 use POE qw[Wheel::Run Filter::Line];
  44         25145  
  44         424  
15 44     44   965357 use Digest::SHA qw[sha256_hex];
  44         122212  
  44         3339  
16 44     44   18612 use Env::Sanctify;
  44         19361  
  44         1552  
17 44     44   18839 use Module::Pluggable search_path => 'POE::Component::SmokeBox::Backend', sub_name => 'backends', except => 'POE::Component::SmokeBox::Backend::Base';
  44         410955  
  44         329  
18              
19             my $GOT_KILLFAM;
20             my $GOT_PTY;
21              
22             BEGIN {
23 44     44   9328 $GOT_KILLFAM = 0;
24 44         129 eval {
25 44         5971 require Proc::ProcessTable;
26 0         0 $GOT_KILLFAM = 1;
27             };
28 44         212 $GOT_PTY = 0;
29 44         104 eval {
30 44         507 require IO::Pty;
31 44         125 $GOT_PTY = 1;
32             };
33 44 50       128741 if ( $^O eq 'MSWin32' ) {
34 0         0 require POE::Wheel::Run::Win32;
35              
36             # MSWin32: Disable critical error popups
37             # Thanks to https://rt.cpan.org/Public/Bug/Display.html?id=56547
38              
39             # Call kernel32.SetErrorMode(SEM_FAILCRITICALERRORS):
40             # "The system does not display the critical-error-handler message box.
41             # Instead, the system sends the error to the calling process." and
42             # "A child process inherits the error mode of its parent process."
43 0 0       0 if ( eval { require Win32API::File } ) {
  0         0  
44 0         0 Win32API::File->import( qw( SetErrorMode SEM_FAILCRITICALERRORS SEM_NOGPFAULTERRORBOX ) );
45 0         0 SetErrorMode( SEM_FAILCRITICALERRORS() | SEM_NOGPFAULTERRORBOX() );
46             } else {
47 0         0 warn "Unable to use Win32API::File -> $@";
48 0         0 warn 'This means sometimes perl.exe will popup a dialog box... Annoying!';
49             }
50             }
51             }
52              
53             my @cmds = qw(check index smoke);
54              
55             sub check {
56 7     7 1 6255 my $package = shift;
57 7         24 return $package->spawn( @_, command => 'check' );
58             }
59              
60             sub index {
61 5     5 1 4064 my $package = shift;
62 5         16 return $package->spawn( @_, command => 'index' );
63             }
64              
65             sub smoke {
66 12     12 1 10339 my $package = shift;
67 12         42 return $package->spawn( @_, command => 'smoke' );
68             }
69              
70             sub spawn {
71 108     108 1 8085 my $package = shift;
72 108         919 my %opts = @_;
73 108         609 my $extra = { map { ( $_ => delete $opts{$_} ) } grep { /^\_/ } keys %opts };
  0         0  
  852         1940  
74 108         355 $opts{extra} = $extra;
75 108         1133 $opts{lc $_} = delete $opts{$_} for keys %opts;
76 108         309 my $options = delete $opts{options};
77 108 50       392 unless ( $opts{event} ) {
78 0         0 carp "The 'event' parameter is a mandatory requirement\n";
79 0         0 return;
80             }
81 108 100       337 $opts{idle} = 600 unless $opts{idle};
82 108 100       347 $opts{timeout} = 3600 unless $opts{timeout};
83 108 100       387 $opts{timer} = 60 unless $opts{timer};
84 108 50       362 $opts{reaper} = 30 unless $opts{reaper};
85 108 100       330 $opts{type} = 'CPANPLUS::YACSmoke' unless $opts{type};
86 108   50     409 $opts{command} = lc $opts{command} || 'check';
87 108 50       267 $opts{command} = 'check' unless grep { $_ eq $opts{command} } @cmds;
  324         943  
88 108 50       384 $opts{perl} = $^X unless $opts{perl}; # and -e $opts{perl};
89 108 100       392 $opts{no_log} = 0 unless $opts{no_log};
90 108 100       406 $opts{check_warnings} = 1 unless exists $opts{check_warnings};
91              
92 108 50       333 if ( $opts{check_warnings} ) {
93 108         28654 require String::Perl::Warnings;
94             }
95              
96 108 50 66     62507599 if ( $opts{command} eq 'smoke' and !$opts{module} ) {
97 0         0 carp "You must specify a 'module' with 'smoke'\n";
98 0         0 return;
99             }
100 108         848 my $self = bless \%opts, $package;
101 108         1203 my @backends = $self->backends();
102 108         711859 my ($type) = grep { /\Q$opts{type}\E$/ } @backends;
  972         4278  
103 108 100       481 unless ( $type ) {
104 1         294 carp "No such backend '$opts{type}'\n";
105 1         56 return;
106             }
107 107         14209 eval "require $type;";
108 107 50       749 if ( $@ ) {
109 0         0 carp "Could not load '$type' '$@'\n";
110 0         0 return;
111             }
112 107         1163 $self->{backend} = $type->new();
113 107 50 33     549 unless ( $self->{backend} or $self->{backend}->can($self->{command}) ) {
114 0         0 croak "Problem loading backend '$type'\n";
115 0         0 return;
116             }
117 107 50       825 if ( $self->{backend}->can('digest') ) {
118 107         602 $self->{_reset_digest} = $self->{backend}->digest();
119             }
120 107         345 my $cmd = $self->{command};
121 107         608 $self->{program} = $self->{backend}->$cmd;
122 107 50 33     524 unless ( $self->{program} or ref $self->{program} eq 'ARRAY' ) {
123 0         0 carp "The backend method '$cmd' did not return an arrayref\n";
124 0         0 return;
125             }
126 107         232 unshift @{ $self->{program} }, $self->{perl};
  107         506  
127 107 100       450 push @{ $self->{program} }, $self->{module} if $cmd eq 'smoke';
  37         169  
128 107 100       2358 $self->{session_id} = POE::Session->create(
129             package_states => [
130             $self => { shutdown => '_shutdown', },
131             $self => [qw(_start _spawn_wheel _wheel_error _wheel_closed _wheel_stdout _wheel_stderr _wheel_idle _wheel_reap _wheel_kill _sig_child)],
132             ],
133             heap => $self,
134             ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
135             )->ID();
136 107         16309 return $self;
137             }
138              
139             sub session_id {
140 0     0 1 0 return $_[0]->{session_id};
141             }
142              
143             sub current_log {
144 0     0 1 0 my $self = shift;
145 0 0       0 return unless $self->{_wheel_log};
146 0         0 my $item = Storable::dclone( $self->{_wheel_log} );
147 0         0 return $item;
148             }
149              
150             sub shutdown {
151 0     0 1 0 my $self = shift;
152 0         0 $poe_kernel->post( $self->session_id() => 'shutdown' => @_ );
153             }
154              
155             sub _start {
156 107     107   42491 my ($kernel,$sender,$self) = @_[KERNEL,SENDER,OBJECT];
157 107         392 $self->{session_id} = $_[SESSION]->ID();
158 107 50 33     999 if ( $kernel == $sender and !$self->{session} ) {
159 0         0 croak "Not called from another POE session and 'session' wasn't set\n";
160             }
161 107         246 my $sender_id;
162 107 50       374 if ( $self->{session} ) {
163 0 0       0 if ( my $ref = $kernel->alias_resolve( $self->{session} ) ) {
164 0         0 $sender_id = $ref->ID();
165             }
166             else {
167 0         0 croak "Could not resolve 'session' to a valid POE session\n";
168             }
169             }
170             else {
171 107         306 $sender_id = $sender->ID();
172             }
173 107         803 $kernel->refcount_increment( $sender_id, __PACKAGE__ );
174 107         4880 $self->{session} = $sender_id;
175 107 50       768 $kernel->detach_myself() if $kernel != $sender;
176              
177 107         15032 $self->{_wheel_log} = [ ];
178              
179 107 100       393 if ( !$self->{_reset_digest} ) {
180 74         310 $self->_tie_digests();
181             }
182             else {
183 33         84 $self->{_digests} = { };
184             }
185              
186 107         376 $self->{_loop_detect} = 0;
187 107         321 $self->{start_time} = time();
188              
189 107         931 $kernel->yield( '_spawn_wheel' );
190 107         10979 return;
191             }
192              
193             sub _shutdown {
194 0     0   0 my ($kernel,$self) = @_[KERNEL,OBJECT];
195 0 0       0 if ( !$self->{_reset_digest} ) {
196 0         0 $self->_untie_digests();
197             }
198             else {
199 0         0 delete $self->{_digests};
200             }
201 0         0 $self->{term_kill} = 1;
202 0         0 $kernel->yield( '_wheel_kill', 'Killing current due to component shutdown event' );
203 0         0 return;
204             }
205              
206             # Digests tie and untie
207              
208             sub _tie_digests {
209 74     74   180 my $self = shift;
210 74         1167 $self->{_tempdir} = File::Temp->newdir();
211 74         42272 $self->{_tmpdirname} = $self->{_tempdir}->dirname;
212 74         1857 my $file = File::Spec->catfile( $self->{_tmpdirname}, 'digests.dat' );
213 74         420 $self->{_digests} = { };
214 74 50       188 tie %{ $self->{_digests} }, 'SDBM_File', $file, O_CREAT|O_RDWR, 0644 or die "Could not tie: $!\n";
  74         11122  
215 74         420 return 1;
216             }
217              
218             sub _untie_digests {
219 74     74   215 my $self = shift;
220 74 50       596 if ( $self->{_digests} ) {
221 74         188 untie %{ $self->{_digests} };
  74         2894  
222 74         806 delete $self->{_digests};
223 74         1497 delete $self->{_tempdir};
224 74 50       57808 rmtree( $self->{_tmpdirname} ) if -d $self->{_tmpdirname};
225             }
226 74         322 return 1;
227             }
228              
229             sub _spawn_wheel {
230 107     107   42323 my ($kernel,$self) = @_[KERNEL,OBJECT];
231              
232             # do we need to process callbacks?
233 107 100       512 if ( $self->{do_callback} ) {
234             # Ask it if we should process this job or not?
235 6 100       26 unless ( $self->{do_callback}->( 'BEFORE', $self ) ) {
236 1 50 33     101 warn "Callback denied job, aborting!\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
237 1         5 my $job = $self->_finalize_job( -1 );
238 1         2 $job->{cb_kill} = 1;
239 1         5 $kernel->post( $self->{session}, $self->{event}, $job );
240 1         105 return;
241             }
242             }
243              
244             # Set appropriate %ENV values before we fork()
245             my $sanctify = Env::Sanctify->sanctify(
246             env => $self->{env},
247 106         2533 sanctify => [
248             '^POE_',
249             '^PERL5_SMOKEBOX',
250             '^HARNESS_',
251             '^(PERL5LIB|TAP_VERSION|TEST_VERBOSE)$',
252             '^AUTHOR_TESTING$',
253             '^PERL_TEST',
254             ] );
255 106         69837 my $type = 'POE::Wheel::Run';
256 106 50       676 $type .= '::Win32' if $^O eq 'MSWin32';
257             $self->{wheel} = $type->new(
258             Program => $self->{program},
259 106 50       1763 StdoutEvent => '_wheel_stdout',
260             StderrEvent => '_wheel_stderr',
261             StdoutFilter => POE::Filter::Line->new( InputLiteral => "\n" ),
262             StderrFilter => POE::Filter::Line->new( InputLiteral => "\n" ),
263             ErrorEvent => '_wheel_error',
264             CloseEvent => '_wheel_closed',
265             ( $GOT_PTY ? ( Conduit => 'pty-pipe' ) : () ),
266             );
267             # Restore the %ENV values
268 106         788865 $sanctify->restore();
269 106         12469 $self->{_wheel_time} = time();
270 106         2249 $self->{PID} = $self->{wheel}->PID();
271 106         4076 $kernel->sig_child( $self->{PID}, '_sig_child' );
272 106 100       31906 $kernel->delay( '_wheel_idle', $self->{timer} ) unless $self->{command} eq 'index';
273 106         19296 return;
274             }
275              
276             sub _sig_child {
277 106     106   72633 my ($kernel,$self,$thing,$pid,$status) = @_[KERNEL,OBJECT,ARG0..ARG2];
278 106 100       650 push @{ $self->{_wheel_log} }, "$thing $pid $status" if ! $self->{no_log};
  105         787  
279 106 50 33     1577 warn "$thing $pid $status\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
280 106         763 $kernel->sig_handled();
281 106         2001 $kernel->delay( '_wheel_idle' );
282              
283 106         7048 my $job = $self->_finalize_job( $status );
284              
285             # do we need to process callbacks?
286 106 100       458 if ( $self->{do_callback} ) {
287             # Inform the callback that the job is done
288 5         34 $self->{do_callback}->( 'AFTER', $self, $job );
289             }
290              
291 106         2423 $kernel->post( $self->{session}, $self->{event}, $job );
292 106 100       15489 $kernel->delay( '_wheel_reap' => $self->{reaper} ) if $self->{wheel};
293 106         618 return;
294             }
295              
296             sub _finalize_job {
297 107     107   512 my( $self, $status ) = @_;
298              
299 107         876 $self->{end_time} = time();
300              
301 107 100       588 if ( !$self->{_reset_digest} ) {
302 74         404 $self->_untie_digests();
303             }
304             else {
305 33         241 delete $self->{_digests};
306             }
307              
308 107         392 delete $self->{_loop_detect};
309              
310 107         340 my $job = { };
311 107         839 $job->{status} = $status;
312 107         836 $job->{log} = $self->{_wheel_log};
313 107         346 $job->{$_} = $self->{extra}->{$_} for keys %{ $self->{extra} };
  107         634  
314 107         428 $job->{$_} = $self->{$_} for grep { $self->{$_} } qw(command env PID start_time end_time idle_kill excess_kill term_kill perl type);
  1070         4675  
315 107 50 33     1537 $job->{program} = $self->{program} if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
316 107 100       1025 $job->{module} = $self->{module} if $self->{command} eq 'smoke';
317 107         976 $poe_kernel->refcount_decrement( $self->{session}, __PACKAGE__ );
318              
319 107         7693 return $job;
320             }
321              
322             sub _wheel_reap {
323 0     0   0 my ($kernel,$self) = @_[KERNEL,OBJECT];
324 0 0 0     0 warn "wheel reaped\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
325 0         0 delete $self->{wheel};
326 0         0 return;
327             }
328              
329             sub _wheel_error {
330 212     212   84148170 my ($self,$operation,$errnum,$errstr,$wheel_id) = @_[OBJECT,ARG0..ARG3];
331 212 100 66     2661 $errstr = "remote end closed" if $operation eq "read" and !$errnum;
332 212 50 33     2608 warn "wheel $wheel_id generated $operation error $errnum: $errstr\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
333 212         893 return;
334             }
335              
336             sub _wheel_closed {
337 106     106   5517 my ($kernel,$self) = @_[KERNEL,OBJECT];
338 106         573 $kernel->delay( '_wheel_idle' );
339 106         12954 $kernel->delay( '_wheel_reap' );
340 106 50 33     6921 warn "wheel closed\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
341 106         1356 delete $self->{wheel};
342 106         47661 return;
343             }
344              
345             sub _wheel_stdout {
346 1091     1091   15149884 my ($self, $input, $wheel_id) = @_[OBJECT, ARG0, ARG1];
347 1091 100       3462 return if $self->{_killed};
348 398         752 $self->{_wheel_time} = time();
349 398 100       934 push @{ $self->{_wheel_log} }, $input if ! $self->{no_log};
  397         1035  
350 398 50 33     1993 warn $input, "\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
351 398 100       1162 if ( $self->_detect_loop( $input, 'stdout' ) ) {
352 1         14 $self->{excess_kill} = 1;
353 1         17 $poe_kernel->yield( '_wheel_kill', 'Killing current run due to detection of looping output' );
354             }
355 398         1687 return;
356             }
357              
358             sub _wheel_stderr {
359 42     42   21138 my ($self, $input, $wheel_id) = @_[OBJECT, ARG0, ARG1];
360 42 50       443 return if $self->{_killed};
361 42         415 $self->{_wheel_time} = time();
362 42 50       286 push @{ $self->{_wheel_log} }, $input if ! $self->{no_log};
  42         275  
363 42 50 33     687 if ( $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG} ) {
364 0 0       0 if ( length( $input ) > 5000 ) {
365 0         0 warn "[SUPPRESSED OUTPUT > 5000]\n";
366             }
367             else {
368 0         0 warn $input, "\n";
369             }
370             }
371 42 50       470 if ( $self->_detect_loop( $input, 'stderr' ) ) {
372 0         0 $self->{excess_kill} = 1;
373 0         0 $poe_kernel->yield( '_wheel_kill', 'Killing current run due to detection of looping output' );
374             }
375 42         195 return;
376             }
377              
378             sub _detect_loop {
379 440     440   887 my $self = shift;
380 440   50     1196 my $input = shift || return;
381 440   50     1174 my $handle = shift || 'stdout';
382 440 100       1342 return if $self->{_loop_detect};
383 350 100       981 if ( my $reset = $self->{_reset_digest} ) {
384 33 50       121 if ( eval { $input =~ $reset } ) {
  33         422  
385 0 0 0     0 warn "Resetting digests\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
386 0         0 $self->{_digests} = { };
387             }
388             }
389 350 50       1677 return if $input =~ /^\[(MSG|ERROR)\]/;
390 350         2687 my $digest = sha256_hex( $input );
391              
392 350         777 my $weighting;
393 350 50 33     1846 if ( $self->{check_warnings} and length( $input ) <= 5000 ) {
394 350 50 66     1556 $weighting = ( $handle eq 'stderr' and String::Perl::Warnings::is_warning($input) ) ? 1 : 10;
395             } else {
396 0 0       0 $weighting = $handle eq 'stderr' ? 1 : 10;
397             }
398              
399 350 100       29181 if ( exists $self->{_digests}->{ $digest } ) {
400 277         5843 $self->{_digests}->{ $digest } += $weighting;
401             }
402             else {
403 73         2688 $self->{_digests}->{ $digest } = $weighting;
404             }
405 350 100       7981 return unless ++$self->{_digests}->{ $digest } > 3000;
406 1         7 return $self->{_loop_detect} = 1;
407             }
408              
409             sub _wheel_idle {
410 2     2   15013583 my ($kernel,$self) = @_[KERNEL,OBJECT];
411 2         10 my $now = time();
412 2 100       19 if ( $now - $self->{_wheel_time} >= $self->{idle} ) {
413 1         18 $self->{idle_kill} = 1;
414 1         16 $kernel->yield( '_wheel_kill', 'Killing current run due to excessive idle' );
415 1         136 return;
416             }
417 1 50       17 if ( $now - $self->{start_time} >= $self->{timeout} ) {
418 1         9 $self->{excess_kill} = 1;
419 1         10 $kernel->yield( '_wheel_kill', 'Killing current run due to excessive run-time' );
420 1         138 return;
421             }
422 0         0 $kernel->delay( '_wheel_idle', 60 );
423 0         0 return;
424             }
425              
426             sub _wheel_kill {
427 3     3   570 my ($kernel,$self,$reason) = @_[KERNEL,OBJECT,ARG0];
428 3 50       37 return if $self->{_killed};
429 3         32 $self->{_killed} = 1;
430 3 50       19 push @{ $self->{_wheel_log} }, $reason if ! $self->{no_log};
  3         21  
431 3 50 33     53 warn $reason, "\n" if $self->{debug} or $ENV{PERL5_SMOKEBOX_DEBUG};
432 3 50 33     60 if ( $^O eq 'MSWin32' and $self->{wheel} ) {
433 0         0 $self->{wheel}->kill();
434             }
435             else {
436 3 50       29 if ( !$self->{no_grp_kill} ) {
437 0 0       0 if ( $^O eq 'solaris' ) {
438 0 0       0 kill( 9, '-' . $self->{wheel}->PID() ) if $self->{wheel};
439             }
440             else {
441 0 0       0 $self->{wheel}->kill(-9) if $self->{wheel};
442             }
443             }
444             # elsif ( $GOT_KILLFAM ) {
445             # _kill_family( 9, $self->{wheel}->PID() ) if $self->{wheel};
446             # }
447             else {
448 3 50       56 $self->{wheel}->kill(9) if $self->{wheel};
449             }
450             }
451 3         403 return;
452             }
453              
454             1;
455              
456             __END__