#line 1 # Copyright (c) 2009 by David Golden. All rights reserved. # Licensed under Apache License, Version 2.0 (the "License"). # You may not use this file except in compliance with the License. # A copy of the License was distributed with this file or you may obtain a # copy of the License from http://www.apache.org/licenses/LICENSE-2.0 package Capture::Tiny; use 5.006; use strict; use warnings; use Carp (); use Exporter (); use IO::Handle (); use File::Spec (); use File::Temp qw/tempfile tmpnam/; # Get PerlIO or fake it BEGIN { eval { require PerlIO; PerlIO->can('get_layers') } or *PerlIO::get_layers = sub { return () }; } our $VERSION = '0.08'; $VERSION = eval $VERSION; ## no critic our @ISA = qw/Exporter/; our @EXPORT_OK = qw/capture capture_merged tee tee_merged/; our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK ); my $IS_WIN32 = $^O eq 'MSWin32'; our $DEBUG = $ENV{PERL_CAPTURE_TINY_DEBUG}; my $DEBUGFH; open $DEBUGFH, ">&STDERR" if $DEBUG; *_debug = $DEBUG ? sub(@) { print {$DEBUGFH} @_ } : sub(){0}; #--------------------------------------------------------------------------# # command to tee output -- the argument is a filename that must # be opened to signal that the process is ready to receive input. # This is annoying, but seems to be the best that can be done # as a simple, portable IPC technique #--------------------------------------------------------------------------# my @cmd = ($^X, '-e', '$SIG{HUP}=sub{exit}; ' . 'if( my $fn=shift ){ open my $fh, qq{>$fn}; print {$fh} $$; close $fh;} ' . 'my $buf; while (sysread(STDIN, $buf, 2048)) { ' . 'syswrite(STDOUT, $buf); syswrite(STDERR, $buf)}' ); #--------------------------------------------------------------------------# # filehandle manipulation #--------------------------------------------------------------------------# sub _relayer { my ($fh, $layers) = @_; _debug("# requested layers (@{$layers}) to $fh\n"); my %seen = ( unix => 1, perlio => 1 ); # filter these out my @unique = grep { !$seen{$_}++ } @$layers; _debug("# applying unique layers (@unique) to $fh\n"); binmode($fh, join(":", ":raw", @unique)); } sub _name { my $glob = shift; no strict 'refs'; ## no critic return *{$glob}{NAME}; } sub _open { open $_[0], $_[1] or Carp::confess "Error from open(" . join(q{, }, @_) . "): $!"; _debug( "# open " . join( ", " , map { defined $_ ? _name($_) : 'undef' } @_ ) . " as " . fileno( $_[0] ) . "\n" ); } sub _close { close $_[0] or Carp::confess "Error from close(" . join(q{, }, @_) . "): $!"; _debug( "# closed " . ( defined $_[0] ? _name($_[0]) : 'undef' ) . "\n" ); } my %dup; # cache this so STDIN stays fd0 my %proxy_count; sub _proxy_std { my %proxies; if ( ! defined fileno STDIN ) { $proxy_count{stdin}++; if (defined $dup{stdin}) { _open \*STDIN, "<&=" . fileno($dup{stdin}); _debug( "# restored proxy STDIN as " . (defined fileno STDIN ? fileno STDIN : 'undef' ) . "\n" ); } else { _open \*STDIN, "<" . File::Spec->devnull; _debug( "# proxied STDIN as " . (defined fileno STDIN ? fileno STDIN : 'undef' ) . "\n" ); _open $dup{stdin} = IO::Handle->new, "<&=STDIN"; } $proxies{stdin} = \*STDIN; binmode(STDIN, ':utf8') if $] >= 5.008; } if ( ! defined fileno STDOUT ) { $proxy_count{stdout}++; if (defined $dup{stdout}) { _open \*STDOUT, ">&=" . fileno($dup{stdout}); _debug( "# restored proxy STDOUT as " . (defined fileno STDOUT ? fileno STDOUT : 'undef' ) . "\n" ); } else { _open \*STDOUT, ">" . File::Spec->devnull; _debug( "# proxied STDOUT as " . (defined fileno STDOUT ? fileno STDOUT : 'undef' ) . "\n" ); _open $dup{stdout} = IO::Handle->new, ">&=STDOUT"; } $proxies{stdout} = \*STDOUT; binmode(STDOUT, ':utf8') if $] >= 5.008; } if ( ! defined fileno STDERR ) { $proxy_count{stderr}++; if (defined $dup{stderr}) { _open \*STDERR, ">&=" . fileno($dup{stderr}); _debug( "# restored proxy STDERR as " . (defined fileno STDERR ? fileno STDERR : 'undef' ) . "\n" ); } else { _open \*STDERR, ">" . File::Spec->devnull; _debug( "# proxied STDERR as " . (defined fileno STDERR ? fileno STDERR : 'undef' ) . "\n" ); _open $dup{stderr} = IO::Handle->new, ">&=STDERR"; } $proxies{stderr} = \*STDERR; binmode(STDERR, ':utf8') if $] >= 5.008; } return %proxies; } sub _unproxy { my (%proxies) = @_; _debug( "# unproxing " . join(" ", keys %proxies) . "\n" ); for my $p ( keys %proxies ) { $proxy_count{$p}--; _debug( "# unproxied " . uc($p) . " ($proxy_count{$p} left)\n" ); if ( ! $proxy_count{$p} ) { _close $proxies{$p}; _close $dup{$p} unless $] < 5.008; # 5.6 will have already closed this as dup delete $dup{$p}; } } } sub _copy_std { my %handles = map { $_, IO::Handle->new } qw/stdin stdout stderr/; _debug( "# copying std handles ...\n" ); _open $handles{stdin}, "<&STDIN"; _open $handles{stdout}, ">&STDOUT"; _open $handles{stderr}, ">&STDERR"; return \%handles; } sub _open_std { my ($handles) = @_; _open \*STDIN, "<&" . fileno $handles->{stdin}; _open \*STDOUT, ">&" . fileno $handles->{stdout}; _open \*STDERR, ">&" . fileno $handles->{stderr}; } #--------------------------------------------------------------------------# # private subs #--------------------------------------------------------------------------# sub _start_tee { my ($which, $stash) = @_; # setup pipes $stash->{$_}{$which} = IO::Handle->new for qw/tee reader/; pipe $stash->{reader}{$which}, $stash->{tee}{$which}; _debug( "# pipe for $which\: " . _name($stash->{tee}{$which}) . " " . fileno( $stash->{tee}{$which} ) . " => " . _name($stash->{reader}{$which}) . " " . fileno( $stash->{reader}{$which}) . "\n" ); select((select($stash->{tee}{$which}), $|=1)[0]); # autoflush # setup desired redirection for parent and child $stash->{new}{$which} = $stash->{tee}{$which}; $stash->{child}{$which} = { stdin => $stash->{reader}{$which}, stdout => $stash->{old}{$which}, stderr => $stash->{capture}{$which}, }; # flag file is used to signal the child is ready $stash->{flag_files}{$which} = scalar tmpnam(); # execute @cmd as a separate process if ( $IS_WIN32 ) { eval "use Win32API::File qw/CloseHandle GetOsFHandle SetHandleInformation fileLastError HANDLE_FLAG_INHERIT INVALID_HANDLE_VALUE/ "; _debug( "# Win32API::File loaded\n") unless $@; my $os_fhandle = GetOsFHandle( $stash->{tee}{$which} ); _debug( "# Couldn't get OS handle: " . fileLastError() . "\n") if ! defined $os_fhandle || $os_fhandle == INVALID_HANDLE_VALUE(); if ( SetHandleInformation( $os_fhandle, HANDLE_FLAG_INHERIT(), 0) ) { _debug( "# set no-inherit flag on $which tee\n" ); } else { _debug( "# can't disable tee handle flag inherit: " . fileLastError() . "\n"); } _open_std( $stash->{child}{$which} ); $stash->{pid}{$which} = system(1, @cmd, $stash->{flag_files}{$which}); # not restoring std here as it all gets redirected again shortly anyway } else { # use fork _fork_exec( $which, $stash ); } } sub _fork_exec { my ($which, $stash) = @_; my $pid = fork; if ( not defined $pid ) { Carp::confess "Couldn't fork(): $!"; } elsif ($pid == 0) { # child _debug( "# in child process ...\n" ); untie *STDIN; untie *STDOUT; untie *STDERR; _close $stash->{tee}{$which}; _debug( "# redirecting handles in child ...\n" ); _open_std( $stash->{child}{$which} ); _debug( "# calling exec on command ...\n" ); exec @cmd, $stash->{flag_files}{$which}; } $stash->{pid}{$which} = $pid } sub _files_exist { -f $_ || return 0 for @_; return 1 } sub _wait_for_tees { my ($stash) = @_; my $start = time; my @files = values %{$stash->{flag_files}}; 1 until _files_exist(@files) || (time - $start > 30); Carp::confess "Timed out waiting for subprocesses to start" if ! _files_exist(@files); unlink $_ for @files; } sub _kill_tees { my ($stash) = @_; if ( $IS_WIN32 ) { _debug( "# closing handles with CloseHandle\n"); CloseHandle( GetOsFHandle($_) ) for values %{ $stash->{tee} }; _debug( "# waiting for subprocesses to finish\n"); my $start = time; 1 until wait == -1 || (time - $start > 30); } else { _close $_ for values %{ $stash->{tee} }; waitpid $_, 0 for values %{ $stash->{pid} }; } } sub _slurp { seek $_[0],0,0; local $/; return scalar readline $_[0]; } #--------------------------------------------------------------------------# # _capture_tee() -- generic main sub for capturing or teeing #--------------------------------------------------------------------------# sub _capture_tee { _debug( "# starting _capture_tee with (@_)...\n" ); my ($tee_stdout, $tee_stderr, $merge, $code) = @_; # save existing filehandles and setup captures local *CT_ORIG_STDIN = *STDIN ; local *CT_ORIG_STDOUT = *STDOUT; local *CT_ORIG_STDERR = *STDERR; # find initial layers my %layers = ( stdin => [PerlIO::get_layers(\*STDIN) ], stdout => [PerlIO::get_layers(\*STDOUT)], stderr => [PerlIO::get_layers(\*STDERR)], ); _debug( "# existing layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/; # bypass scalar filehandles and tied handles my %localize; $localize{stdin}++, local(*STDIN) if grep { $_ eq 'scalar' } @{$layers{stdin}}; $localize{stdout}++, local(*STDOUT) if grep { $_ eq 'scalar' } @{$layers{stdout}}; $localize{stderr}++, local(*STDERR) if grep { $_ eq 'scalar' } @{$layers{stderr}}; $localize{stdout}++, local(*STDOUT), _open( \*STDOUT, ">&=1") if tied *STDOUT && $] >= 5.008; $localize{stderr}++, local(*STDERR), _open( \*STDERR, ">&=2") if tied *STDERR && $] >= 5.008; _debug( "# localized $_\n" ) for keys %localize; my %proxy_std = _proxy_std(); _debug( "# proxy std is @{ [%proxy_std] }\n" ); my $stash = { old => _copy_std() }; # update layers after any proxying %layers = ( stdin => [PerlIO::get_layers(\*STDIN) ], stdout => [PerlIO::get_layers(\*STDOUT)], stderr => [PerlIO::get_layers(\*STDERR)], ); _debug( "# post-proxy layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/; # get handles for capture and apply existing IO layers $stash->{new}{$_} = $stash->{capture}{$_} = File::Temp->new for qw/stdout stderr/; _debug("# will capture $_ on " .fileno($stash->{capture}{$_})."\n" ) for qw/stdout stderr/; # tees may change $stash->{new} _start_tee( stdout => $stash ) if $tee_stdout; _start_tee( stderr => $stash ) if $tee_stderr; _wait_for_tees( $stash ) if $tee_stdout || $tee_stderr; # finalize redirection $stash->{new}{stderr} = $stash->{new}{stdout} if $merge; $stash->{new}{stdin} = $stash->{old}{stdin}; _debug( "# redirecting in parent ...\n" ); _open_std( $stash->{new} ); # execute user provided code my ($exit_code, $error); { local *STDIN = *CT_ORIG_STDIN if $localize{stdin}; # get original, not proxy STDIN local *STDERR = *STDOUT if $merge; # minimize buffer mixups during $code _debug( "# finalizing layers ...\n" ); _relayer(\*STDOUT, $layers{stdout}); _relayer(\*STDERR, $layers{stderr}) unless $merge; _debug( "# running code $code ...\n" ); eval { $code->() }; $exit_code = $?; # save this for later $error = $@; # save this for later } # restore prior filehandles and shut down tees _debug( "# restoring ...\n" ); _open_std( $stash->{old} ); _close( $_ ) for values %{$stash->{old}}; # don't leak fds _unproxy( %proxy_std ); _kill_tees( $stash ) if $tee_stdout || $tee_stderr; # return captured output _relayer($stash->{capture}{stdout}, $layers{stdout}); _relayer($stash->{capture}{stderr}, $layers{stderr}) unless $merge; _debug( "# slurping captured $_ with layers: @{[PerlIO::get_layers($stash->{capture}{$_})]}\n") for qw/stdout stderr/; my $got_out = _slurp($stash->{capture}{stdout}); my $got_err = $merge ? q() : _slurp($stash->{capture}{stderr}); print CT_ORIG_STDOUT $got_out if $localize{stdout} && $tee_stdout; print CT_ORIG_STDERR $got_err if !$merge && $localize{stderr} && $tee_stdout; $? = $exit_code; die $error if $error; _debug( "# ending _capture_tee with (@_)...\n" ); return $got_out if $merge; return wantarray ? ($got_out, $got_err) : $got_out; } #--------------------------------------------------------------------------# # create API subroutines from [tee STDOUT flag, tee STDERR, merge flag] #--------------------------------------------------------------------------# my %api = ( capture => [0,0,0], capture_merged => [0,0,1], tee => [1,1,0], tee_merged => [1,0,1], # don't tee STDOUT since merging ); for my $sub ( keys %api ) { my $args = join q{, }, @{$api{$sub}}; eval "sub $sub(&) {unshift \@_, $args; goto \\&_capture_tee;}"; ## no critic } 1; __END__ #line 562