The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Connector::Proxy::Proc::SafeExec
#
# Connector class for running system commands
#
# Written by Martin Bartosch for the OpenXPKI project 2012
#
package Connector::Proxy::Proc::SafeExec;

use strict;
use warnings;
use English;
use Proc::SafeExec;
use File::Temp;
use Try::Tiny;
use Template;

use Data::Dumper;

use Moose;
extends 'Connector::Proxy';

has args => (
    is => 'rw',
    isa => 'ArrayRef[Str]',
    default => sub { [] },
    );

has timeout => (
    is => 'rw',
    isa => 'Int',
    default => 5,
    );

has chomp_output => (
    is => 'rw',
    isa => 'Bool',
    default => 1,
    );

has stdin => (
    is => 'rw',
    isa => 'Str|ArrayRef[Str]|Undef',
    );

has env => (
    is => 'rw',
    isa => 'HashRef[Str]',
    );

sub _build_config {
    my $self = shift;

    if (! -x $self->LOCATION()) {
	die("Specified system command is not executable: " . $self->LOCATION());
    }

    return 1;
}

# this method always returns the file contents, regardless of the specified
# key
sub get {
    my $self = shift;

    my @args = $self->_build_path( shift );
    my $template = Template->new(
	{
	});

    # compose a list of command arguments
    my $template_vars = {
        ARGS => \@args,
    };

    # process configured system command arguments and replace templates
    # in it with the passed arguments, accessible via [% ARGS.0 %]
    my @cmd_args;
    foreach my $item (@{$self->args()}) {
	my $value;
	$template->process(\$item, $template_vars, \$value) || die "Error processing argument template.";
	push @cmd_args, $value;
    }

    my %filehandles;

    my @feed_to_stdin;
    if (defined $self->stdin()) {
	my @raw_stdin_data;
	if (ref $self->stdin() eq '') {
	    push @raw_stdin_data, $self->stdin();
	} elsif (ref $self->stdin() eq 'ARRAY') {
	    push @raw_stdin_data, @{$self->stdin()};
	}
	foreach my $line (@raw_stdin_data) {
	    my $value;
	    $template->process(\$line, $template_vars, \$value) || die "Error processing stdin template.";
	    push @feed_to_stdin, $value;
	}

	# we have data to pipe to stdin, create a filehandle
	$filehandles{stdin} = 'new';
    }

    if (defined $self->env()) {
	if (ref $self->env() eq 'HASH') {
	    foreach my $key (keys %{$self->env()}) {
		my $value;
		$template->process(\$self->env()->{$key}, $template_vars, \$value) || die "Error processing environment template.";
		$ENV{$key} = $value;
	    }
	}
    }

    my $stdout = File::Temp->new();
    $filehandles{stdout} = \*$stdout;

    my $stderr = File::Temp->new();
    $filehandles{stderr} = \*$stderr;


    # compose the system command to execute
    my @cmd;
    push @cmd, $self->{LOCATION};
    push @cmd, @cmd_args;

    my $command = Proc::SafeExec->new(
	{
	    exec => \@cmd,
	    %filehandles,
	});
    try {
	local $SIG{ALRM} = sub { die "alarm\n" };
	if (scalar @feed_to_stdin) {
	    my $stdin = $command->stdin();
	    print $stdin join("\n", @feed_to_stdin);
	}
	alarm $self->timeout();
	$command->wait();
    } catch {
	if ($_ eq "alarm\n") {
	    die "System command timed out after " . $self->timeout() . " seconds";
	}
	die $_;
    } finally {
	alarm 0;
    };

    my $stderr_content = do {
	open my $fh, '<', $stderr->filename;
	local $INPUT_RECORD_SEPARATOR;
	<$fh>;
    };

    if ($command->exit_status() != 0) {
 	die "System command exited with return code " . ($command->exit_status() >> 8) . ". STDERR: $stderr_content";
    }

    my $stdout_content = do {
	open my $fh, '<', $stdout->filename;
	local $INPUT_RECORD_SEPARATOR;
	<$fh>;
    };

    if ($self->chomp_output()) {
	chomp $stdout_content;
    }

    return $stdout_content;
}

sub get_meta {
    my $self = shift;

    # If we have no path, we tell the caller that we are a connector
    my @path = $self->_build_path( shift );
    if (scalar @path == 0) {
        return { TYPE  => "connector" };
    }

    return {TYPE  => "scalar" };
}

sub exists {

    my $self = shift;

    # No path = connector root which always exists
    my @path = $self->_build_path( shift );
    if (scalar @path == 0) {
        return 1;
    }
    my $val;
    eval {
        $val = $self->get( \@path );
    };
    return defined $val;

}

no Moose;
__PACKAGE__->meta->make_immutable;

1;
__END__

=head1 Name

Connector::Builtin::System::Exec

=head1 Description