File Coverage

inc/Capture/Tiny.pm
Criterion Covered Total %
statement 134 229 58.5
branch 26 100 26.0
condition 9 35 25.7
subroutine 21 29 72.4
pod 4 4 100.0
total 194 397 48.8


line stmt bran cond sub pod time code
1             #line 1
2             # Copyright (c) 2009 by David Golden. All rights reserved.
3             # Licensed under Apache License, Version 2.0 (the "License").
4             # You may not use this file except in compliance with the License.
5             # A copy of the License was distributed with this file or you may obtain a
6             # copy of the License from http://www.apache.org/licenses/LICENSE-2.0
7              
8 1     1   1401 package Capture::Tiny;
  1         4  
  1         43  
9 1     1   6 use 5.006;
  1         1  
  1         38  
10 1     1   5 use strict;
  1         2  
  1         34  
11 1     1   5 use warnings;
  1         2  
  1         25  
12 1     1   5 use Carp ();
  1         1  
  1         20  
13 1     1   1278 use Exporter ();
  1         12010  
  1         34  
14 1     1   12 use IO::Handle ();
  1         2  
  1         26  
15 1     1   2203 use File::Spec ();
  1         18925  
  1         119  
16             use File::Temp qw/tempfile tmpnam/;
17 1 50   1   2 # Get PerlIO or fake it
  0         0  
  1         12  
  1         361  
18             BEGIN { eval { require PerlIO; 1 } or *PerlIO::get_layers = sub { return () }; }
19              
20             our $VERSION = '0.06';
21             $VERSION = eval $VERSION; ## no critic
22             our @ISA = qw/Exporter/;
23             our @EXPORT_OK = qw/capture capture_merged tee tee_merged/;
24             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
25              
26             my $IS_WIN32 = $^O eq 'MSWin32';
27              
28             our $DEBUG = $ENV{PERL_CAPTURE_TINY_DEBUG};
29             my $DEBUGFH;
30             open $DEBUGFH, ">&STDERR" if $DEBUG;
31              
32             *_debug = $DEBUG ? sub(@) { print {$DEBUGFH} @_ } : sub(){0};
33              
34             #--------------------------------------------------------------------------#
35             # command to tee output -- the argument is a filename that must
36             # be opened to signal that the process is ready to receive input.
37             # This is annoying, but seems to be the best that can be done
38             # as a simple, portable IPC technique
39             #--------------------------------------------------------------------------#
40             my @cmd = ($^X, '-e', '$SIG{HUP}=sub{exit}; '
41             . 'if( my $fn=shift ){ open my $fh, qq{>$fn}; print {$fh} $$; close $fh;} '
42             . 'my $buf; while (sysread(STDIN, $buf, 2048)) { '
43             . 'syswrite(STDOUT, $buf); syswrite(STDERR, $buf)}'
44             );
45              
46             #--------------------------------------------------------------------------#
47             # filehandle manipulation
48             #--------------------------------------------------------------------------#
49              
50 4     4   15 sub _relayer {
51 4         11 my ($fh, $layers) = @_;
  4         68  
52 4         47 _debug("# requested layers (@{$layers}) to $fh\n");
53 4 50 66     9 my %seen;
  8         55  
54 4         24 my @unique = grep { $_ ne 'unix' and $_ ne 'perlio' and !$seen{$_}++ } @$layers;
55 4         55 _debug("# applying unique layers (@unique) to $fh\n");
56             binmode($fh, join(":", "", "raw", @unique));
57             }
58              
59 42     42   72 sub _name {
60 1     1   5 my $glob = shift;
  1         3  
  1         3165  
61 42         44 no strict 'refs'; ## no critic
  42         271  
62             return *{$glob}{NAME};
63             }
64              
65 18 50   18   499 sub _open {
66 18 50       53 open $_[0], $_[1] or Carp::confess "Error from open(" . join(q{, }, @_) . "): $!";
  36         94  
67             _debug( "# open " . join( ", " , map { defined $_ ? _name($_) : 'undef' } @_ ) . " as " . fileno( $_[0] ) . "\n" );
68             }
69              
70 6 50   6   57 sub _close {
71 6 50       30 close $_[0] or Carp::confess "Error from close(" . join(q{, }, @_) . "): $!";
72             _debug( "# closed " . ( defined $_[0] ? _name($_[0]) : 'undef' ) . "\n" );
73             }
74              
75             my %dup; # cache this so STDIN stays fd0
76             my %proxy_count;
77 2     2   3 sub _proxy_std {
78 2 50       7 my %proxies;
79 0         0 if ( ! defined fileno STDIN ) {
80 0 0       0 $proxy_count{stdin}++;
81 0         0 if (defined $dup{stdin}) {
82 0 0       0 _open \*STDIN, "<&=" . fileno($dup{stdin});
83             _debug( "# restored proxy STDIN as " . (defined fileno STDIN ? fileno STDIN : 'undef' ) . "\n" );
84             }
85 0         0 else {
86 0 0       0 _open \*STDIN, "<" . File::Spec->devnull;
87 0         0 _debug( "# proxied STDIN as " . (defined fileno STDIN ? fileno STDIN : 'undef' ) . "\n" );
88             _open $dup{stdin} = IO::Handle->new, "<&=STDIN";
89 0         0 }
90 0 0       0 $proxies{stdin} = \*STDIN;
91             binmode(STDIN, ':utf8') if $] >= 5.008;
92 2 50       7 }
93 0         0 if ( ! defined fileno STDOUT ) {
94 0 0       0 $proxy_count{stdout}++;
95 0         0 if (defined $dup{stdout}) {
96 0 0       0 _open \*STDOUT, ">&=" . fileno($dup{stdout});
97             _debug( "# restored proxy STDOUT as " . (defined fileno STDOUT ? fileno STDOUT : 'undef' ) . "\n" );
98             }
99 0         0 else {
100 0 0       0 _open \*STDOUT, ">" . File::Spec->devnull;
101 0         0 _debug( "# proxied STDOUT as " . (defined fileno STDOUT ? fileno STDOUT : 'undef' ) . "\n" );
102             _open $dup{stdout} = IO::Handle->new, ">&=STDOUT";
103 0         0 }
104 0 0       0 $proxies{stdout} = \*STDOUT;
105             binmode(STDOUT, ':utf8') if $] >= 5.008;
106 2 50       7 }
107 0         0 if ( ! defined fileno STDERR ) {
108 0 0       0 $proxy_count{stderr}++;
109 0         0 if (defined $dup{stderr}) {
110 0 0       0 _open \*STDERR, ">&=" . fileno($dup{stderr});
111             _debug( "# restored proxy STDERR as " . (defined fileno STDERR ? fileno STDERR : 'undef' ) . "\n" );
112             }
113 0         0 else {
114 0 0       0 _open \*STDERR, ">" . File::Spec->devnull;
115 0         0 _debug( "# proxied STDERR as " . (defined fileno STDERR ? fileno STDERR : 'undef' ) . "\n" );
116             _open $dup{stderr} = IO::Handle->new, ">&=STDERR";
117 0         0 }
118 0 0       0 $proxies{stderr} = \*STDERR;
119             binmode(STDERR, ':utf8') if $] >= 5.008;
120 2         6 }
121             return %proxies;
122             }
123              
124 2     2   8 sub _unproxy {
125 2         15 my (%proxies) = @_;
126 2         15 _debug( "# unproxing " . join(" ", keys %proxies) . "\n" );
127 0         0 for my $p ( keys %proxies ) {
128 0         0 $proxy_count{$p}--;
129 0 0       0 _debug( "# unproxied " . uc($p) . " ($proxy_count{$p} left)\n" );
130 0         0 if ( ! $proxy_count{$p} ) {
131 0 0       0 _close $proxies{$p};
132 0         0 _close $dup{$p} unless $] < 5.008; # 5.6 will have already closed this as dup
133             delete $dup{$p};
134             }
135             }
136             }
137              
138 2     2   5 sub _copy_std {
  6         216  
139 2         37 my %handles = map { $_, IO::Handle->new } qw/stdin stdout stderr/;
140 2         8 _debug( "# copying std handles ...\n" );
141 2         7 _open $handles{stdin}, "<&STDIN";
142 2         7 _open $handles{stdout}, ">&STDOUT";
143 2         8 _open $handles{stderr}, ">&STDERR";
144             return \%handles;
145             }
146              
147 4     4   16 sub _open_std {
148 4         71 my ($handles) = @_;
149 4         23 _open \*STDIN, "<&" . fileno $handles->{stdin};
150 4         27 _open \*STDOUT, ">&" . fileno $handles->{stdout};
151             _open \*STDERR, ">&" . fileno $handles->{stderr};
152             }
153              
154             #--------------------------------------------------------------------------#
155             # private subs
156             #--------------------------------------------------------------------------#
157              
158 0     0   0 sub _start_tee {
159             my ($which, $stash) = @_;
160 0         0 # setup pipes
161 0         0 $stash->{$_}{$which} = IO::Handle->new for qw/tee reader/;
162 0         0 pipe $stash->{reader}{$which}, $stash->{tee}{$which};
163             _debug( "# pipe for $which\: " . _name($stash->{tee}{$which}) . " "
164             . fileno( $stash->{tee}{$which} ) . " => " . _name($stash->{reader}{$which})
165 0         0 . " " . fileno( $stash->{reader}{$which}) . "\n" );
166             select((select($stash->{tee}{$which}), $|=1)[0]); # autoflush
167 0         0 # setup desired redirection for parent and child
168 0         0 $stash->{new}{$which} = $stash->{tee}{$which};
169             $stash->{child}{$which} = {
170             stdin => $stash->{reader}{$which},
171             stdout => $stash->{old}{$which},
172             stderr => $stash->{capture}{$which},
173             };
174 0         0 # flag file is used to signal the child is ready
175             $stash->{flag_files}{$which} = scalar tmpnam();
176 0 0       0 # execute @cmd as a separate process
177 0         0 if ( $IS_WIN32 ) {
178 0 0       0 eval "use Win32API::File qw/CloseHandle GetOsFHandle SetHandleInformation fileLastError HANDLE_FLAG_INHERIT INVALID_HANDLE_VALUE/ ";
179 0         0 _debug( "# Win32API::File loaded\n") unless $@;
180 0 0 0     0 my $os_fhandle = GetOsFHandle( $stash->{tee}{$which} );
181 0 0       0 _debug( "# Couldn't get OS handle: " . fileLastError() . "\n") if ! defined $os_fhandle || $os_fhandle == INVALID_HANDLE_VALUE();
182 0         0 if ( SetHandleInformation( $os_fhandle, HANDLE_FLAG_INHERIT(), 0) ) {
183             _debug( "# set no-inherit flag on $which tee\n" );
184             }
185 0         0 else {
186             _debug( "# can't disable tee handle flag inherit: " . fileLastError() . "\n");
187 0         0 }
188 0         0 _open_std( $stash->{child}{$which} );
189             $stash->{pid}{$which} = system(1, @cmd, $stash->{flag_files}{$which});
190             # not restoring std here as it all gets redirected again shortly anyway
191             }
192 0         0 else { # use fork
193             _fork_exec( $which, $stash );
194             }
195             }
196              
197 0     0   0 sub _fork_exec {
198 0         0 my ($which, $stash) = @_;
199 0 0       0 my $pid = fork;
    0          
200 0         0 if ( not defined $pid ) {
201             Carp::confess "Couldn't fork(): $!";
202             }
203 0         0 elsif ($pid == 0) { # child
204 0         0 _debug( "# in child process ...\n" );
  0         0  
  0         0  
205 0         0 untie *STDIN; untie *STDOUT; untie *STDERR;
206 0         0 _close $stash->{tee}{$which};
207 0         0 _debug( "# redirecting handles in child ...\n" );
208 0         0 _open_std( $stash->{child}{$which} );
209 0         0 _debug( "# calling exec on command ...\n" );
210             exec @cmd, $stash->{flag_files}{$which};
211 0         0 }
212             $stash->{pid}{$which} = $pid
213             }
214 0   0 0   0  
  0         0  
215             sub _files_exist { -f $_ || return 0 for @_; return 1 }
216              
217 0     0   0 sub _wait_for_tees {
218 0         0 my ($stash) = @_;
219 0         0 my $start = time;
  0         0  
220 0   0     0 my @files = values %{$stash->{flag_files}};
221 0 0       0 1 until _files_exist(@files) || (time - $start > 30);
222 0         0 Carp::confess "Timed out waiting for subprocesses to start" if ! _files_exist(@files);
223             unlink $_ for @files;
224             }
225              
226 0     0   0 sub _kill_tees {
227 0 0       0 my ($stash) = @_;
228 0         0 if ( $IS_WIN32 ) {
229 0         0 _debug( "# closing handles with CloseHandle\n");
  0         0  
230 0         0 CloseHandle( GetOsFHandle($_) ) for values %{ $stash->{tee} };
231 0         0 _debug( "# waiting for subprocesses to finish\n");
232 0   0     0 my $start = time;
233             1 until wait == -1 || (time - $start > 30);
234             }
235 0         0 else {
  0         0  
236 0         0 _close $_ for values %{ $stash->{tee} };
  0         0  
237             waitpid $_, 0 for values %{ $stash->{pid} };
238             }
239             }
240              
241 2     2   82 sub _slurp {
  2         23  
  2         109  
242             seek $_[0],0,0; local $/; return scalar readline $_[0];
243             }
244              
245             #--------------------------------------------------------------------------#
246             # _capture_tee() -- generic main sub for capturing or teeing
247             #--------------------------------------------------------------------------#
248              
249 2     2   19 sub _capture_tee {
250 2         9 _debug( "# starting _capture_tee with (@_)...\n" );
251             my ($tee_stdout, $tee_stderr, $merge, $code) = @_;
252 2         15 # save existing filehandles and setup captures
253 2         7 local *CT_ORIG_STDIN = *STDIN ;
254 2         7 local *CT_ORIG_STDOUT = *STDOUT;
255             local *CT_ORIG_STDERR = *STDERR;
256 2         34 # find initial layers
257             my %layers = (
258             stdin => [PerlIO::get_layers(\*STDIN) ],
259             stdout => [PerlIO::get_layers(\*STDOUT)],
260             stderr => [PerlIO::get_layers(\*STDERR)],
261 2         11 );
  6         26  
262             _debug( "# existing layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/;
263 2         4 # bypass scalar filehandles and tied handles
264 2 50       2 my %localize;
  4         13  
  2         5  
265 2 50       3 $localize{stdin}++, local(*STDIN) if grep { $_ eq 'scalar' } @{$layers{stdin}};
  4         10  
  2         7  
266 2 50       4 $localize{stdout}++, local(*STDOUT) if grep { $_ eq 'scalar' } @{$layers{stdout}};
  4         8  
  2         4  
267 2 50 33     9 $localize{stderr}++, local(*STDERR) if grep { $_ eq 'scalar' } @{$layers{stderr}};
268 2 50 33     8 $localize{stdout}++, local(*STDOUT), _open( \*STDOUT, ">&=1") if tied *STDOUT && $] >= 5.008;
269 2         5 $localize{stderr}++, local(*STDERR), _open( \*STDERR, ">&=2") if tied *STDERR && $] >= 5.008;
270 2         11 _debug( "# localized $_\n" ) for keys %localize;
271 2         3 my %proxy_std = _proxy_std();
  2         8  
272 2         7 _debug( "# proxy std is @{ [%proxy_std] }\n" );
273             my $stash = { old => _copy_std() };
274 2         42 # update layers after any proxying
275             %layers = (
276             stdin => [PerlIO::get_layers(\*STDIN) ],
277             stdout => [PerlIO::get_layers(\*STDOUT)],
278             stderr => [PerlIO::get_layers(\*STDERR)],
279 2         12 );
  6         25  
280             _debug( "# post-proxy layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/;
281 2         27 # get handles for capture and apply existing IO layers
282 2         2799 $stash->{new}{$_} = $stash->{capture}{$_} = File::Temp->new for qw/stdout stderr/;
283             _debug("# will capture $_ on " .fileno($stash->{capture}{$_})."\n" ) for qw/stdout stderr/;
284 2 50       10 # tees may change $stash->{new}
285 2 50       5 _start_tee( stdout => $stash ) if $tee_stdout;
286 2 50 33     19 _start_tee( stderr => $stash ) if $tee_stderr;
287             _wait_for_tees( $stash ) if $tee_stdout || $tee_stderr;
288 2 50       11 # finalize redirection
289 2         8 $stash->{new}{stderr} = $stash->{new}{stdout} if $merge;
290 2         6 $stash->{new}{stdin} = $stash->{old}{stdin};
291 2         8 _debug( "# redirecting in parent ...\n" );
292             _open_std( $stash->{new} );
293 2         4 # execute user provided code
294             my $exit_code;
295 2 50       3 {
  2         8  
296 2 50       6 local *STDIN = *CT_ORIG_STDIN if $localize{stdin}; # get original, not proxy STDIN
297 2         5 local *STDERR = *STDOUT if $merge; # minimize buffer mixups during $code
298 2         7 _debug( "# finalizing layers ...\n" );
299 2 50       6 _relayer(\*STDOUT, $layers{stdout});
300 2         10 _relayer(\*STDERR, $layers{stderr}) unless $merge;
301 2         7 _debug( "# running code $code ...\n" );
302 2         1158380 $code->();
303             $exit_code = $?; # save this for later
304             }
305 2         40 # restore prior filehandles and shut down tees
306 2         59 _debug( "# restoring ...\n" );
307 2         7 _open_std( $stash->{old} );
  2         31  
308 2         49 _close( $_ ) for values %{$stash->{old}}; # don't leak fds
309 2 50 33     37 _unproxy( %proxy_std );
310             _kill_tees( $stash ) if $tee_stdout || $tee_stderr;
311 2         21 # return captured output
312 2 50       8 _relayer($stash->{capture}{stdout}, $layers{stdout});
313 2         12 _relayer($stash->{capture}{stderr}, $layers{stderr}) unless $merge;
  4         48  
314 2         23 _debug( "# slurping captured $_ with layers: @{[PerlIO::get_layers($stash->{capture}{$_})]}\n") for qw/stdout stderr/;
315 2 50       14 my $got_out = _slurp($stash->{capture}{stdout});
316 2 50 33     15 my $got_err = $merge ? q() : _slurp($stash->{capture}{stderr});
317 2 0 33     10 print CT_ORIG_STDOUT $got_out if $localize{stdout} && $tee_stdout;
      33        
318 2         4 print CT_ORIG_STDERR $got_err if !$merge && $localize{stderr} && $tee_stdout;
319 2         16 $? = $exit_code;
320 2 50       158 _debug( "# ending _capture_tee with (@_)...\n" );
321 0 0       0 return $got_out if $merge;
322             return wantarray ? ($got_out, $got_err) : $got_out;
323             }
324              
325             #--------------------------------------------------------------------------#
326             # create API subroutines from [tee STDOUT flag, tee STDERR, merge flag]
327             #--------------------------------------------------------------------------#
328              
329             my %api = (
330             capture => [0,0,0],
331             capture_merged => [0,0,1],
332             tee => [1,1,0],
333             tee_merged => [1,0,1], # don't tee STDOUT since merging
334             );
335              
336             for my $sub ( keys %api ) {
337 0     0 1 0 my $args = join q{, }, @{$api{$sub}};
  0     2 1 0  
  2     0 1 5154  
  2     0 1 12  
  0            
  0            
  0            
  0            
338             eval "sub $sub(&) {unshift \@_, $args; goto \\&_capture_tee;}"; ## no critic
339             }
340              
341             1;
342              
343             __END__