зеркало из https://github.com/mozilla/pjs.git
454 строки
15 KiB
Perl
454 строки
15 KiB
Perl
package MozBuild::Util;
|
|
|
|
use strict;
|
|
|
|
use File::Path;
|
|
use IO::Handle;
|
|
use IO::Select;
|
|
use IPC::Open3;
|
|
use POSIX qw(:sys_wait_h);
|
|
use File::Temp qw(tempfile);
|
|
use Cwd;
|
|
|
|
use base qw(Exporter);
|
|
|
|
our @EXPORT_OK = qw(RunShellCommand MkdirWithPath HashFile DownloadFile Email);
|
|
|
|
my $DEFAULT_EXEC_TIMEOUT = 600;
|
|
my $EXEC_IO_READINCR = 1000;
|
|
my $EXEC_REAP_TIMEOUT = 10;
|
|
|
|
# RunShellCommand is a safe, performant way that handles all the gotchas of
|
|
# spawning a simple shell command. It's meant to replace backticks and open()s,
|
|
# while providing flexibility to handle stdout and stderr differently, provide
|
|
# timeouts to stop runaway programs, and provide easy-to-obtain information
|
|
# about return values, signals, output, and the like.
|
|
#
|
|
# Arguments:
|
|
# command - string: the command to run
|
|
# args - optional array ref: list of arguments to an array
|
|
# logfile - optional string: logfile to copy output to
|
|
# timeout - optional int: timeout value, in seconds, to wait for a command;
|
|
# defaults to ten minutes; set to 0 for no timeout
|
|
# redirectStderr - bool, default true: redirect child's stderr onto stdout
|
|
# stream?
|
|
# appendLogfile - bool, default true: append to the logfile (as opposed to
|
|
# overwriting it?)
|
|
# printOutputImmedaitely - bool, default false: print the output here to
|
|
# whatever is currently defined as *STDOUT?
|
|
# background - bool, default false: spawn a command and return all the info
|
|
# the caller needs to handle the program; assumes the caller takes
|
|
# complete responsibility for waitpid(), handling of the stdout/stderr
|
|
# IO::Handles, etc.
|
|
#
|
|
|
|
sub RunShellCommand {
|
|
my %args = @_;
|
|
|
|
my $shellCommand = $args{'command'};
|
|
die('ASSERT: RunShellCommand(): Empty command.')
|
|
if (not(defined($shellCommand)) || $shellCommand =~ /^\s+$/);
|
|
my $commandArgs = $args{'args'};
|
|
die('ASSERT: RunShellCommand(): commandArgs not an array ref.')
|
|
if (defined($commandArgs) && ref($commandArgs) ne 'ARRAY');
|
|
|
|
my $logfile = $args{'logfile'};
|
|
|
|
# Optional
|
|
my $timeout = exists($args{'timeout'}) ?
|
|
int($args{'timeout'}) : $DEFAULT_EXEC_TIMEOUT;
|
|
my $redirectStderr = exists($args{'redirectStderr'}) ?
|
|
$args{'redirectStderr'} : 1;
|
|
my $appendLogfile = exists($args{'appendLog'}) ? $args{'appendLog'} : 1;
|
|
my $printOutputImmediately = exists($args{'output'}) ? $args{'output'} : 0;
|
|
my $background = exists($args{'bg'}) ? $args{'bg'} : 0;
|
|
my $changeToDir = exists($args{'dir'}) ? $args{'dir'} : undef;
|
|
|
|
# This is a compatibility check for the old calling convention;
|
|
if ($shellCommand =~ /\s/) {
|
|
$shellCommand =~ s/^\s+//;
|
|
$shellCommand =~ s/\s+$//;
|
|
|
|
die("ASSERT: old RunShellCommand() calling convention detected\n")
|
|
if ($shellCommand =~ /\s+/);
|
|
}
|
|
|
|
# Glob the command together to check for 2>&1 constructs...
|
|
my $entireCommand = $shellCommand .
|
|
(defined($commandArgs) ? join (' ', @{$commandArgs}) : '');
|
|
|
|
# If we see 2>&1 in the command, set redirectStderr (overriding the option
|
|
# itself, and remove the construct from the command and arguments.
|
|
if ($entireCommand =~ /2\>\&1/) {
|
|
$redirectStderr = 1;
|
|
$shellCommand =~ s/2\>\&1//g;
|
|
if (defined($commandArgs)) {
|
|
for (my $i = 0; $i < scalar(@{$commandArgs}); $i++) {
|
|
$commandArgs->[$i] =~ s/2\>\&1//g;
|
|
}
|
|
}
|
|
}
|
|
|
|
local $_;
|
|
|
|
chomp($shellCommand);
|
|
|
|
my $cwd = getcwd();
|
|
my $exitValue = undef;
|
|
my $signalNum = undef;
|
|
my $sigName = undef;
|
|
my $dumpedCore = undef;
|
|
my $childEndedTime = undef;
|
|
my $timedOut = 0;
|
|
my $output = '';
|
|
my $childPid = 0;
|
|
my $childStartedTime = 0;
|
|
my $childReaped = 0;
|
|
my $prevStdoutBufferingSetting = 0;
|
|
local *LOGFILE;
|
|
|
|
if ($printOutputImmediately) {
|
|
# We Only end up doing this if it's requested that we're going to print
|
|
# output immediately. Additionally, we can't call autoflush() on STDOUT
|
|
# here, because doing so automatically sets it to on (gee, thanks);
|
|
# $| is the only way to get the value.
|
|
my $prevFd = select(STDOUT);
|
|
$prevStdoutBufferingSetting = $|;
|
|
select($prevFd);
|
|
STDOUT->autoflush(1);
|
|
}
|
|
|
|
if (defined($changeToDir)) {
|
|
chdir($changeToDir) or die("RunShellCommand(): failed to chdir() to "
|
|
. "$changeToDir\n");
|
|
}
|
|
|
|
eval {
|
|
local $SIG{'ALRM'} = sub { die("alarm\n") };
|
|
local $SIG{'PIPE'} = sub { die("pipe\n") };
|
|
|
|
my @execCommand = ($shellCommand);
|
|
push(@execCommand, @{$commandArgs}) if (defined($commandArgs) &&
|
|
scalar(@{$commandArgs} > 0));
|
|
|
|
my $childIn = new IO::Handle();
|
|
my $childOut = new IO::Handle();
|
|
my $childErr = new IO::Handle();
|
|
|
|
alarm($timeout);
|
|
$childStartedTime = time();
|
|
|
|
$childPid = open3($childIn, $childOut, $childErr, @execCommand);
|
|
$childIn->close();
|
|
|
|
if ($args{'background'}) {
|
|
alarm(0);
|
|
|
|
# Restore external state
|
|
chdir($cwd) if (defined($changeToDir));
|
|
if ($printOutputImmediately) {
|
|
my $prevFd = select(STDOUT);
|
|
$| = $prevStdoutBufferingSetting;
|
|
select($prevFd);
|
|
}
|
|
|
|
return { startTime => $childStartedTime,
|
|
endTime => undef,
|
|
timedOut => $timedOut,
|
|
exitValue => $exitValue,
|
|
sigNum => $signalNum,
|
|
output => undef,
|
|
dumpedCore => $dumpedCore,
|
|
pid => $childPid,
|
|
stdout => $childOut,
|
|
stderr => $childErr };
|
|
}
|
|
|
|
if (defined($logfile)) {
|
|
my $openArg = $appendLogfile ? '>>' : '>';
|
|
open(LOGFILE, $openArg . $logfile) or
|
|
die('Could not ' . $appendLogfile ? 'append' : 'open' .
|
|
" logfile $logfile: $!");
|
|
LOGFILE->autoflush(1);
|
|
}
|
|
|
|
my $childSelect = new IO::Select();
|
|
$childSelect->add($childErr);
|
|
$childSelect->add($childOut);
|
|
|
|
# Should be safe to call can_read() in blocking mode, since,
|
|
# IF NOTHING ELSE, the alarm() we set will catch a program that
|
|
# fails to finish executing within the timeout period.
|
|
|
|
while (my @ready = $childSelect->can_read()) {
|
|
foreach my $fh (@ready) {
|
|
my $line = undef;
|
|
my $rv = $fh->sysread($line, $EXEC_IO_READINCR);
|
|
|
|
# Check for read()ing nothing, and getting errors...
|
|
if (not defined($rv)) {
|
|
warn "sysread() failed with: $!\n";
|
|
next;
|
|
}
|
|
|
|
# If we didn't get anything from the read() and the child is
|
|
# dead, we've probably exhausted the buffer, and can stop
|
|
# trying...
|
|
if (0 == $rv) {
|
|
$childSelect->remove($fh) if ($childReaped);
|
|
next;
|
|
}
|
|
|
|
# This check is down here instead of up above because if we're
|
|
# throwing away stderr, we want to empty out the buffer, so
|
|
# the pipe isn't constantly readable. So, sysread() stderr,
|
|
# alas, only to throw it away.
|
|
next if (not($redirectStderr) && ($fh == $childErr));
|
|
|
|
$output .= $line;
|
|
print STDOUT $line if ($printOutputImmediately);
|
|
print LOGFILE $line if (defined($logfile));
|
|
}
|
|
|
|
if (!$childReaped && (waitpid($childPid, WNOHANG) > 0)) {
|
|
alarm(0);
|
|
$childEndedTime = time();
|
|
$exitValue = WEXITSTATUS($?);
|
|
$signalNum = WIFSIGNALED($?) && WTERMSIG($?);
|
|
$dumpedCore = WIFSIGNALED($?) && ($? & 128);
|
|
$childReaped = 1;
|
|
}
|
|
}
|
|
|
|
die('ASSERT: RunShellCommand(): stdout handle not empty')
|
|
if ($childOut->sysread(undef, $EXEC_IO_READINCR) != 0);
|
|
die('ASSERT: RunShellCommand(): stderr handle not empty')
|
|
if ($childErr->sysread(undef, $EXEC_IO_READINCR) != 0);
|
|
};
|
|
|
|
if (defined($logfile)) {
|
|
close(LOGFILE) or die("Could not close logfile $logfile: $!");
|
|
}
|
|
|
|
if ($@) {
|
|
if ($@ eq "alarm\n") {
|
|
$timedOut = 1;
|
|
if ($childReaped) {
|
|
die('ASSERT: RunShellCommand(): timed out, but child already '.
|
|
'reaped?');
|
|
}
|
|
|
|
if (kill('KILL', $childPid) != 1) {
|
|
warn("SIGKILL to timed-out child $childPid failed: $!\n");
|
|
}
|
|
|
|
# Processes get 10 seconds to obey.
|
|
eval {
|
|
local $SIG{'ALRM'} = sub { die("alarm\n") };
|
|
alarm($EXEC_REAP_TIMEOUT);
|
|
my $waitRv = waitpid($childPid, 0);
|
|
alarm(0);
|
|
# Don't fill in these values if they're bogus.
|
|
if ($waitRv > 0) {
|
|
$exitValue = WEXITSTATUS($?);
|
|
$signalNum = WIFSIGNALED($?) && WTERMSIG($?);
|
|
$dumpedCore = WIFSIGNALED($?) && ($? & 128);
|
|
}
|
|
};
|
|
} else {
|
|
warn "Error running $shellCommand: $@\n";
|
|
$output = $@;
|
|
}
|
|
}
|
|
|
|
# Restore external state
|
|
chdir($cwd) if (defined($changeToDir));
|
|
if ($printOutputImmediately) {
|
|
my $prevFd = select(STDOUT);
|
|
$| = $prevStdoutBufferingSetting;
|
|
select($prevFd);
|
|
}
|
|
|
|
return { startTime => $childStartedTime,
|
|
endTime => $childEndedTime,
|
|
timedOut => $timedOut,
|
|
exitValue => $exitValue,
|
|
sigNum => $signalNum,
|
|
output => $output,
|
|
dumpedCore => $dumpedCore
|
|
};
|
|
}
|
|
|
|
## This is a wrapper function to get easy true/false return values from a
|
|
## mkpath()-like function. mkpath() *actually* returns the list of directories
|
|
## it created in the pursuit of your request, and keeps its actual success
|
|
## status in $@.
|
|
|
|
sub MkdirWithPath {
|
|
my %args = @_;
|
|
|
|
my $dirToCreate = $args{'dir'};
|
|
my $printProgress = $args{'printProgress'};
|
|
my $dirMask = undef;
|
|
|
|
# Renamed this argument, since "mask" makes more sense; it takes
|
|
# precedence over the older argument name.
|
|
if (exists($args{'mask'})) {
|
|
$dirMask = $args{'mask'};
|
|
} elsif (exists($args{'dirMask'})) {
|
|
$dirMask = $args{'dirMask'};
|
|
}
|
|
|
|
die("ASSERT: MkdirWithPath() needs an arg") if not defined($dirToCreate);
|
|
|
|
## Defaults based on what mkpath does...
|
|
$printProgress = defined($printProgress) ? $printProgress : 0;
|
|
$dirMask = defined($dirMask) ? $dirMask : 0777;
|
|
|
|
eval { mkpath($dirToCreate, $printProgress, $dirMask) };
|
|
return defined($@);
|
|
}
|
|
|
|
sub HashFile {
|
|
my %args = @_;
|
|
die "ASSERT: HashFile(): null file\n" if (not defined($args{'file'}));
|
|
|
|
my $fileToHash = $args{'file'};
|
|
my $hashFunction = lc($args{'type'}) || 'md5';
|
|
my $dumpOutput = $args{'output'} || 0;
|
|
my $ignoreErrors = $args{'ignoreErrors'} || 0;
|
|
|
|
die 'ASSERT: HashFile(): invalid hashFunction; use md5/sha1: ' .
|
|
"$hashFunction\n" if
|
|
($hashFunction ne 'md5' && $hashFunction ne 'sha1');
|
|
|
|
if (not(-f $fileToHash) || not(-r $fileToHash)) {
|
|
if ($ignoreErrors) {
|
|
return '';
|
|
} else {
|
|
die "ASSERT: HashFile(): unusable/unreadable file to hash\n";
|
|
}
|
|
}
|
|
|
|
# We use openssl because that's pretty much guaranteed to be on all the
|
|
# platforms we want; md5sum and sha1sum aren't.
|
|
my $rv = RunShellCommand(command => 'openssl',
|
|
args => ['dgst', "-$hashFunction",
|
|
$fileToHash, ],
|
|
output => $dumpOutput);
|
|
|
|
if ($rv->{'timedOut'} || $rv->{'exitValue'} != 0) {
|
|
if ($ignoreErrors) {
|
|
return '';
|
|
} else {
|
|
die("MozUtil::HashFile(): hash call failed: $rv->{'exitValue'}\n");
|
|
}
|
|
}
|
|
|
|
my $hashValue = $rv->{'output'};
|
|
chomp($hashValue);
|
|
|
|
# Expects input like MD5(mozconfig)= d7433cc4204b4f3c65d836fe483fa575
|
|
# Removes everything up to and including the "= "
|
|
$hashValue =~ s/^.+\s+(\w+)$/$1/;
|
|
return $hashValue;
|
|
}
|
|
|
|
sub Email {
|
|
my %args = @_;
|
|
|
|
my $from = $args{'from'};
|
|
my $to = $args{'to'};
|
|
my $ccList = $args{'cc'} ? $args{'cc'} : undef;
|
|
my $subject = $args{'subject'};
|
|
my $message = $args{'message'};
|
|
my $sendmail = $args{'sendmail'};
|
|
my $blat = $args{'blat'};
|
|
|
|
if (not defined($from)) {
|
|
die("ASSERT: MozBuild::Utils::Email(): from is required");
|
|
} elsif (not defined($to)) {
|
|
die("ASSERT: MozBuild::Utils::Email(): to is required");
|
|
} elsif (not defined($subject)) {
|
|
die("ASSERT: MozBuild::Utils::Email(): subject is required");
|
|
} elsif (not defined($message)) {
|
|
die("ASSERT: MozBuild::Utils::Email(): subject is required");
|
|
}
|
|
|
|
if (defined($ccList) and ref($ccList) ne 'ARRAY') {
|
|
die("ASSERT: MozBuild::Utils::Email(): ccList is not an array ref\n");
|
|
}
|
|
|
|
if (-f $sendmail) {
|
|
open(SENDMAIL, "|$sendmail -oi -t")
|
|
or die("MozBuild::Utils::Email(): Can't fork for sendmail: $!\n");
|
|
print SENDMAIL "From: $from\n";
|
|
print SENDMAIL "To: $to\n";
|
|
foreach my $cc (@{$ccList}) {
|
|
print SENDMAIL "CC: $cc\n";
|
|
}
|
|
print SENDMAIL "Subject: $subject\n\n";
|
|
print SENDMAIL "\n$message";
|
|
|
|
close(SENDMAIL);
|
|
} elsif(-f $blat) {
|
|
my ($mh, $mailfile) = tempfile(DIR => '.');
|
|
|
|
my $toList = $to;
|
|
foreach my $cc (@{$ccList}) {
|
|
$toList .= ',';
|
|
$toList .= $cc;
|
|
}
|
|
print $mh "\n$message";
|
|
close($mh) or die("MozBuild::Utils::Email(): could not close tempmail file $mailfile: $!");
|
|
|
|
my $rv = RunShellCommand(command => $blat,
|
|
args => [$mailfile, '-to', $toList,
|
|
'-subject', '"' . $subject . '"']);
|
|
if ($rv->{'timedOut'} || $rv->{'exitValue'} != 0) {
|
|
die("MozBuild::Utils::Email(): FAILED: $rv->{'exitValue'}," .
|
|
" output: $rv->{'output'}\n");
|
|
}
|
|
|
|
print "$rv->{'output'}\n";
|
|
|
|
} else {
|
|
die("MozBuild::Utils::Email(): ASSERT: cannot find $sendmail or $blat");
|
|
}
|
|
}
|
|
|
|
sub DownloadFile {
|
|
my %args = @_;
|
|
|
|
my $sourceUrl = $args{'url'};
|
|
|
|
die("ASSERT: DownloadFile() Invalid Source URL: $sourceUrl\n")
|
|
if (not(defined($sourceUrl)) || $sourceUrl !~ m|^http://|);
|
|
|
|
my @wgetArgs = ();
|
|
|
|
if (defined($args{'dest'})) {
|
|
push(@wgetArgs, ('-O', $args{'dest'}));
|
|
}
|
|
|
|
if (defined($args{'user'})) {
|
|
push(@wgetArgs, ('--http-user', $args{'user'}));
|
|
}
|
|
|
|
if (defined($args{'password'})) {
|
|
push(@wgetArgs, ('--http-password', $args{'password'}));
|
|
}
|
|
|
|
push(@wgetArgs, ('--progress=dot:mega', $sourceUrl));
|
|
|
|
my $rv = RunShellCommand(command => 'wget',
|
|
args => \@wgetArgs);
|
|
|
|
if ($rv->{'timedOut'} || $rv->{'exitValue'} != 0) {
|
|
die("DownloadFile(): FAILED: $rv->{'exitValue'}," .
|
|
" output: $rv->{'output'}\n");
|
|
}
|
|
}
|
|
|
|
1;
|