File Coverage

lib/IPC/RunExternal.pm
Criterion Covered Total %
statement 88 105 83.8
branch 23 36 63.8
condition 6 15 40.0
subroutine 11 11 100.0
pod 1 1 100.0
total 129 168 76.7


line stmt bran cond sub pod time code
1             ## no critic (Documentation::RequirePodSections)
2             ## no critic (ControlStructures::ProhibitDeepNests)
3             ## no critic (ValuesAndExpressions::ProhibitMagicNumbers)
4              
5             package IPC::RunExternal;
6              
7 1     1   64448 use strict;
  1         2  
  1         27  
8 1     1   5 use warnings;
  1         2  
  1         21  
9 1     1   17 use 5.010000;
  1         3  
10              
11             # ABSTRACT: Execute an external command conveniently by hiding the details of IPC::Open3.
12              
13             our $VERSION = '0.101'; # VERSION: generated by DZP::OurPkgVersion
14              
15              
16 1     1   520 use English '-no_match_vars';
  1         3416  
  1         5  
17 1     1   351 use Carp 'croak';
  1         1  
  1         42  
18 1     1   444 use IPC::Open3;
  1         3819  
  1         51  
19 1     1   432 use IO::Select; # for select
  1         1592  
  1         57  
20 1     1   7 use Symbol 'gensym'; # for gensym
  1         2  
  1         43  
21              
22 1     1   6 use Exporter 'import';
  1         1  
  1         59  
23             our @EXPORT_OK = qw(runexternal);
24             our @EXPORT = qw(runexternal); ## no critic (Modules::ProhibitAutomaticExportation)
25             our %EXPORT_TAGS = ( all => [ qw(runexternal) ] );
26              
27 1     1   508 use autodie;
  1         15111  
  1         4  
28              
29             # CONSTANTS for this module
30             my $TRUE = 1;
31             my $FALSE = 0;
32             my $EMPTY_STR = q{};
33              
34             my $DEFAULT_PRINT_PROGRESS_INDICATOR = $FALSE;
35             my $DEFAULT_PROGRESS_INDICATOR_CHARACTER = q{.};
36             my $DEFAULT_EXECUTE_EVERY_SECOND_ROUTINE_POINTER = $FALSE;
37             my $EXIT_STATUS_OK = 1;
38             my $EXIT_STATUS_TIMEOUT = 0;
39             my $EXIT_STATUS_FAILED = -1;
40             my $SIGKILL = 9;
41              
42             # GLOBALS
43             # No global variables
44              
45              
46              
47              
48             sub runexternal { ## no critic (Subroutines::ProhibitExcessComplexity)
49              
50             # Parameters
51 7     7 1 14856 my ($command, $input, $timeout, $parameter_tags) = @_;
52              
53 7 50       43 if(!defined $command) {
54 0         0 croak('Parameter \'command\' is not initialised!');
55             }
56 7 50       21 if(!defined $input) {
57 0         0 croak('Parameter \'input\' is not initialised!');
58             }
59 7 50       21 if($timeout < 0) {
60 0         0 croak('Parameter \'timeout\' is not valid!');
61             }
62              
63 7         13 my $print_progress_indicator = $DEFAULT_PRINT_PROGRESS_INDICATOR;
64 7 50       20 if(exists $parameter_tags->{'print_progress_indicator'}) {
65 0 0 0     0 if($parameter_tags->{'print_progress_indicator'} == $FALSE ||
66             $parameter_tags->{'print_progress_indicator'} == $TRUE) {
67 0         0 $print_progress_indicator = $parameter_tags->{'print_progress_indicator'};
68             }
69             else {
70 0         0 croak('Parameter \'print_progress_indicator\' is not valid (must be 1/0)!');
71             }
72             }
73              
74 7         11 my $progress_indicator_char = $DEFAULT_PROGRESS_INDICATOR_CHARACTER;
75 7 100       50 if(exists $parameter_tags->{'progress_indicator_char'}) {
76 1         8 $progress_indicator_char = $parameter_tags->{'progress_indicator_char'};
77             }
78              
79 7         14 my $execute_every_second = $DEFAULT_EXECUTE_EVERY_SECOND_ROUTINE_POINTER;
80 7 50       16 if(exists $parameter_tags->{'execute_every_second'}) {
81 0 0       0 if(ref($parameter_tags->{'execute_every_second'}) eq 'CODE') {
82 0         0 $execute_every_second = $parameter_tags->{'execute_every_second'};
83             }
84             else {
85 0         0 croak('Parameter execute_every_second is not a code reference!');
86             }
87             }
88              
89             # Variables
90 7         10 my $command_exit_status = $EXIT_STATUS_OK;
91 7         12 my $output_std = $EMPTY_STR;
92 7         6 my $output_error = $EMPTY_STR;
93 7         9 my $output_all = $EMPTY_STR;
94              
95             # Validity check
96 7 50 33     216 if(
97             $command ne $EMPTY_STR
98             #&& defined($input)
99             && $timeout >= 0
100             )
101             {
102 7         57 local $OUTPUT_AUTOFLUSH = $TRUE; # Equals to var $|. Flushes always after writing.
103 7         17 my ($infh,$outfh,$errfh); # these are the FHs for our child
104 7         86 $errfh = gensym(); # we create a symbol for the errfh
105             # because open3 will not do that for us
106 7         202 my $pid;
107             # Read Perl::Critic::Policy::ErrorHandling::RequireCheckingReturnValueOfEval
108             # for an evil eval weakness and a dasdardly difficult eval handling.
109 7         13 my $eval_ok = 1;
110             eval {
111 7         29 $pid = open3($infh, $outfh, $errfh, $command);
112             # To cover the possiblity that an operation may correctly return a
113             # false value, end the block with "1":
114 6         17422 1;
115 7 100       11 } or do {
116 1         10958 $eval_ok = 0;
117             };
118 7 100       42 if($eval_ok) {
119 6         11 print {$infh} $input; ## no critic (InputOutput::RequireCheckedSyscalls)
  6         30  
120 6         83 close $infh;
121 6         3292 my $sel = IO::Select->new(); # create a select object to notify
122             # us on reads on our FHs
123 6         153 $sel->add($outfh, $errfh); # add the FHs we're interested in
124 6         582 my $out_handles_open = 2;
125             ## no critic (ControlStructures::ProhibitCStyleForLoops)
126 6   100     72 for(my $slept_secs = -1; $out_handles_open > 0 && $slept_secs < $timeout; $slept_secs++) {
127 13         137 while(my @ready = $sel->can_read(1)) { # read ready, timeout after 1 second.
128 16         5204456 foreach my $fh (@ready) {
129 20         486 my $line = <$fh>; # read one line from this fh
130 20 100       139 if( !(defined $line) ){ # EOF on this FH
131 8         34 $sel->remove($fh); # remove it from the list
132 8         373 $out_handles_open -= 1;
133 8         32 next; # and go handle the next FH
134             }
135 12 100       94 if($fh == $outfh) { # if we read from the outfh
    50          
136 5         43 $output_std .= $line;
137 5         44 $output_all .= $line;
138             } elsif($fh == $errfh) { # do the same for errfh
139 7         94 $output_error .= $line;
140 7         113 $output_all .= $line;
141             } else { # we read from something else?!?!
142 0         0 croak "Shouldn't be here!\n";
143             }
144             }
145             }
146 13 100       9011540 if($timeout == 0) {
147             # No timeout, so we lower the counter by one to keep it forever under 0.
148             # Only the closing of the output handles ($out_handles_open == 0) can break the loop
149 3         24 $slept_secs--;
150             }
151 13 50 33     71 if($print_progress_indicator == $TRUE && $out_handles_open > 0) {
152 0         0 print {*STDOUT} $progress_indicator_char; ## no critic (InputOutput::RequireCheckedSyscalls)
  0         0  
153             }
154 13 50 33     137 if($execute_every_second && $out_handles_open > 0) {
155 0         0 &{$execute_every_second}($slept_secs);
  0         0  
156             }
157             }
158             # It is safe to kill in all circumstances.
159             # Anyway, we must reap the child process.
160 6         124 my $killed = kill $SIGKILL, $pid;
161 6         1368 my $command_return_status = $CHILD_ERROR >> 8;
162 6 100       233 if($out_handles_open > 0) {
163 2         25 $output_error .= 'Timeout';
164 2         11 $output_all .= 'Timeout';
165 2         131 $command_exit_status = $EXIT_STATUS_TIMEOUT;
166             }
167             }
168             else {
169             # open3 failed!
170 1         10 $output_error .= 'Could not run command';
171 1         9 $output_all .= 'Could not run command';
172 1         53 $command_exit_status = $EXIT_STATUS_FAILED;
173             }
174             }
175             else {
176             # Parameter check
177 0         0 $output_error .= 'Invalid parameters';
178 0         0 $output_all .= 'Invalid parameters';
179 0         0 $command_exit_status = $EXIT_STATUS_FAILED;
180             }
181              
182 7         301 return ($command_exit_status, $output_std, $output_error, $output_all);
183             }
184              
185             1;
186              
187             __END__