File Coverage

blib/lib/Command/Run.pm
Criterion Covered Total %
statement 233 246 94.7
branch 119 172 69.1
condition 13 18 72.2
subroutine 27 28 96.4
pod 9 14 64.2
total 401 478 83.8


line stmt bran cond sub pod time code
1             package Command::Run;
2              
3             our $VERSION = "1.02";
4              
5 29     29   2330854 use v5.14;
  29         88  
6 29     29   117 use warnings;
  29         141  
  29         1458  
7 29     29   8767 use utf8;
  29         4919  
  29         123  
8 29     29   685 use Carp;
  29         53  
  29         1559  
9 29     29   131 use Fcntl;
  29         29  
  29         6049  
10 29     29   10756 use IO::File;
  29         206857  
  29         4560  
11              
12 29     29   10673 use parent 'Command::Run::Tmpfile';
  29         6710  
  29         163  
13              
14             our $debug;
15             sub debug {
16 0     0 0 0 my $obj = shift;
17 0 0       0 @_ ? $debug = shift : $debug;
18             }
19              
20             sub code_name {
21 89     89 0 272 my $code = shift;
22 89         768 require B;
23 89         1055 my $cv = B::svref_2object($code);
24 89 50       2118 return if $cv->GV->isa('B::SPECIAL');
25 89         980 $cv->GV->NAME;
26             }
27              
28             my %default_option = (
29             stderr => undef, # undef: pass-through, 'redirect': merge to stdout, 'capture': separate capture
30             );
31              
32             sub new {
33 210     210 1 4049593 my $class = shift;
34 210         1791 my $obj = $class->SUPER::new;
35 210         1158 $obj->{OPTION} = { %default_option };
36 210         627 $obj->{RESULT} = {};
37 210 100       940 $obj->configure(@_) if @_;
38 210         3156 $obj;
39             }
40              
41             sub configure {
42 183     183 0 231 my $obj = shift;
43 183         879 my %args = @_;
44 183         626 for my $key (keys %args) {
45 354         549 my $val = $args{$key};
46 354 100       1250 if ($key eq 'command') {
    100          
    100          
    100          
47 154 50       742 $obj->command(ref $val eq 'ARRAY' ? @$val : $val);
48             } elsif ($key eq 'stdin') {
49 51         277 $obj->_set_stdin($val);
50             } elsif ($key eq 'stdout') {
51 17         56 $obj->{STDOUT_REF} = $val;
52             } elsif ($key eq 'stderr') {
53 33 100       160 if (ref $val eq 'SCALAR') {
54 6         18 $obj->{STDERR_REF} = $val;
55 6         42 $obj->option(stderr => 'capture');
56             } else {
57 27         104 $obj->option(stderr => $val);
58             }
59             } else {
60 99         235 $obj->option($key => $val);
61             }
62             }
63 183         669 $obj;
64             }
65              
66             sub command {
67 425     425 1 8461 my $obj = shift;
68 425 100       872 if (@_) {
69 210         843 $obj->{COMMAND} = [ @_ ];
70 210         3463 $obj;
71             } else {
72 215   50     241 @{$obj->{COMMAND} // []};
  215         1062  
73             }
74             }
75              
76             sub option {
77 132     132 0 176 my $obj = shift;
78 132 50       269 if (@_ == 1) {
79 0         0 return $obj->{OPTION}->{+shift};
80             } else {
81 132         459 while (my($k, $v) = splice @_, 0, 2) {
82 132         434 $obj->{OPTION}->{$k} = $v;
83             }
84 132         350 return $obj;
85             }
86             }
87              
88             sub run {
89 186     186 1 3843 my $obj = shift;
90 186         864 $obj->update(@_);
91 164 100       565 if (my $ref = $obj->{STDOUT_REF}) {
92 13         157 $$ref = $obj->data;
93             }
94 164 100       476 if (my $ref = $obj->{STDERR_REF}) {
95 5         39 $$ref = $obj->error;
96             }
97 164         887 return $obj->result;
98             }
99              
100             sub update {
101 29     29   27247 use Time::localtime;
  29         115392  
  29         70847  
102 195     195 1 364 my $obj = shift;
103 195         417 my @command = $obj->command;
104 195 50       473 if (@command) {
105 195         780 $obj->{RESULT} = $obj->execute(\@command, @_);
106             # Store stdout in temp file for path access
107 172         2585 my $fh = $obj->fh;
108 172 50       2374 $fh->seek(0, 0) or die "seek: $!\n";
109 172 50       3925 $fh->truncate(0) or die "truncate: $!\n";
110 172   50     11002 $fh->print($obj->{RESULT}->{data} // '');
111 172         8975 $fh->flush;
112 172 50       1198 $fh->seek(0, 0) or die "seek: $!\n";
113             }
114 172         2498 $obj->date(ctime());
115 172         903 $obj;
116             }
117              
118             sub result {
119 168     168 1 298 my $obj = shift;
120 168         1060 $obj->{RESULT};
121             }
122              
123             sub execute {
124 195     195 0 286 my $obj = shift;
125 195         273 my $command = shift;
126 195         413 my %opt = (%{$obj->{OPTION}}, @_);
  195         726  
127 195 50       664 my @command = ref $command eq 'ARRAY' ? @$command : ($command);
128              
129             # Use nofork path for code references when requested
130 195 100 100     891 if ($opt{nofork} and ref $command[0] eq 'CODE') {
131 81         213 return $obj->_execute_in_place(\@command, %opt);
132             }
133              
134 114   100     784 my $stderr = $opt{stderr} // '';
135              
136             # Create pipes for stdout and stderr
137 114 50       5012 pipe(my $stdout_r, my $stdout_w) or die "pipe: $!\n";
138 114 100 50     705 pipe(my $stderr_r, my $stderr_w) or die "pipe: $!\n" if $stderr eq 'capture';
139              
140 114   50     180503 my $pid = fork // die "fork: $!\n";
141 114 100       3515 if ($pid == 0) {
142             # Child process
143 23         3022 close $stdout_r;
144 23 100       1486 close $stderr_r if $stderr eq 'capture';
145              
146 23 100       1861 if (exists $opt{stdin}) {
    100          
147 1 50       511 my $tmp = new_tmpfile IO::File or die "tmpfile: $!\n";
148 1         64 binmode $tmp, ':encoding(utf8)';
149 1         182 $tmp->print($opt{stdin});
150 1 50       70 $tmp->seek(0, 0) or die "seek: $!\n";
151 1 50       130 open STDIN, '<&', $tmp or die "dup: $!\n";
152 1         118 binmode STDIN, ':encoding(utf8)';
153             } elsif (my $input = $obj->{INPUT}) {
154 5 50       544 open STDIN, "<&=", $input->fileno or die "open: $!\n";
155 5         963 binmode STDIN, ':encoding(utf8)';
156             }
157              
158 23 50       2647 open STDOUT, ">&=", $stdout_w->fileno or die "open stdout: $!\n";
159 23 100       2617 if ($stderr eq 'redirect') {
    100          
160 3 50       166 open STDERR, ">&STDOUT" or die "open stderr: $!\n";
161             } elsif ($stderr eq 'capture') {
162 4 50       85 open STDERR, ">&=", $stderr_w->fileno or die "open stderr: $!\n";
163             }
164             # else: stderr passes through to terminal
165              
166 23 100       821 if (ref $command[0] eq 'CODE') {
167 8         287 my $code = shift @command;
168 8         182 @ARGV = @command;
169 8 50       247 if (my $name = code_name($code)) {
170 8         255 $0 = $name;
171             }
172 8         346 $code->(@command);
173 8         6560 exit 0;
174             }
175 15         0 exec @command;
176 0         0 die "exec: $@\n";
177             }
178              
179             # Parent process
180 91         6365 close $stdout_w;
181 91 100       1276 close $stderr_w if $stderr eq 'capture';
182              
183 91         18831 binmode $stdout_r, ':encoding(utf8)';
184 91 100       27724 binmode $stderr_r, ':encoding(utf8)' if $stderr eq 'capture';
185              
186 91         761 my $stdout = do { local $/; <$stdout_r> };
  91         3135  
  91         18378406  
187 91 100       9273604 my $stderr_out = $stderr eq 'capture' ? do { local $/; <$stderr_r> } : '';
  7         149  
  7         303  
188              
189 91         4275 close $stdout_r;
190 91 100       687 close $stderr_r if $stderr eq 'capture';
191              
192 91         3346 waitpid $pid, 0;
193 91         1173 my $result = $?;
194              
195             return {
196 91         7979 result => $result,
197             data => $stdout,
198             error => $stderr_out,
199             pid => $pid,
200             };
201             }
202              
203             sub _tmpfile {
204 154     154   560 my ($obj, $key, %opt) = @_;
205 154 100       294 $key .= '_RAW' if $opt{raw};
206 154   66     552 my $fh = $obj->{$key} //= do {
207 144 50       26574 my $f = new_tmpfile IO::File or die "tmpfile: $!\n";
208 144 100       1600 binmode $f, $opt{raw} ? ':utf8' : ':encoding(utf8)';
209 144         4337 $f;
210             };
211 154 50       574 $fh->seek(0, 0) or die "seek: $!\n";
212 154 50       1801 $fh->truncate(0) or die "truncate: $!\n";
213 154         5190 $fh;
214             }
215              
216             ##
217             ## Remove an encoding layer pushed on the handle. Re-opening a
218             ## filehandle over a dup (open FH, '>&', ...) keeps the existing
219             ## layer stack, so without this the layer pushed for each execution
220             ## would accumulate indefinitely.
221             ## See https://github.com/kaz-utashiro/perl-perlio-leak-bench
222             ##
223             sub _pop_encoding {
224 88     88   92 my $fh = shift;
225 88         333 my @layers = PerlIO::get_layers($fh);
226 88 50       804 if (grep { defined && /^encoding\b/ } @layers[-2 .. -1]) {
  176 50       709  
227 88         456 binmode $fh, ':pop';
228             }
229             }
230              
231             sub _utf8_flagged {
232 117     117   145 my $fh = shift;
233 117         586 !! grep { $_ eq 'utf8' } PerlIO::get_layers($fh);
  234         448  
234             }
235              
236             ##
237             ## Undo the layer change made for the execution: pop the :encoding
238             ## layer (non-raw), or clear the :utf8 flag if we set it (raw).
239             ##
240             sub _restore_encoding {
241 117     117   220 my($fh, $raw, $was_utf8) = @_;
242 117 100       150 if ($raw) {
243 29 50       96 binmode $fh, ':bytes' unless $was_utf8;
244             } else {
245 88         153 _pop_encoding($fh);
246             }
247             }
248              
249             sub _execute_in_place {
250 81     81   87 my $obj = shift;
251 81         83 my $command = shift;
252 81         146 my %opt = @_;
253 81         109 my @command = @$command;
254 81   100     243 my $stderr_mode = $opt{stderr} // '';
255 81         97 my $raw = $opt{raw};
256              
257 81         89 my $code = shift @command;
258              
259 81         135 my $tmp_stdout = $obj->_tmpfile('NOFORK_STDOUT', raw => $raw);
260              
261             # Save and redirect STDOUT (always needed)
262 81 50       1299 open my $save_stdout, '>&', \*STDOUT or die "dup STDOUT: $!\n";
263 81         195 my $stdout_was_utf8 = _utf8_flagged(\*STDOUT);
264 81 50       957 open STDOUT, '>&', $tmp_stdout or die "redirect STDOUT: $!\n";
265 81 100       3176 binmode STDOUT, $raw ? ':utf8' : ':encoding(utf8)';
266              
267             # Handle STDERR — only save/redirect when needed
268 81         1241 my ($save_stderr, $tmp_stderr);
269 81 100       189 if ($stderr_mode eq 'redirect') {
    100          
270 3 50       48 open $save_stderr, '>&', \*STDERR or die "dup STDERR: $!\n";
271 3 50       18 open STDERR, '>&', \*STDOUT or die "redirect STDERR: $!\n";
272             } elsif ($stderr_mode eq 'capture') {
273 9         21 $tmp_stderr = $obj->_tmpfile('NOFORK_STDERR', raw => $raw);
274 9 50       129 open $save_stderr, '>&', \*STDERR or die "dup STDERR: $!\n";
275 9 50       63 open STDERR, '>&', $tmp_stderr or die "redirect STDERR: $!\n";
276             }
277              
278             # Handle STDIN — only save/redirect when needed
279 81         494 my $save_stdin;
280             my $stdin_was_utf8;
281 81 100       271 if (exists $opt{stdin}) {
    100          
282 13         32 my $tmp_stdin = $obj->_tmpfile('NOFORK_STDIN', raw => $raw);
283 13         43 $tmp_stdin->print($opt{stdin});
284 13 50       101 $tmp_stdin->seek(0, 0) or die "seek: $!\n";
285 13 50       687 open $save_stdin, '<&', \*STDIN or die "dup STDIN: $!\n";
286 13         35 $stdin_was_utf8 = _utf8_flagged(\*STDIN);
287 13 50       125 open STDIN, '<&', $tmp_stdin or die "redirect STDIN: $!\n";
288 13 100       392 binmode STDIN, $raw ? ':utf8' : ':encoding(utf8)';
289             } elsif (my $input = $obj->{INPUT}) {
290 23 50       54 $input->seek(0, 0) or die "seek: $!\n";
291 23 50       479 open $save_stdin, '<&', \*STDIN or die "dup STDIN: $!\n";
292 23         40 $stdin_was_utf8 = _utf8_flagged(\*STDIN);
293 23 50       63 open STDIN, '<&', $input->fileno or die "redirect STDIN: $!\n";
294 23 100       449 binmode STDIN, $raw ? ':utf8' : ':encoding(utf8)';
295             }
296              
297             # Set global state
298 81         475 local $_;
299 81         152 local @ARGV = @command;
300 81         81 my $orig_0;
301 81 50       159 if (my $name = code_name($code)) {
302 81         162 $orig_0 = $0;
303 81         558 $0 = $name;
304             }
305              
306             # Execute
307 81         110 my $result = 0;
308 81         104 eval { $code->(@command) };
  81         199  
309 81 100       3529 if ($@) {
310 6         6 $result = -1;
311             }
312              
313             # Flush and restore — only what was redirected. Undo the layer
314             # change made above; restoring by re-open keeps the layer stack,
315             # so it would persist (and :encoding would accumulate on every
316             # execution) otherwise.
317 81         416 STDOUT->flush;
318 81         250 _restore_encoding(\*STDOUT, $raw, $stdout_was_utf8);
319 81 50       1275 open STDOUT, '>&', $save_stdout or die "restore STDOUT: $!\n";
320 81 100       157 if ($save_stderr) {
321 12         45 STDERR->flush;
322 12 50       108 open STDERR, '>&', $save_stderr or die "restore STDERR: $!\n";
323             }
324 81 100       115 if ($save_stdin) {
325 36         69 _restore_encoding(\*STDIN, $raw, $stdin_was_utf8);
326 36 50       520 open STDIN, '<&', $save_stdin or die "restore STDIN: $!\n";
327             }
328 81 50       118 if (defined $orig_0) {
329 81         355 $0 = $orig_0;
330             }
331              
332             # Read captured output from tmpfiles
333 81 50       279 $tmp_stdout->seek(0, 0) or die "seek: $!\n";
334 81         654 my $stdout_data = do { local $/; <$tmp_stdout> };
  81         244  
  81         2084  
335              
336 81         869 my $stderr_data = '';
337 81 100       134 if ($tmp_stderr) {
338 9 50       24 $tmp_stderr->seek(0, 0) or die "seek: $!\n";
339 9         60 $stderr_data = do { local $/; <$tmp_stderr> };
  9         15  
  9         174  
340             }
341              
342             return {
343 81         1241 result => $result,
344             data => $stdout_data,
345             error => $stderr_data,
346             };
347             }
348              
349             sub data {
350 25     25 1 1008 my $obj = shift;
351 25 50       205 if (@_) {
352 0         0 my $data = shift;
353 0         0 $obj->{RESULT}->{data} = $data;
354 0         0 my $fh = $obj->fh;
355 0 0       0 $fh->seek(0, 0) or die "seek: $!\n";
356 0 0       0 $fh->truncate(0) or die "truncate: $!\n";
357 0         0 $fh->print($data);
358 0         0 $fh->flush;
359 0 0       0 $fh->seek(0, 0) or die "seek: $!\n";
360 0         0 return $obj;
361             }
362 25         419 $obj->{RESULT}->{data};
363             }
364              
365             sub error {
366 9     9 1 23 my $obj = shift;
367 9         46 $obj->{RESULT}->{error};
368             }
369              
370             sub date {
371 173     173 1 7633 my $obj = shift;
372 173 100       1156 @_ ? $obj->{DATE} = shift : $obj->{DATE};
373             }
374              
375             sub _set_stdin {
376 51     51   89 my $obj = shift;
377 51         91 my $data = shift;
378 51         226 my $input = $obj->_tmpfile('INPUT');
379 51 50       218 $input->fcntl(F_SETFD, 0) or die "fcntl F_SETFD: $!\n";
380 51         638 $input->print($data);
381 51 50       428 $input->seek(0, 0) or die "seek: $!\n";
382 51         2546 $obj;
383             }
384              
385             sub with {
386 29     29 1 54 my $obj = shift;
387 29         169 $obj->configure(@_);
388             }
389              
390             1;
391              
392             __END__