* Made the build hook mechanism more efficient. Rather than starting

the hook every time we want to ask whether we can run a remote build
  (which can be very often), we now reuse a hook process for answering
  those queries until it accepts a build.  So if there are N
  derivations to be built, at most N hooks will be started.
This commit is contained in:
Eelco Dolstra 2010-08-25 20:44:28 +00:00
parent 1a396f3789
commit e437b08250
3 changed files with 332 additions and 346 deletions

View file

@ -31,57 +31,20 @@ $ENV{"DISPLAY"} = "";
$ENV{"SSH_ASKPASS"} = ""; $ENV{"SSH_ASKPASS"} = "";
my $loadIncreased = 0;
my ($amWilling, $localSystem, $neededSystem, $drvPath, $maxSilentTime) = @ARGV;
$maxSilentTime = 0 unless defined $maxSilentTime;
sub sendReply { sub sendReply {
my $reply = shift; my $reply = shift;
print STDERR "# $reply\n"; print STDERR "# $reply\n";
} }
sub decline {
sendReply "decline"; # Initialisation.
exit 0; my $loadIncreased = 0;
}
my ($localSystem, $maxSilentTime) = @ARGV;
$maxSilentTime = 0 unless defined $maxSilentTime;
my $currentLoad = $ENV{"NIX_CURRENT_LOAD"}; my $currentLoad = $ENV{"NIX_CURRENT_LOAD"};
decline unless defined $currentLoad;
mkdir $currentLoad, 0777 or die unless -d $currentLoad;
my $conf = $ENV{"NIX_REMOTE_SYSTEMS"}; my $conf = $ENV{"NIX_REMOTE_SYSTEMS"};
decline if !defined $conf || ! -e $conf;
my $canBuildLocally = $amWilling && ($localSystem eq $neededSystem);
# Read the list of machines.
my @machines;
open CONF, "< $conf" or die;
while (<CONF>) {
chomp;
s/\#.*$//g;
next if /^\s*$/;
/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\d+)(\s+([0-9\.]+))?\s*$/ or die;
push @machines,
{ hostName => $1
, systemTypes => [split(/,/, $2)]
, sshKeys => $3
, maxJobs => $4
, speedFactor => 1.0 * ($6 || 1)
, enabled => 1
};
}
close CONF;
# Acquire the exclusive lock on $currentLoad/main-lock.
my $mainLock = "$currentLoad/main-lock";
open MAINLOCK, ">>$mainLock" or die;
flock(MAINLOCK, LOCK_EX) or die;
sub openSlotLock { sub openSlotLock {
@ -91,111 +54,147 @@ sub openSlotLock {
open $slotLock, ">>$slotLockFn" or die; open $slotLock, ">>$slotLockFn" or die;
return $slotLock; return $slotLock;
} }
# Read the list of machines.
my @machines;
if (defined $conf && -e $conf) {
open CONF, "< $conf" or die;
while (<CONF>) {
chomp;
s/\#.*$//g;
next if /^\s*$/;
/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\d+)(\s+([0-9\.]+))?\s*$/ or die;
push @machines,
{ hostName => $1
, systemTypes => [split(/,/, $2)]
, sshKeys => $3
, maxJobs => $4
, speedFactor => 1.0 * ($6 || 1)
, enabled => 1
};
}
close CONF;
}
# Wait for the calling process to ask us whether we can build some derivation.
my ($drvPath, $hostName, $slotLock);
REQ: while (1) {
$_ = <STDIN> || exit 0;
my ($amWilling, $neededSystem);
($amWilling, $neededSystem, $drvPath) = split;
my $canBuildLocally = $amWilling && ($localSystem eq $neededSystem);
if (!defined $currentLoad) {
sendReply "decline";
next;
}
# Acquire the exclusive lock on $currentLoad/main-lock.
my $hostName; mkdir $currentLoad, 0777 or die unless -d $currentLoad;
my $slotLock; my $mainLock = "$currentLoad/main-lock";
open MAINLOCK, ">>$mainLock" or die;
while (1) { flock(MAINLOCK, LOCK_EX) or die;
# Find all machine that can execute this build, i.e., that support
# builds for the given platform and are not at their job limit. while (1) {
my $rightType = 0; # Find all machine that can execute this build, i.e., that
my @available = (); # support builds for the given platform and are not at their
LOOP: foreach my $cur (@machines) { # job limit.
if ($cur->{enabled} && grep { $neededSystem eq $_ } @{$cur->{systemTypes}}) { my $rightType = 0;
$rightType = 1; my @available = ();
LOOP: foreach my $cur (@machines) {
if ($cur->{enabled} && grep { $neededSystem eq $_ } @{$cur->{systemTypes}}) {
$rightType = 1;
# We have a machine of the right type. Determine the load on # We have a machine of the right type. Determine the load on
# the machine. # the machine.
my $slot = 0; my $slot = 0;
my $load = 0; my $load = 0;
my $free; my $free;
while ($slot < $cur->{maxJobs}) { while ($slot < $cur->{maxJobs}) {
my $slotLock = openSlotLock($cur, $slot); my $slotLock = openSlotLock($cur, $slot);
if (flock($slotLock, LOCK_EX | LOCK_NB)) { if (flock($slotLock, LOCK_EX | LOCK_NB)) {
$free = $slot unless defined $free; $free = $slot unless defined $free;
flock($slotLock, LOCK_UN) or die; flock($slotLock, LOCK_UN) or die;
} else { } else {
$load++; $load++;
}
close $slotLock;
$slot++;
} }
close $slotLock;
$slot++; push @available, { machine => $cur, load => $load, free => $free }
if $load < $cur->{maxJobs};
} }
push @available, { machine => $cur, load => $load, free => $free }
if $load < $cur->{maxJobs};
} }
}
if (defined $ENV{NIX_DEBUG_HOOK}) { if (defined $ENV{NIX_DEBUG_HOOK}) {
print STDERR "load on " . $_->{machine}->{hostName} . " = " . $_->{load} . "\n" print STDERR "load on " . $_->{machine}->{hostName} . " = " . $_->{load} . "\n"
foreach @available; foreach @available;
}
# Didn't find any available machine? Then decline or postpone.
if (scalar @available == 0) {
# Postpone if we have a machine of the right type, except if the
# local system can and wants to do the build.
if ($rightType && !$canBuildLocally) {
sendReply "postpone";
exit 0;
} else {
decline;
} }
}
# Prioritise the available machines as follows: # Didn't find any available machine? Then decline or postpone.
# - First by load divided by speed factor, rounded to the nearest if (scalar @available == 0) {
# integer. This causes fast machines to be preferred over slow # Postpone if we have a machine of the right type, except
# machines with similar loads. # if the local system can and wants to do the build.
# - Then by speed factor. if ($rightType && !$canBuildLocally) {
# - Finally by load. sendReply "postpone";
sub lf { my $x = shift; return int($x->{load} / $x->{machine}->{speedFactor} + 0.4999); } } else {
@available = sort sendReply "decline";
{ lf($a) <=> lf($b) }
|| $b->{machine}->{speedFactor} <=> $a->{machine}->{speedFactor} close MAINLOCK;
|| $a->{load} <=> $b->{load} next REQ;
} @available; }
# Select the best available machine and lock a free slot. # Prioritise the available machines as follows:
my $selected = $available[0]; # - First by load divided by speed factor, rounded to the nearest
my $machine = $selected->{machine}; # integer. This causes fast machines to be preferred over slow
# machines with similar loads.
$slotLock = openSlotLock($machine, $selected->{free}); # - Then by speed factor.
flock($slotLock, LOCK_EX | LOCK_NB) or die; # - Finally by load.
utime undef, undef, $slotLock; sub lf { my $x = shift; return int($x->{load} / $x->{machine}->{speedFactor} + 0.4999); }
@available = sort
close MAINLOCK; { lf($a) <=> lf($b)
|| $b->{machine}->{speedFactor} <=> $a->{machine}->{speedFactor}
|| $a->{load} <=> $b->{load}
} @available;
# Connect to the selected machine. # Select the best available machine and lock a free slot.
@sshOpts = ("-i", $machine->{sshKeys}, "-x"); my $selected = $available[0];
$hostName = $machine->{hostName}; my $machine = $selected->{machine};
last if openSSHConnection $hostName;
$slotLock = openSlotLock($machine, $selected->{free});
flock($slotLock, LOCK_EX | LOCK_NB) or die;
utime undef, undef, $slotLock;
close MAINLOCK;
# Connect to the selected machine.
@sshOpts = ("-i", $machine->{sshKeys}, "-x");
$hostName = $machine->{hostName};
last REQ if openSSHConnection $hostName;
warn "unable to open SSH connection to $hostName, trying other available machines...\n"; warn "unable to open SSH connection to $hostName, trying other available machines...\n";
$machine->{enabled} = 0; $machine->{enabled} = 0;
}
} }
# Tell Nix we've accepted the build. # Tell Nix we've accepted the build.
sendReply "accept";
my $x = <STDIN>;
chomp $x;
if ($x ne "okay") {
exit 0;
}
print STDERR "building `$drvPath' on `$hostName'\n"; print STDERR "building `$drvPath' on `$hostName'\n";
sendReply "accept";
my @inputs = split /\s/, readline(STDIN);
my @outputs = split /\s/, readline(STDIN);
my @inputs = split /\s/, do { local $/; local @ARGV = "inputs"; <> };
my @outputs = split /\s/, do { local $/; local @ARGV = "outputs"; <> };
my $maybeSign = ""; my $maybeSign = "";
$maybeSign = "--sign" if -e "/nix/etc/nix/signing-key.sec"; $maybeSign = "--sign" if -e "/nix/etc/nix/signing-key.sec";
@ -238,7 +237,7 @@ if (system("ssh $hostName @sshOpts -tt 'nix-store -r $drvPath $buildFlags > /dev
exit $res; exit $res;
} }
print "build of `$drvPath' on `$hostName' succeeded\n"; #print "build of `$drvPath' on `$hostName' succeeded\n";
# Copy the output from the build machine. # Copy the output from the build machine.
@ -246,7 +245,8 @@ foreach my $output (@outputs) {
my $maybeSignRemote = ""; my $maybeSignRemote = "";
$maybeSignRemote = "--sign" if $UID != 0; $maybeSignRemote = "--sign" if $UID != 0;
system("ssh $hostName @sshOpts 'nix-store --export $maybeSignRemote $output' | @bindir@/nix-store --import > /dev/null") == 0 system("ssh $hostName @sshOpts 'nix-store --export $maybeSignRemote $output'" .
"| NIX_HELD_LOCKS=$output @bindir@/nix-store --import > /dev/null") == 0
or die "cannot copy $output from $hostName: $?"; or die "cannot copy $output from $hostName: $?";
} }

View file

@ -64,6 +64,7 @@ static const uid_t rootUserId = 0;
/* Forward definition. */ /* Forward definition. */
class Worker; class Worker;
class HookInstance;
/* A pointer to a goal. */ /* A pointer to a goal. */
@ -215,6 +216,8 @@ public:
LocalStore & store; LocalStore & store;
boost::shared_ptr<HookInstance> hook;
Worker(LocalStore & store); Worker(LocalStore & store);
~Worker(); ~Worker();
@ -615,6 +618,94 @@ void deletePathWrapped(const Path & path)
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
struct HookInstance
{
/* Pipes for talking to the build hook. */
Pipe toHook;
/* Pipe for the hook's standard output/error. */
Pipe fromHook;
/* The process ID of the hook. */
Pid pid;
HookInstance();
~HookInstance();
};
HookInstance::HookInstance()
{
debug("starting build hook");
Path buildHook = absPath(getEnv("NIX_BUILD_HOOK"));
/* Create a pipe to get the output of the child. */
fromHook.create();
/* Create the communication pipes. */
toHook.create();
/* Fork the hook. */
pid = fork();
switch (pid) {
case -1:
throw SysError("unable to fork");
case 0:
try { /* child */
commonChildInit(fromHook);
if (chdir("/") == -1) throw SysError("changing into `/");
/* Dup the communication pipes. */
toHook.writeSide.close();
if (dup2(toHook.readSide, STDIN_FILENO) == -1)
throw SysError("dupping to-hook read side");
execl(buildHook.c_str(), buildHook.c_str(), thisSystem.c_str(),
(format("%1%") % maxSilentTime).str().c_str(), NULL);
throw SysError(format("executing `%1%'") % buildHook);
} catch (std::exception & e) {
std::cerr << format("build hook error: %1%") % e.what() << std::endl;
}
quickExit(1);
}
/* parent */
pid.setSeparatePG(true);
pid.setKillSignal(SIGTERM);
fromHook.writeSide.close();
toHook.readSide.close();
}
HookInstance::~HookInstance()
{
try {
/* Cleanly shut down the hook by closing its stdin if it's not
already building. Otherwise pid's destructor will kill
it. */
if (pid != -1 && toHook.writeSide != -1) {
toHook.writeSide.close();
pid.wait(true);
}
} catch (...) {
ignoreException();
}
}
//////////////////////////////////////////////////////////////////////
typedef enum {rpAccept, rpDecline, rpPostpone} HookReply;
class DerivationGoal : public Goal class DerivationGoal : public Goal
{ {
private: private:
@ -651,12 +742,9 @@ private:
/* Pipe for the builder's standard output/error. */ /* Pipe for the builder's standard output/error. */
Pipe logPipe; Pipe logPipe;
/* Whether we're building using a build hook. */ /* The build hook. */
bool usingBuildHook; boost::shared_ptr<HookInstance> hook;
/* Pipes for talking to the build hook (if any). */
Pipe toHook;
/* Whether we're currently doing a chroot build. */ /* Whether we're currently doing a chroot build. */
bool useChroot; bool useChroot;
@ -694,12 +782,8 @@ private:
void buildDone(); void buildDone();
/* Is the build hook willing to perform the build? */ /* Is the build hook willing to perform the build? */
typedef enum {rpAccept, rpDecline, rpPostpone} HookReply;
HookReply tryBuildHook(); HookReply tryBuildHook();
/* Synchronously wait for a build hook to finish. */
void terminateBuildHook(bool kill = false);
/* Start building a derivation. */ /* Start building a derivation. */
void startBuilder(); void startBuilder();
@ -711,10 +795,6 @@ private:
/* Open a log file and a pipe to it. */ /* Open a log file and a pipe to it. */
Path openLogFile(); Path openLogFile();
/* Common initialisation to be performed in child processes (i.e.,
both in builders and in build hooks). */
void initChild();
/* Delete the temporary directory, if we have one. */ /* Delete the temporary directory, if we have one. */
void deleteTmpDir(bool force); void deleteTmpDir(bool force);
@ -742,6 +822,7 @@ DerivationGoal::DerivationGoal(const Path & drvPath, Worker & worker)
trace("created"); trace("created");
} }
DerivationGoal::~DerivationGoal() DerivationGoal::~DerivationGoal()
{ {
/* Careful: we should never ever throw an exception from a /* Careful: we should never ever throw an exception from a
@ -754,6 +835,7 @@ DerivationGoal::~DerivationGoal()
} }
} }
void DerivationGoal::killChild() void DerivationGoal::killChild()
{ {
if (pid != -1) { if (pid != -1) {
@ -778,6 +860,8 @@ void DerivationGoal::killChild()
assert(pid == -1); assert(pid == -1);
} }
hook.reset();
} }
@ -1048,7 +1132,6 @@ void DerivationGoal::tryToBuild()
/* Is the build hook willing to accept this job? */ /* Is the build hook willing to accept this job? */
if (!preferLocalBuild) { if (!preferLocalBuild) {
usingBuildHook = true;
switch (tryBuildHook()) { switch (tryBuildHook()) {
case rpAccept: case rpAccept:
/* Yes, it has started doing so. Wait until we get /* Yes, it has started doing so. Wait until we get
@ -1056,7 +1139,8 @@ void DerivationGoal::tryToBuild()
state = &DerivationGoal::buildDone; state = &DerivationGoal::buildDone;
return; return;
case rpPostpone: case rpPostpone:
/* Not now; wait until at least one child finishes. */ /* Not now; wait until at least one child finishes or
the wake-up timeout expires. */
worker.waitForAWhile(shared_from_this()); worker.waitForAWhile(shared_from_this());
outputLocks.unlock(); outputLocks.unlock();
return; return;
@ -1066,8 +1150,6 @@ void DerivationGoal::tryToBuild()
} }
} }
usingBuildHook = false;
/* Make sure that we are allowed to start a build. If this /* Make sure that we are allowed to start a build. If this
derivation prefers to be done locally, do it even if derivation prefers to be done locally, do it even if
maxBuildJobs is 0. */ maxBuildJobs is 0. */
@ -1108,16 +1190,23 @@ void DerivationGoal::buildDone()
to have terminated. In fact, the builder could also have to have terminated. In fact, the builder could also have
simply have closed its end of the pipe --- just don't do that simply have closed its end of the pipe --- just don't do that
:-) */ :-) */
/* !!! this could block! security problem! solution: kill the int status;
child */ pid_t savedPid;
pid_t savedPid = pid; if (hook) {
int status = pid.wait(true); savedPid = hook->pid;
status = hook->pid.wait(true);
} else {
/* !!! this could block! security problem! solution: kill the
child */
savedPid = pid;
status = pid.wait(true);
}
debug(format("builder process for `%1%' finished") % drvPath); debug(format("builder process for `%1%' finished") % drvPath);
/* So the child is gone now. */ /* So the child is gone now. */
worker.childTerminated(savedPid); worker.childTerminated(savedPid);
/* Close the read side of the logger pipe. */ /* Close the read side of the logger pipe. */
logPipe.readSide.close(); logPipe.readSide.close();
@ -1192,11 +1281,11 @@ void DerivationGoal::buildDone()
/* When using a build hook, the hook will return a remote /* When using a build hook, the hook will return a remote
build failure using exit code 100. Anything else is a hook build failure using exit code 100. Anything else is a hook
problem. */ problem. */
bool hookError = usingBuildHook && bool hookError = hook &&
(!WIFEXITED(status) || WEXITSTATUS(status) != 100); (!WIFEXITED(status) || WEXITSTATUS(status) != 100);
if (printBuildTrace) { if (printBuildTrace) {
if (usingBuildHook && hookError) if (hook && hookError)
printMsg(lvlError, format("@ hook-failed %1% %2% %3% %4%") printMsg(lvlError, format("@ hook-failed %1% %2% %3% %4%")
% drvPath % drv.outputs["out"].path % status % e.msg()); % drvPath % drv.outputs["out"].path % status % e.msg());
else else
@ -1231,162 +1320,74 @@ void DerivationGoal::buildDone()
} }
DerivationGoal::HookReply DerivationGoal::tryBuildHook() HookReply DerivationGoal::tryBuildHook()
{ {
if (!useBuildHook) return rpDecline; if (!useBuildHook || getEnv("NIX_BUILD_HOOK") == "") return rpDecline;
Path buildHook = getEnv("NIX_BUILD_HOOK");
if (buildHook == "") return rpDecline;
buildHook = absPath(buildHook);
/* Create a directory where we will store files used for if (!worker.hook)
communication between us and the build hook. */ worker.hook = boost::shared_ptr<HookInstance>(new HookInstance);
tmpDir = createTempDir();
/* Create the log file and pipe. */
Path logFile = openLogFile();
/* Create the communication pipes. */ writeLine(worker.hook->toHook.writeSide, (format("%1% %2% %3%") %
toHook.create(); (worker.getNrLocalBuilds() < maxBuildJobs ? "1" : "0") % drv.platform % drvPath).str());
/* Fork the hook. */
pid = fork();
switch (pid) {
case -1:
throw SysError("unable to fork");
case 0:
try { /* child */
initChild();
string s;
foreach (DerivationOutputs::const_iterator, i, drv.outputs)
s += i->second.path + " ";
if (setenv("NIX_HELD_LOCKS", s.c_str(), 1))
throw SysError("setting an environment variable");
execl(buildHook.c_str(), buildHook.c_str(),
(worker.getNrLocalBuilds() < maxBuildJobs ? (string) "1" : "0").c_str(),
thisSystem.c_str(),
drv.platform.c_str(),
drvPath.c_str(),
(format("%1%") % maxSilentTime).str().c_str(),
NULL);
throw SysError(format("executing `%1%'") % buildHook);
} catch (std::exception & e) {
std::cerr << format("build hook error: %1%") % e.what() << std::endl;
}
quickExit(1);
}
/* parent */
pid.setSeparatePG(true);
pid.setKillSignal(SIGTERM);
logPipe.writeSide.close();
worker.childStarted(shared_from_this(),
pid, singleton<set<int> >(logPipe.readSide), false, false);
toHook.readSide.close();
/* Read the first line of input, which should be a word indicating /* Read the first line of input, which should be a word indicating
whether the hook wishes to perform the build. */ whether the hook wishes to perform the build. */
string reply; string reply;
try { while (true) {
while (true) { string s = readLine(worker.hook->fromHook.readSide);
string s = readLine(logPipe.readSide); if (string(s, 0, 2) == "# ") {
if (string(s, 0, 2) == "# ") { reply = string(s, 2);
reply = string(s, 2); break;
break;
}
handleChildOutput(logPipe.readSide, s + "\n");
} }
} catch (Error & e) { handleChildOutput(worker.hook->fromHook.readSide, s + "\n");
terminateBuildHook(true);
throw;
} }
debug(format("hook reply is `%1%'") % reply); debug(format("hook reply is `%1%'") % reply);
if (reply == "decline" || reply == "postpone") { if (reply == "decline" || reply == "postpone")
/* Clean up the child. !!! hacky / should verify */
terminateBuildHook();
return reply == "decline" ? rpDecline : rpPostpone; return reply == "decline" ? rpDecline : rpPostpone;
} else if (reply != "accept")
throw Error(format("bad hook reply `%1%'") % reply);
else if (reply == "accept") { printMsg(lvlTalkative, format("using hook to build path(s) %1%")
% showPaths(outputPaths(drv.outputs)));
printMsg(lvlInfo, format("using hook to build path(s) %1%") hook = worker.hook;
% showPaths(outputPaths(drv.outputs))); worker.hook.reset();
/* Write the information that the hook needs to perform the /* Tell the hook all the inputs that have to be copied to the
build, i.e., the set of input paths, the set of output remote system. This unfortunately has to contain the entire
paths, and the references (pointer graph) in the input derivation closure to ensure that the validity invariant holds
paths. */ on the remote system. (I.e., it's unfortunate that we have to
list it since the remote system *probably* already has it.) */
PathSet allInputs;
allInputs.insert(inputPaths.begin(), inputPaths.end());
computeFSClosure(drvPath, allInputs);
Path inputListFN = tmpDir + "/inputs"; string s;
Path outputListFN = tmpDir + "/outputs"; foreach (PathSet::iterator, i, allInputs) s += *i + " ";
Path referencesFN = tmpDir + "/references"; writeLine(hook->toHook.writeSide, s);
/* The `inputs' file lists all inputs that have to be copied
to the remote system. This unfortunately has to contain
the entire derivation closure to ensure that the validity
invariant holds on the remote system. (I.e., it's
unfortunate that we have to list it since the remote system
*probably* already has it.) */
PathSet allInputs;
allInputs.insert(inputPaths.begin(), inputPaths.end());
computeFSClosure(drvPath, allInputs);
string s; /* Tell the hooks the outputs that have to be copied back from the
foreach (PathSet::iterator, i, allInputs) s += *i + "\n"; remote system. */
s = "";
foreach (DerivationOutputs::iterator, i, drv.outputs)
s += i->second.path + " ";
writeLine(hook->toHook.writeSide, s);
hook->toHook.writeSide.close();
/* Create the log file and pipe. */
Path logFile = openLogFile();
worker.childStarted(shared_from_this(),
hook->pid, singleton<set<int> >(hook->fromHook.readSide), false, false);
if (printBuildTrace)
printMsg(lvlError, format("@ build-started %1% %2% %3% %4%")
% drvPath % drv.outputs["out"].path % drv.platform % logFile);
writeFile(inputListFN, s); return rpAccept;
/* The `outputs' file lists all outputs that have to be copied
from the remote system. */
s = "";
foreach (DerivationOutputs::iterator, i, drv.outputs)
s += i->second.path + "\n";
writeFile(outputListFN, s);
/* The `references' file has exactly the format accepted by
`nix-store --register-validity'. */
writeFile(referencesFN,
makeValidityRegistration(allInputs, true, false));
/* Tell the hook to proceed. */
writeLine(toHook.writeSide, "okay");
toHook.writeSide.close();
if (printBuildTrace)
printMsg(lvlError, format("@ build-started %1% %2% %3% %4%")
% drvPath % drv.outputs["out"].path % drv.platform % logFile);
return rpAccept;
}
else throw Error(format("bad hook reply `%1%'") % reply);
}
void DerivationGoal::terminateBuildHook(bool kill)
{
debug("terminating build hook");
pid_t savedPid = pid;
if (kill)
pid.kill();
else
pid.wait(true);
/* `false' means don't wake up waiting goals, since we want to
keep this build slot ourselves. */
worker.childTerminated(savedPid, false);
toHook.writeSide.close();
fdLogFile.close();
logPipe.readSide.close();
deleteTmpDir(true); /* get rid of the hook's temporary directory */
} }
@ -1667,9 +1668,12 @@ void DerivationGoal::startBuilder()
printMsg(lvlChatty, format("executing builder `%1%'") % printMsg(lvlChatty, format("executing builder `%1%'") %
drv.builder); drv.builder);
/* Create the log file and pipe. */ /* Create the log file. */
Path logFile = openLogFile(); Path logFile = openLogFile();
/* Create a pipe to get the output of the child. */
logPipe.create();
/* Fork a child to build the package. Note that while we /* Fork a child to build the package. Note that while we
currently use forks to run and wait for the children, it currently use forks to run and wait for the children, it
shouldn't be hard to use threads for this on systems where shouldn't be hard to use threads for this on systems where
@ -1710,18 +1714,23 @@ void DerivationGoal::startBuilder()
throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target); throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
} }
/* Do the chroot(). initChild() will do a chdir() to /* Do the chroot(). Below we do a chdir() to the
the temporary build directory to make sure the temporary build directory to make sure the current
current directory is in the chroot. (Actually the directory is in the chroot. (Actually the order
order doesn't matter, since due to the bind mount doesn't matter, since due to the bind mount tmpDir
tmpDir and tmpRootDit/tmpDir are the same and tmpRootDit/tmpDir are the same directories.) */
directories.) */
if (chroot(chrootRootDir.c_str()) == -1) if (chroot(chrootRootDir.c_str()) == -1)
throw SysError(format("cannot change root directory to `%1%'") % chrootRootDir); throw SysError(format("cannot change root directory to `%1%'") % chrootRootDir);
} }
#endif #endif
initChild(); commonChildInit(logPipe);
if (chdir(tmpDir.c_str()) == -1)
throw SysError(format("changing into `%1%'") % tmpDir);
/* Close all other file descriptors. */
closeMostFDs(set<int>());
#ifdef CAN_DO_LINUX32_BUILDS #ifdef CAN_DO_LINUX32_BUILDS
if (drv.platform == "i686-linux" && thisSystem == "x86_64-linux") { if (drv.platform == "i686-linux" && thisSystem == "x86_64-linux") {
@ -1742,10 +1751,10 @@ void DerivationGoal::startBuilder()
/* If we are running in `build-users' mode, then switch to /* If we are running in `build-users' mode, then switch to
the user we allocated above. Make sure that we drop the user we allocated above. Make sure that we drop
all root privileges. Note that initChild() above has all root privileges. Note that above we have closed
closed all file descriptors except std*, so that's all file descriptors except std*, so that's safe. Also
safe. Also note that setuid() when run as root sets note that setuid() when run as root sets the real,
the real, effective and saved UIDs. */ effective and saved UIDs. */
if (buildUser.enabled()) { if (buildUser.enabled()) {
printMsg(lvlChatty, format("switching to user `%1%'") % buildUser.getUser()); printMsg(lvlChatty, format("switching to user `%1%'") % buildUser.getUser());
@ -1838,7 +1847,7 @@ void DerivationGoal::computeClosure()
/* When using a build hook, the build hook can register the output /* When using a build hook, the build hook can register the output
as valid (by doing `nix-store --import'). If so we don't have as valid (by doing `nix-store --import'). If so we don't have
to do anything here. */ to do anything here. */
if (usingBuildHook) { if (hook) {
bool allValid = true; bool allValid = true;
foreach (DerivationOutputs::iterator, i, drv.outputs) foreach (DerivationOutputs::iterator, i, drv.outputs)
if (!worker.store.isValidPath(i->second.path)) allValid = false; if (!worker.store.isValidPath(i->second.path)) allValid = false;
@ -1966,32 +1975,10 @@ Path DerivationGoal::openLogFile()
if (fdLogFile == -1) if (fdLogFile == -1)
throw SysError(format("creating log file `%1%'") % logFileName); throw SysError(format("creating log file `%1%'") % logFileName);
/* Create a pipe to get the output of the child. */
logPipe.create();
return logFileName; return logFileName;
} }
void DerivationGoal::initChild()
{
commonChildInit(logPipe);
if (chdir(tmpDir.c_str()) == -1)
throw SysError(format("changing into `%1%'") % tmpDir);
/* When running a hook, dup the communication pipes. */
if (usingBuildHook) {
toHook.writeSide.close();
if (dup2(toHook.readSide, STDIN_FILENO) == -1)
throw SysError("dupping to-hook read side");
}
/* Close all other file descriptors. */
closeMostFDs(set<int>());
}
void DerivationGoal::deleteTmpDir(bool force) void DerivationGoal::deleteTmpDir(bool force)
{ {
if (tmpDir != "") { if (tmpDir != "") {
@ -2011,19 +1998,16 @@ void DerivationGoal::deleteTmpDir(bool force)
void DerivationGoal::handleChildOutput(int fd, const string & data) void DerivationGoal::handleChildOutput(int fd, const string & data)
{ {
if (fd == logPipe.readSide) { if (verbosity >= buildVerbosity)
if (verbosity >= buildVerbosity) writeToStderr((unsigned char *) data.c_str(), data.size());
writeToStderr((unsigned char *) data.c_str(), data.size()); if (fdLogFile != -1)
writeFull(fdLogFile, (unsigned char *) data.c_str(), data.size()); writeFull(fdLogFile, (unsigned char *) data.c_str(), data.size());
}
else abort();
} }
void DerivationGoal::handleEOF(int fd) void DerivationGoal::handleEOF(int fd)
{ {
if (fd == logPipe.readSide) worker.wakeUp(shared_from_this()); worker.wakeUp(shared_from_this());
} }

View file

@ -2,20 +2,22 @@
#set -x #set -x
drv=$4 while read x y drv rest; do
echo "HOOK for $drv" >&2 echo "HOOK for $drv" >&2
outPath=`sed 's/Derive(\[("out",\"\([^\"]*\)\".*/\1/' $drv` outPath=`sed 's/Derive(\[("out",\"\([^\"]*\)\".*/\1/' $drv`
echo "output path is $outPath" >&2 echo "output path is $outPath" >&2
if `echo $outPath | grep -q input-1`; then if `echo $outPath | grep -q input-1`; then
echo "# accept" >&2 echo "# accept" >&2
read x read inputs
echo "got $x" read outputs
mkdir $outPath mkdir $outPath
echo "BAR" > $outPath/foo echo "BAR" > $outPath/foo
else else
echo "# decline" >&2 echo "# decline" >&2
fi fi
done