Capture::Tiny incompatible with Mason?

Viewed 62

As a general policy, I avoid backticks and instead rely on Capture::Tiny and system calls.

my ( $stdout, $stderr, @results ) = capture { system($command) };

However, I just ran into an exception internal to Capture::Tiny when used under HTML::Mason that I had not seen before

 Error from open(GLOB(0x555ef4527090), >&-1): Invalid argument at /usr/share/perl5/vendor_perl/Capture/Tiny.pm line 107.
    Capture::Tiny::_open(GLOB(0x555ef4527090), ">\\x{26}-1") called at /usr/share/perl5/vendor_perl/Capture/Tiny.pm line 194
    Capture::Tiny::_open_std(HASH(0x555ec25b12b8)) called at /usr/share/perl5/vendor_perl/Capture/Tiny.pm line 391
    Capture::Tiny::_capture_tee(1, 1, 0, 0, CODE(0x555f05d5ce70)) called at InternalModule.pm line 41
    ...
    InternalModule.pm called at /.../apache/htdocs/autohandler line 84
    HTML::Mason::Commands::__ANON__() called at /usr/share/perl5/vendor_perl/HTML/Mason/Component.pm line 135

This is a legacy system, so the module versions are a little old.

Doing some internals diving into Capture::Tiny shows this method is throwing the exception

sub _open {
  open $_[0], $_[1] or Carp::confess "Error from open(" . join(q{, }, @_) . "): $!";   # Line 107
  # _debug( "# open " . join( ", " , map { defined $_ ? _name($_) : 'undef' } @_ ) . " as " . fileno( $_[0] ) . "\n" );
}

And specifically, it's the opening of STDOUT that is calling _open and leading to the exception:

# In some cases we open all (prior to forking) and in others we only open
# the output handles (setting up redirection)
sub _open_std {
  my ($handles) = @_;
  _open \*STDIN, "<&" . fileno $handles->{stdin} if defined $handles->{stdin};
  _open \*STDOUT, ">&" . fileno $handles->{stdout} if defined $handles->{stdout};   # Line 194
  _open \*STDERR, ">&" . fileno $handles->{stderr} if defined $handles->{stderr};
}

I've attempted localizing *STDOUT, although it appears that Capture::Tiny already does this.

For now I'm concluding that I should just go with a solution like this

system("program args 1>program.stdout 2>program.stderr");

Is there something else I'm missing with regard to Capture::Tiny to make it work while under Mason?

1 Answers

There are a few benefits to using Capture::Tiny with a system call over backticks

  • Error conditions can be inspected and reported in more detail
  • Separate info for stdout and stderr
  • Nothing is saved to the disk (therefore risking sensitive info temporarily being on the file system)

However, in this case, I've decided to code a backup method in case Capture::Tiny is not available in any specific environment. If it isn't then we'll just output those streams to temp files and ingest them afterwords.

# Capture::Tiny has an incompatibility with Mason.  Therefore we simply need to
# detect if it's safe to use, and if not we should do another mechanism for
# capturing the results.
state $is_safe_to_use_capture_tiny = eval {
    capture sub { };
    1;
};

my ( $stdout, $stderr, @result );

if ($is_safe_to_use_capture_tiny) {
    ( $stdout, $stderr, @result ) = capture { system($command) };

} else {
    # Backup equivalent to Capture::Tiny
    #     There is some risk to this approach, because we are choosing to cache
    # the stdout and stderr results to a file temporarily.  If these results
    # of the system call include sensitive information, then it would exist on
    # disk momentarily.
    #     However, given this is just a backup method that is only needed in
    # the case of system calls under Mason which is front end servers that do not
    # have access to sensitive information, it's an okay sacrifice.

    my $stdout_file = tempfile();
    my $stderr_file = tempfile();

    @result = system(qq{$command 1>$stdout_file 2>$stderr_file});
    $stdout = $stdout_file->exists ? $stdout_file->slurp : '';
    $stderr = $stderr_file->exists ? $stderr_file->slurp : '';
}
Related