# File: ExecUtils.pm # Project: Anything # Description: Helper functions for executing external programs and scripts from perl. package ExecUtils; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(invoke stringify_arg stringify_args stringify_args2); use Fcntl qw(SEEK_SET); use File::Spec::Functions qw(catfile tmpdir); use File::Temp qw(tempfile); use Time::HiRes qw(gettimeofday tv_interval); # # Run a command and optionally save stderr/stdout # to a variable/file and optionally read stdin # from a variable/file # # Makes use of temporary files to do the reading/writing # from/to variables. # # PROG => program name # BIN => program directory # ARGS => reference to array of program arguments # IN_FILE => file name or handle to set as stdin # IN_VAR => variable (or reference to variable) to feed in as stdin # IN_NAME => the displayed name for the source of stdin # ALL_FILE => file name or handle to store stdout and stderr # ALL_VAR => reference to variable to store stdout and stderr # ALL_NAME => the displayed name for the destination of output # OUT_FILE => file name or handle to store stdout # OUT_VAR => reference to variable to store stdout # OUT_NAME => the displayed name for the destination of stdout # OUT_TRUNCATE => true to truncate non-error output files (defaults to TRUNCATE setting) # ERR_FILE => file name or handle to store stderr # ERR_VAR => reference to variable to store stderr # ERR_NAME => the displayed name for the destination of stderr # ERR_TRUNCATE => true to truncate error output files (defaults to TRUNCATE setting) # TRUNCATE => true to truncate output files if they exist (default true!) # TMPDIR => directory to create temporary files # NICE => the level of nicing to use (niceing disabled by default) # TIMEOUT => time (in seconds) to run child process before timing out # TIME => reference to store the running time in seconds (floating point) # CMD => reference to store a human readable form of the command run # OOT => reference to store if the command ran out of time (hit timeout) # CHECK_STATUS => true to die on bad status codes # sub invoke { my %opts = @_; my $logger = $opts{LOGGER}; $logger->trace("call invoke") if $logger; # check that the IO redirection options make sense die("Both IN_FILE and IN_VAR specified!") if (defined($opts{IN_FILE}) && defined($opts{IN_VAR})); die("Both ALL_FILE and ALL_VAR specified!") if (defined($opts{ALL_FILE}) && defined($opts{ALL_VAR})); if (defined($opts{ALL_FILE}) || defined($opts{ALL_VAR})) { die("OUT_FILE incompatible with ALL_xxx") if (defined($opts{OUT_FILE})); die("OUT_VAR incompatible with ALL_xxx") if (defined($opts{OUT_VAR})); die("ERR_FILE incompatible with ALL_xxx") if (defined($opts{ERR_FILE})); die("ERR_VAR incompatible with ALL_xxx") if (defined($opts{ERR_VAR})); } die("Both OUT_FILE and OUT_VAR specified!") if (defined($opts{OUT_FILE}) && defined($opts{OUT_VAR})); die("Both ERR_FILE and ERR_VAR specified!") if (defined($opts{ERR_FILE}) && defined($opts{ERR_VAR})); # nice level my @nice = (); if (defined($opts{NICE})) { if ($opts{NICE} !~ m/^[+-]?\d+$/ || $opts{NICE} < -20 || $opts{NICE} > 19) { die("Nice level not in range -20 to 19"); } push(@nice, 'nice', '-n', int($opts{NICE})); } # program die("No program passed to invoke") unless defined($opts{PROG}); my $exe = (defined($opts{BIN}) ? &catfile($opts{BIN}, $opts{PROG}) : $opts{PROG}); # program arguments my @args = (defined($opts{ARGS}) ? @{$opts{ARGS}} : ()); # timeout my $timeout = (defined($opts{TIMEOUT}) ? $opts{TIMEOUT} : 0); # pick temporary directory my $temp_dir = ($opts{TMPDIR} ? $opts{TMPDIR} : &tmpdir()); # should files be truncated before writing? my $truncate = (defined($opts{TRUNCATE}) ? $opts{TRUNCATE} : 1); my ($out_mode, $err_mode); if (defined($opts{OUT_TRUNCATE})) { $out_mode = ($opts{OUT_TRUNCATE} ? '>' : '>>'); } else { $out_mode = ($truncate ? '>' : '>>'); } if (defined($opts{ERR_TRUNCATE})) { $err_mode = ($opts{ERR_TRUNCATE} ? '>' : '>>'); } else { $err_mode = ($truncate ? '>' : '>>'); } # make a temporary file to hold the variable for standard input my ($in_var_fh, $in_var_fn); if (defined($opts{IN_VAR})) { ($in_var_fh, $in_var_fn) = &tempfile('stdin_XXXXXXXXXX', DIR => $temp_dir, UNLINK => 1); print $in_var_fh (ref($opts{IN_VAR}) ? ${$opts{IN_VAR}} : $opts{IN_VAR}); seek($in_var_fh, 0, SEEK_SET); # rewind file } # make a temporary file to hold the variable for standard output my ($out_var_fh, $out_var_fn); if (defined($opts{OUT_VAR})) { ($out_var_fh, $out_var_fn) = &tempfile('stdout_XXXXXXXXXX', DIR => $temp_dir, UNLINK => 1); } # make a temporary file to hold the variable for standard error my ($err_var_fh, $err_var_fn); if (defined($opts{ERR_VAR})) { ($err_var_fh, $err_var_fn) = &tempfile('stderr_XXXXXXXXXX', DIR => $temp_dir, UNLINK => 1); } # make a temporary file to hold the variable for standard out + err my ($all_var_fh, $all_var_fn); if (defined($opts{ALL_VAR})) { ($all_var_fh, $all_var_fn) = &tempfile('stdall_XXXXXXXXXX', DIR => $temp_dir, UNLINK => 1); } # record the start time my $t0 = [&gettimeofday()]; # now split into two processes my $pid = fork(); if ($pid == 0) { # child process # setup standard input redirect my $in_file = (defined($in_var_fh) ? $in_var_fh : $opts{IN_FILE}); if (defined($in_file)) { open(STDIN, (ref($in_file) ? '<&' : '<'), $in_file) or die("Can't redirect STDIN: $!"); } # the file to redirect all output to my $all_file = (defined($all_var_fh) ? $all_var_fh : $opts{ALL_FILE}); if (defined($all_file) && $truncate) { # can not use the truncate mode (as file opened twice) $out_mode = '>>'; $err_mode = '>>'; # so use the truncate method instead. truncate($all_file, 0); } # setup standard output redirect my $out_file = (defined($all_file) ? $all_file : (defined($out_var_fh) ? $out_var_fh : $opts{OUT_FILE})); if (defined($out_file)) { open(STDOUT, (ref($out_file) ? $out_mode.'&' : $out_mode), $out_file) or die("Can't redirect STDOUT: $!"); } # setup standard error redirect my $err_file = (defined($all_file) ? $all_file : (defined($err_var_fh) ? $err_var_fh : $opts{ERR_FILE})); if (defined($err_file)) { open(STDERR, (ref($err_file) ? $err_mode.'&' : $err_mode), $err_file) or die("Can't redirect STDERR: $!"); } # disable buffering if redirecting both to the same file so output order will remain the same if (defined($all_file)) { my $old_fh = select(STDOUT); $| = 1; select(STDERR); $| = 1; select($old_fh); } # run the program, this call shouldn't return unless it fails exec(@nice, $exe, @args); die("Exec failed"); } # parent process continues # now try to wait for the child process my $status = -1; my $oot = 0;#FALSE if (defined($pid)) { # fork worked # setup alarm handler local $SIG{ALRM} = sub { die("Timeout!\n"); }; eval { local $SIG{TERM} = sub { die("Exit!\n"); }; local $SIG{INT} = sub { die("Exit!\n"); }; alarm($timeout); # set alarm to stop us if the process takes too long waitpid($pid, 0); # wait for the child process to exit alarm(0); # clear the alarm }; if ($@) { if ($@ =~ /Timeout!/) { $oot = 1;#TRUE # tell child process to terminate my $SIGTERM = 15; kill($SIGTERM, $pid); # wait for it to quit waitpid($pid, 0); } elsif ($@ =~ /Exit!/) { # tell child process to terminate my $SIGTERM = 15; kill($SIGTERM, $pid); # wait for it to quit waitpid($pid, 0); # terminate exit(2); } else { die($@); # throw non-timeout related errors } } $status = $? } # record the end time my $t1 = [&gettimeofday()]; # check if the caller wants the elapsed time if (defined($opts{TIME})) { ${$opts{TIME}} = &tv_interval($t0, $t1); } # close and unlink input temporary file if (defined($in_var_fh)) { close($in_var_fh); unlink($in_var_fn); } # rewind, slurp, close and unlink output temporary files ${$opts{ALL_VAR}} = &rewind_slurp_close($all_var_fh, $all_var_fn) if $all_var_fh; ${$opts{OUT_VAR}} = &rewind_slurp_close($out_var_fh, $out_var_fn) if $out_var_fh; ${$opts{ERR_VAR}} = &rewind_slurp_close($err_var_fh, $err_var_fn) if $err_var_fh; # now check the status (if the caller requests it) if ($opts{CHECK_STATUS}) { if ($status == -1) { die("Failed to execute command '". &stringify_args2(%opts) . "'\n"); } elsif ($status & 127) { die(sprintf("Process executing command '%s' died with signal %d, %s coredump.", &stringify_args2(%opts), ($status & 127), ($status & 128) ? 'with' : 'without')); } elsif ($status != 0) { die(sprintf("Process executing command '%s' exited with value %d indicating failure.", &stringify_args2(%opts), $? >> 8)); } } # store a human readable version of the command if (defined($opts{CMD})) { ${$opts{CMD}} = &stringify_args2(%opts); } # store if the timeout occured if (defined($opts{OOT})) { ${$opts{OOT}} = $oot; } # finally return the status of the called program return $status; } # # stringify_arg # # Escapes and quotes an argument # sub stringify_arg { my ($argcpy) = @_; die("Undefined argument value!") unless defined $argcpy; # escape shell characters (Bourne shell specific) $argcpy =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; # quote string if it contains spaces $argcpy = "\"$argcpy\"" if $argcpy =~ m/\s/; return $argcpy; } # # stringify_args # # Convert an arguments array into a string in a way that should # not be ambiguous. Intended for logging. If you are invoking a # program you should still use the extended version of system # that takes an argument array. # sub stringify_args { my @dest = (); for (my $i = 0; $i < scalar(@_); $i++) { die("Undefined argument at position $i after \"" . ($i > 0 ? $_[$i-1] : '') . "\"") unless defined $_[$i]; push(@dest, &stringify_arg($_[$i])); } return join(' ', @dest); } # # stringify_args2 # # Convert arguments and redirects into a string # sub stringify_args2 { my %opts = @_; my $cmd = &stringify_args($opts{PROG}, @{$opts{ARGS}}); my $truncate = (defined($opts{TRUNCATE}) ? $opts{TRUNCATE} : 1); my $dir = ($truncate ? '>' : '>>'); if (defined($opts{IN_FILE}) || defined($opts{IN_VAR})) { my $name = '$input'; if (defined($opts{IN_FILE})) { $name = (ref($opts{IN_FILE}) ? 'input_file' : &stringify_arg($opts{IN_FILE})); } $name = $opts{IN_NAME} if defined($opts{IN_NAME}); $cmd .= ' < ' . $name; } if (defined($opts{ALL_FILE}) || defined($opts{ALL_VAR})) { my $name = '$all_messages'; if (defined($opts{ALL_FILE})) { $name = (ref($opts{ALL_FILE}) ? 'output_file' : &stringify_arg($opts{ALL_FILE})); } $name = $opts{ALL_NAME} if defined($opts{ALL_NAME}); $cmd .= ' &'. $dir . ' ' . $name; } else { if (defined($opts{OUT_FILE}) || defined($opts{OUT_VAR})) { my $name = '$output_messages'; if (defined($opts{OUT_FILE})) { $name = (ref($opts{OUT_FILE}) ? 'output_file' : &stringify_arg($opts{OUT_FILE})); } $name = $opts{OUT_NAME} if defined($opts{OUT_NAME}); $cmd .= ' 1'. $dir . ' ' . $name; } # check if we're redirecting stderr if (defined($opts{ERR_FILE}) || defined($opts{ERR_VAR})) { my $name = '$error_messages'; if (defined($opts{ERR_FILE})) { $name = (ref($opts{ERR_FILE}) ? 'error_file' : &stringify_arg($opts{ERR_FILE})); } $name = $opts{ERR_NAME} if defined($opts{ERR_NAME}); $cmd .= ' 2' . $dir . ' ' . $name; } } return $cmd; } sub rewind_slurp_close { my ($fh, $fn) = @_; return unless defined($fh); seek($fh, 0, SEEK_SET); my $content = do {local $/ = undef; <$fh>}; if (defined($fn)) { close($fh); unlink($fn); } else { close($fh); } return $content; }