Merge branch 'master' into master
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
SUBDIRS = hydra-evaluator hydra-eval-jobs hydra-queue-runner sql script lib root ttf
|
||||
BOOTCLEAN_SUBDIRS = $(SUBDIRS)
|
||||
DIST_SUBDIRS = $(SUBDIRS)
|
||||
@@ -1,5 +0,0 @@
|
||||
bin_PROGRAMS = hydra-eval-jobs
|
||||
|
||||
hydra_eval_jobs_SOURCES = hydra-eval-jobs.cc
|
||||
hydra_eval_jobs_LDADD = $(NIX_LIBS) -lnixcmd
|
||||
hydra_eval_jobs_CXXFLAGS = $(NIX_CFLAGS) -I ../libhydra
|
||||
@@ -1,558 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "shared.hh"
|
||||
#include "store-api.hh"
|
||||
#include "eval.hh"
|
||||
#include "eval-inline.hh"
|
||||
#include "util.hh"
|
||||
#include "get-drvs.hh"
|
||||
#include "globals.hh"
|
||||
#include "common-eval-args.hh"
|
||||
#include "flake/flakeref.hh"
|
||||
#include "flake/flake.hh"
|
||||
#include "attr-path.hh"
|
||||
#include "derivations.hh"
|
||||
#include "local-fs-store.hh"
|
||||
|
||||
#include "hydra-config.hh"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
void check_pid_status_nonblocking(pid_t check_pid) {
|
||||
// Only check 'initialized' and known PID's
|
||||
if (check_pid <= 0) { return; }
|
||||
|
||||
int wstatus = 0;
|
||||
pid_t pid = waitpid(check_pid, &wstatus, WNOHANG);
|
||||
// -1 = failure, WNOHANG: 0 = no change
|
||||
if (pid <= 0) { return; }
|
||||
|
||||
std::cerr << "child process (" << pid << ") ";
|
||||
|
||||
if (WIFEXITED(wstatus)) {
|
||||
std::cerr << "exited with status=" << WEXITSTATUS(wstatus) << std::endl;
|
||||
} else if (WIFSIGNALED(wstatus)) {
|
||||
std::cerr << "killed by signal=" << WTERMSIG(wstatus) << std::endl;
|
||||
} else if (WIFSTOPPED(wstatus)) {
|
||||
std::cerr << "stopped by signal=" << WSTOPSIG(wstatus) << std::endl;
|
||||
} else if (WIFCONTINUED(wstatus)) {
|
||||
std::cerr << "continued" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
using namespace nix;
|
||||
|
||||
static Path gcRootsDir;
|
||||
static size_t maxMemorySize;
|
||||
|
||||
struct MyArgs : MixEvalArgs, MixCommonArgs
|
||||
{
|
||||
Path releaseExpr;
|
||||
bool flake = false;
|
||||
bool dryRun = false;
|
||||
|
||||
MyArgs() : MixCommonArgs("hydra-eval-jobs")
|
||||
{
|
||||
addFlag({
|
||||
.longName = "gc-roots-dir",
|
||||
.description = "garbage collector roots directory",
|
||||
.labels = {"path"},
|
||||
.handler = {&gcRootsDir}
|
||||
});
|
||||
|
||||
addFlag({
|
||||
.longName = "dry-run",
|
||||
.description = "don't create store derivations",
|
||||
.handler = {&dryRun, true}
|
||||
});
|
||||
|
||||
addFlag({
|
||||
.longName = "flake",
|
||||
.description = "build a flake",
|
||||
.handler = {&flake, true}
|
||||
});
|
||||
|
||||
expectArg("expr", &releaseExpr);
|
||||
}
|
||||
};
|
||||
|
||||
static MyArgs myArgs;
|
||||
|
||||
static std::string queryMetaStrings(EvalState & state, DrvInfo & drv, const std::string & name, const std::string & subAttribute)
|
||||
{
|
||||
Strings res;
|
||||
std::function<void(Value & v)> rec;
|
||||
|
||||
rec = [&](Value & v) {
|
||||
state.forceValue(v, noPos);
|
||||
if (v.type() == nString)
|
||||
res.push_back(v.string.s);
|
||||
else if (v.isList())
|
||||
for (unsigned int n = 0; n < v.listSize(); ++n)
|
||||
rec(*v.listElems()[n]);
|
||||
else if (v.type() == nAttrs) {
|
||||
auto a = v.attrs->find(state.symbols.create(subAttribute));
|
||||
if (a != v.attrs->end())
|
||||
res.push_back(std::string(state.forceString(*a->value)));
|
||||
}
|
||||
};
|
||||
|
||||
Value * v = drv.queryMeta(name);
|
||||
if (v) rec(*v);
|
||||
|
||||
return concatStringsSep(", ", res);
|
||||
}
|
||||
|
||||
static void worker(
|
||||
EvalState & state,
|
||||
Bindings & autoArgs,
|
||||
AutoCloseFD & to,
|
||||
AutoCloseFD & from)
|
||||
{
|
||||
Value vTop;
|
||||
|
||||
if (myArgs.flake) {
|
||||
using namespace flake;
|
||||
|
||||
auto flakeRef = parseFlakeRef(myArgs.releaseExpr);
|
||||
|
||||
auto vFlake = state.allocValue();
|
||||
|
||||
auto lockedFlake = lockFlake(state, flakeRef,
|
||||
LockFlags {
|
||||
.updateLockFile = false,
|
||||
.useRegistries = false,
|
||||
.allowMutable = false,
|
||||
});
|
||||
|
||||
callFlake(state, lockedFlake, *vFlake);
|
||||
|
||||
auto vOutputs = vFlake->attrs->get(state.symbols.create("outputs"))->value;
|
||||
state.forceValue(*vOutputs, noPos);
|
||||
|
||||
auto aHydraJobs = vOutputs->attrs->get(state.symbols.create("hydraJobs"));
|
||||
if (!aHydraJobs)
|
||||
aHydraJobs = vOutputs->attrs->get(state.symbols.create("checks"));
|
||||
if (!aHydraJobs)
|
||||
throw Error("flake '%s' does not provide any Hydra jobs or checks", flakeRef);
|
||||
|
||||
vTop = *aHydraJobs->value;
|
||||
|
||||
} else {
|
||||
state.evalFile(lookupFileArg(state, myArgs.releaseExpr), vTop);
|
||||
}
|
||||
|
||||
auto vRoot = state.allocValue();
|
||||
state.autoCallFunction(autoArgs, vTop, *vRoot);
|
||||
|
||||
while (true) {
|
||||
/* Wait for the master to send us a job name. */
|
||||
writeLine(to.get(), "next");
|
||||
|
||||
auto s = readLine(from.get());
|
||||
if (s == "exit") break;
|
||||
if (!hasPrefix(s, "do ")) abort();
|
||||
std::string attrPath(s, 3);
|
||||
|
||||
debug("worker process %d at '%s'", getpid(), attrPath);
|
||||
|
||||
/* Evaluate it and send info back to the master. */
|
||||
nlohmann::json reply;
|
||||
|
||||
try {
|
||||
auto vTmp = findAlongAttrPath(state, attrPath, autoArgs, *vRoot).first;
|
||||
|
||||
auto v = state.allocValue();
|
||||
state.autoCallFunction(autoArgs, *vTmp, *v);
|
||||
|
||||
if (auto drv = getDerivation(state, *v, false)) {
|
||||
|
||||
DrvInfo::Outputs outputs = drv->queryOutputs();
|
||||
|
||||
if (drv->querySystem() == "unknown")
|
||||
throw EvalError("derivation must have a 'system' attribute");
|
||||
|
||||
auto drvPath = state.store->printStorePath(drv->requireDrvPath());
|
||||
|
||||
nlohmann::json job;
|
||||
|
||||
job["nixName"] = drv->queryName();
|
||||
job["system"] =drv->querySystem();
|
||||
job["drvPath"] = drvPath;
|
||||
job["description"] = drv->queryMetaString("description");
|
||||
job["license"] = queryMetaStrings(state, *drv, "license", "shortName");
|
||||
job["homepage"] = drv->queryMetaString("homepage");
|
||||
job["maintainers"] = queryMetaStrings(state, *drv, "maintainers", "email");
|
||||
job["schedulingPriority"] = drv->queryMetaInt("schedulingPriority", 100);
|
||||
job["timeout"] = drv->queryMetaInt("timeout", 36000);
|
||||
job["maxSilent"] = drv->queryMetaInt("maxSilent", 7200);
|
||||
job["isChannel"] = drv->queryMetaBool("isHydraChannel", false);
|
||||
|
||||
/* If this is an aggregate, then get its constituents. */
|
||||
auto a = v->attrs->get(state.symbols.create("_hydraAggregate"));
|
||||
if (a && state.forceBool(*a->value, a->pos)) {
|
||||
auto a = v->attrs->get(state.symbols.create("constituents"));
|
||||
if (!a)
|
||||
throw EvalError("derivation must have a ‘constituents’ attribute");
|
||||
|
||||
|
||||
PathSet context;
|
||||
state.coerceToString(a->pos, *a->value, context, true, false);
|
||||
for (auto & i : context)
|
||||
if (i.at(0) == '!') {
|
||||
size_t index = i.find("!", 1);
|
||||
job["constituents"].push_back(std::string(i, index + 1));
|
||||
}
|
||||
|
||||
state.forceList(*a->value, a->pos);
|
||||
for (unsigned int n = 0; n < a->value->listSize(); ++n) {
|
||||
auto v = a->value->listElems()[n];
|
||||
state.forceValue(*v, noPos);
|
||||
if (v->type() == nString)
|
||||
job["namedConstituents"].push_back(state.forceStringNoCtx(*v));
|
||||
}
|
||||
}
|
||||
|
||||
/* Register the derivation as a GC root. !!! This
|
||||
registers roots for jobs that we may have already
|
||||
done. */
|
||||
auto localStore = state.store.dynamic_pointer_cast<LocalFSStore>();
|
||||
if (gcRootsDir != "" && localStore) {
|
||||
Path root = gcRootsDir + "/" + std::string(baseNameOf(drvPath));
|
||||
if (!pathExists(root))
|
||||
localStore->addPermRoot(localStore->parseStorePath(drvPath), root);
|
||||
}
|
||||
|
||||
nlohmann::json out;
|
||||
for (auto & j : outputs)
|
||||
// FIXME: handle CA/impure builds.
|
||||
if (j.second)
|
||||
out[j.first] = state.store->printStorePath(*j.second);
|
||||
job["outputs"] = std::move(out);
|
||||
|
||||
reply["job"] = std::move(job);
|
||||
}
|
||||
|
||||
else if (v->type() == nAttrs) {
|
||||
auto attrs = nlohmann::json::array();
|
||||
StringSet ss;
|
||||
for (auto & i : v->attrs->lexicographicOrder(state.symbols)) {
|
||||
std::string name(state.symbols[i->name]);
|
||||
if (name.find('.') != std::string::npos || name.find(' ') != std::string::npos) {
|
||||
printError("skipping job with illegal name '%s'", name);
|
||||
continue;
|
||||
}
|
||||
attrs.push_back(name);
|
||||
}
|
||||
reply["attrs"] = std::move(attrs);
|
||||
}
|
||||
|
||||
else if (v->type() == nNull)
|
||||
;
|
||||
|
||||
else throw TypeError("attribute '%s' is %s, which is not supported", attrPath, showType(*v));
|
||||
|
||||
} catch (EvalError & e) {
|
||||
auto msg = e.msg();
|
||||
// Transmits the error we got from the previous evaluation
|
||||
// in the JSON output.
|
||||
reply["error"] = filterANSIEscapes(msg, true);
|
||||
// Don't forget to print it into the STDERR log, this is
|
||||
// what's shown in the Hydra UI.
|
||||
printError(msg);
|
||||
}
|
||||
|
||||
writeLine(to.get(), reply.dump());
|
||||
|
||||
/* If our RSS exceeds the maximum, exit. The master will
|
||||
start a new process. */
|
||||
struct rusage r;
|
||||
getrusage(RUSAGE_SELF, &r);
|
||||
if ((size_t) r.ru_maxrss > maxMemorySize * 1024) break;
|
||||
}
|
||||
|
||||
writeLine(to.get(), "restart");
|
||||
}
|
||||
|
||||
int main(int argc, char * * argv)
|
||||
{
|
||||
/* Prevent undeclared dependencies in the evaluation via
|
||||
$NIX_PATH. */
|
||||
unsetenv("NIX_PATH");
|
||||
|
||||
return handleExceptions(argv[0], [&]() {
|
||||
|
||||
auto config = std::make_unique<HydraConfig>();
|
||||
|
||||
auto nrWorkers = config->getIntOption("evaluator_workers", 1);
|
||||
maxMemorySize = config->getIntOption("evaluator_max_memory_size", 4096);
|
||||
|
||||
initNix();
|
||||
initGC();
|
||||
|
||||
myArgs.parseCmdline(argvToStrings(argc, argv));
|
||||
|
||||
auto pureEval = config->getBoolOption("evaluator_pure_eval", myArgs.flake);
|
||||
|
||||
/* FIXME: The build hook in conjunction with import-from-derivation is causing "unexpected EOF" during eval */
|
||||
settings.builders = "";
|
||||
|
||||
/* Prevent access to paths outside of the Nix search path and
|
||||
to the environment. */
|
||||
evalSettings.restrictEval = true;
|
||||
|
||||
/* When building a flake, use pure evaluation (no access to
|
||||
'getEnv', 'currentSystem' etc. */
|
||||
evalSettings.pureEval = pureEval;
|
||||
|
||||
if (myArgs.dryRun) settings.readOnlyMode = true;
|
||||
|
||||
if (myArgs.releaseExpr == "") throw UsageError("no expression specified");
|
||||
|
||||
if (gcRootsDir == "") printMsg(lvlError, "warning: `--gc-roots-dir' not specified");
|
||||
|
||||
struct State
|
||||
{
|
||||
std::set<std::string> todo{""};
|
||||
std::set<std::string> active;
|
||||
nlohmann::json jobs;
|
||||
std::exception_ptr exc;
|
||||
};
|
||||
|
||||
std::condition_variable wakeup;
|
||||
|
||||
Sync<State> state_;
|
||||
|
||||
/* Start a handler thread per worker process. */
|
||||
auto handler = [&]()
|
||||
{
|
||||
pid_t pid = -1;
|
||||
try {
|
||||
AutoCloseFD from, to;
|
||||
|
||||
while (true) {
|
||||
|
||||
/* Start a new worker process if necessary. */
|
||||
if (pid == -1) {
|
||||
Pipe toPipe, fromPipe;
|
||||
toPipe.create();
|
||||
fromPipe.create();
|
||||
pid = startProcess(
|
||||
[&,
|
||||
to{std::make_shared<AutoCloseFD>(std::move(fromPipe.writeSide))},
|
||||
from{std::make_shared<AutoCloseFD>(std::move(toPipe.readSide))}
|
||||
]()
|
||||
{
|
||||
try {
|
||||
EvalState state(myArgs.searchPath, openStore());
|
||||
Bindings & autoArgs = *myArgs.getAutoArgs(state);
|
||||
worker(state, autoArgs, *to, *from);
|
||||
} catch (Error & e) {
|
||||
nlohmann::json err;
|
||||
auto msg = e.msg();
|
||||
err["error"] = filterANSIEscapes(msg, true);
|
||||
printError(msg);
|
||||
writeLine(to->get(), err.dump());
|
||||
// Don't forget to print it into the STDERR log, this is
|
||||
// what's shown in the Hydra UI.
|
||||
writeLine(to->get(), "restart");
|
||||
}
|
||||
},
|
||||
ProcessOptions { .allowVfork = false });
|
||||
from = std::move(fromPipe.readSide);
|
||||
to = std::move(toPipe.writeSide);
|
||||
debug("created worker process %d", pid);
|
||||
}
|
||||
|
||||
/* Check whether the existing worker process is still there. */
|
||||
auto s = readLine(from.get());
|
||||
if (s == "restart") {
|
||||
pid = -1;
|
||||
continue;
|
||||
} else if (s != "next") {
|
||||
auto json = nlohmann::json::parse(s);
|
||||
throw Error("worker error: %s", (std::string) json["error"]);
|
||||
}
|
||||
|
||||
/* Wait for a job name to become available. */
|
||||
std::string attrPath;
|
||||
|
||||
while (true) {
|
||||
checkInterrupt();
|
||||
auto state(state_.lock());
|
||||
if ((state->todo.empty() && state->active.empty()) || state->exc) {
|
||||
writeLine(to.get(), "exit");
|
||||
return;
|
||||
}
|
||||
if (!state->todo.empty()) {
|
||||
attrPath = *state->todo.begin();
|
||||
state->todo.erase(state->todo.begin());
|
||||
state->active.insert(attrPath);
|
||||
break;
|
||||
} else
|
||||
state.wait(wakeup);
|
||||
}
|
||||
|
||||
/* Tell the worker to evaluate it. */
|
||||
writeLine(to.get(), "do " + attrPath);
|
||||
|
||||
/* Wait for the response. */
|
||||
auto response = nlohmann::json::parse(readLine(from.get()));
|
||||
|
||||
/* Handle the response. */
|
||||
StringSet newAttrs;
|
||||
|
||||
if (response.find("job") != response.end()) {
|
||||
auto state(state_.lock());
|
||||
state->jobs[attrPath] = response["job"];
|
||||
}
|
||||
|
||||
if (response.find("attrs") != response.end()) {
|
||||
for (auto & i : response["attrs"]) {
|
||||
auto s = (attrPath.empty() ? "" : attrPath + ".") + (std::string) i;
|
||||
newAttrs.insert(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.find("error") != response.end()) {
|
||||
auto state(state_.lock());
|
||||
state->jobs[attrPath]["error"] = response["error"];
|
||||
}
|
||||
|
||||
/* Add newly discovered job names to the queue. */
|
||||
{
|
||||
auto state(state_.lock());
|
||||
state->active.erase(attrPath);
|
||||
for (auto & s : newAttrs)
|
||||
state->todo.insert(s);
|
||||
wakeup.notify_all();
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
check_pid_status_nonblocking(pid);
|
||||
auto state(state_.lock());
|
||||
state->exc = std::current_exception();
|
||||
wakeup.notify_all();
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
for (size_t i = 0; i < nrWorkers; i++)
|
||||
threads.emplace_back(std::thread(handler));
|
||||
|
||||
for (auto & thread : threads)
|
||||
thread.join();
|
||||
|
||||
auto state(state_.lock());
|
||||
|
||||
if (state->exc)
|
||||
std::rethrow_exception(state->exc);
|
||||
|
||||
/* For aggregate jobs that have named consistuents
|
||||
(i.e. constituents that are a job name rather than a
|
||||
derivation), look up the referenced job and add it to the
|
||||
dependencies of the aggregate derivation. */
|
||||
auto store = openStore();
|
||||
|
||||
for (auto i = state->jobs.begin(); i != state->jobs.end(); ++i) {
|
||||
auto jobName = i.key();
|
||||
auto & job = i.value();
|
||||
|
||||
auto named = job.find("namedConstituents");
|
||||
if (named == job.end()) continue;
|
||||
|
||||
std::unordered_map<std::string, std::string> brokenJobs;
|
||||
auto getNonBrokenJobOrRecordError = [&brokenJobs, &jobName, &state](
|
||||
const std::string & childJobName) -> std::optional<nlohmann::json> {
|
||||
auto childJob = state->jobs.find(childJobName);
|
||||
if (childJob == state->jobs.end()) {
|
||||
printError("aggregate job '%s' references non-existent job '%s'", jobName, childJobName);
|
||||
brokenJobs[childJobName] = "does not exist";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (childJob->find("error") != childJob->end()) {
|
||||
std::string error = (*childJob)["error"];
|
||||
printError("aggregate job '%s' references broken job '%s': %s", jobName, childJobName, error);
|
||||
brokenJobs[childJobName] = error;
|
||||
return std::nullopt;
|
||||
}
|
||||
return *childJob;
|
||||
};
|
||||
|
||||
if (myArgs.dryRun) {
|
||||
for (std::string jobName2 : *named) {
|
||||
auto job2 = getNonBrokenJobOrRecordError(jobName2);
|
||||
if (!job2) {
|
||||
continue;
|
||||
}
|
||||
std::string drvPath2 = (*job2)["drvPath"];
|
||||
job["constituents"].push_back(drvPath2);
|
||||
}
|
||||
} else {
|
||||
auto drvPath = store->parseStorePath((std::string) job["drvPath"]);
|
||||
auto drv = store->readDerivation(drvPath);
|
||||
|
||||
for (std::string jobName2 : *named) {
|
||||
auto job2 = getNonBrokenJobOrRecordError(jobName2);
|
||||
if (!job2) {
|
||||
continue;
|
||||
}
|
||||
auto drvPath2 = store->parseStorePath((std::string) (*job2)["drvPath"]);
|
||||
auto drv2 = store->readDerivation(drvPath2);
|
||||
job["constituents"].push_back(store->printStorePath(drvPath2));
|
||||
drv.inputDrvs[drvPath2] = {drv2.outputs.begin()->first};
|
||||
}
|
||||
|
||||
if (brokenJobs.empty()) {
|
||||
std::string drvName(drvPath.name());
|
||||
assert(hasSuffix(drvName, drvExtension));
|
||||
drvName.resize(drvName.size() - drvExtension.size());
|
||||
|
||||
auto hashModulo = hashDerivationModulo(*store, drv, true);
|
||||
if (hashModulo.kind != DrvHash::Kind::Regular) continue;
|
||||
auto h = hashModulo.hashes.find("out");
|
||||
if (h == hashModulo.hashes.end()) continue;
|
||||
auto outPath = store->makeOutputPath("out", h->second, drvName);
|
||||
drv.env["out"] = store->printStorePath(outPath);
|
||||
drv.outputs.insert_or_assign("out", DerivationOutput::InputAddressed { .path = outPath });
|
||||
auto newDrvPath = store->printStorePath(writeDerivation(*store, drv));
|
||||
|
||||
debug("rewrote aggregate derivation %s -> %s", store->printStorePath(drvPath), newDrvPath);
|
||||
|
||||
job["drvPath"] = newDrvPath;
|
||||
job["outputs"]["out"] = store->printStorePath(outPath);
|
||||
}
|
||||
}
|
||||
|
||||
job.erase("namedConstituents");
|
||||
|
||||
/* Register the derivation as a GC root. !!! This
|
||||
registers roots for jobs that we may have already
|
||||
done. */
|
||||
auto localStore = store.dynamic_pointer_cast<LocalFSStore>();
|
||||
if (gcRootsDir != "" && localStore) {
|
||||
auto drvPath = job["drvPath"].get<std::string>();
|
||||
Path root = gcRootsDir + "/" + std::string(baseNameOf(drvPath));
|
||||
if (!pathExists(root))
|
||||
localStore->addPermRoot(localStore->parseStorePath(drvPath), root);
|
||||
}
|
||||
|
||||
if (!brokenJobs.empty()) {
|
||||
std::stringstream ss;
|
||||
for (const auto& [jobName, error] : brokenJobs) {
|
||||
ss << jobName << ": " << error << "\n";
|
||||
}
|
||||
job["error"] = ss.str();
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << state->jobs.dump(2) << "\n";
|
||||
});
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
bin_PROGRAMS = hydra-evaluator
|
||||
|
||||
hydra_evaluator_SOURCES = hydra-evaluator.cc
|
||||
hydra_evaluator_LDADD = $(NIX_LIBS) -lpqxx
|
||||
hydra_evaluator_CXXFLAGS = $(NIX_CFLAGS) -Wall -I ../libhydra -Wno-deprecated-declarations
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "db.hh"
|
||||
#include "hydra-config.hh"
|
||||
#include "pool.hh"
|
||||
#include "shared.hh"
|
||||
#include <nix/util/pool.hh>
|
||||
#include <nix/main/shared.hh>
|
||||
#include <nix/util/signals.hh>
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
@@ -37,7 +38,7 @@ class JobsetId {
|
||||
friend bool operator!= (const JobsetId & lhs, const JobsetName & rhs);
|
||||
|
||||
std::string display() const {
|
||||
return str(format("%1%:%2% (jobset#%3%)") % project % jobset % id);
|
||||
return boost::str(boost::format("%1%:%2% (jobset#%3%)") % project % jobset % id);
|
||||
}
|
||||
};
|
||||
bool operator==(const JobsetId & lhs, const JobsetId & rhs)
|
||||
@@ -179,10 +180,8 @@ struct Evaluator
|
||||
{
|
||||
auto conn(dbPool.get());
|
||||
pqxx::work txn(*conn);
|
||||
txn.exec_params0
|
||||
("update Jobsets set startTime = $1 where id = $2",
|
||||
now,
|
||||
jobset.name.id);
|
||||
txn.exec("update Jobsets set startTime = $1 where id = $2",
|
||||
pqxx::params{now, jobset.name.id}).no_rows();
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
@@ -233,7 +232,7 @@ struct Evaluator
|
||||
pqxx::work txn(*conn);
|
||||
|
||||
if (jobset.evaluation_style == EvaluationStyle::ONE_AT_A_TIME) {
|
||||
auto evaluation_res = txn.exec_params
|
||||
auto evaluation_res = txn.exec
|
||||
("select id from JobsetEvals "
|
||||
"where jobset_id = $1 "
|
||||
"order by id desc limit 1"
|
||||
@@ -249,7 +248,7 @@ struct Evaluator
|
||||
|
||||
auto evaluation_id = evaluation_res[0][0].as<int>();
|
||||
|
||||
auto unfinished_build_res = txn.exec_params
|
||||
auto unfinished_build_res = txn.exec
|
||||
("select id from Builds "
|
||||
"join JobsetEvalMembers "
|
||||
" on (JobsetEvalMembers.build = Builds.id) "
|
||||
@@ -366,6 +365,9 @@ struct Evaluator
|
||||
printInfo("received jobset event");
|
||||
}
|
||||
|
||||
} catch (pqxx::broken_connection & e) {
|
||||
printError("Database connection broken: %s", e.what());
|
||||
std::_Exit(1);
|
||||
} catch (std::exception & e) {
|
||||
printError("exception in database monitor thread: %s", e.what());
|
||||
sleep(30);
|
||||
@@ -416,21 +418,18 @@ struct Evaluator
|
||||
/* Clear the trigger time to prevent this
|
||||
jobset from getting stuck in an endless
|
||||
failing eval loop. */
|
||||
txn.exec_params0
|
||||
txn.exec
|
||||
("update Jobsets set triggerTime = null where id = $1 and startTime is not null and triggerTime <= startTime",
|
||||
jobset.name.id);
|
||||
jobset.name.id).no_rows();
|
||||
|
||||
/* Clear the start time. */
|
||||
txn.exec_params0
|
||||
txn.exec
|
||||
("update Jobsets set startTime = null where id = $1",
|
||||
jobset.name.id);
|
||||
jobset.name.id).no_rows();
|
||||
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) > 1) {
|
||||
txn.exec_params0
|
||||
("update Jobsets set errorMsg = $1, lastCheckedTime = $2, errorTime = $2, fetchErrorMsg = null where id = $3",
|
||||
fmt("evaluation %s", statusToString(status)),
|
||||
now,
|
||||
jobset.name.id);
|
||||
txn.exec("update Jobsets set errorMsg = $1, lastCheckedTime = $2, errorTime = $2, fetchErrorMsg = null where id = $3",
|
||||
pqxx::params{fmt("evaluation %s", statusToString(status)), now, jobset.name.id}).no_rows();
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
@@ -455,7 +454,7 @@ struct Evaluator
|
||||
{
|
||||
auto conn(dbPool.get());
|
||||
pqxx::work txn(*conn);
|
||||
txn.exec("update Jobsets set startTime = null");
|
||||
txn.exec("update Jobsets set startTime = null").no_rows();
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
@@ -473,6 +472,9 @@ struct Evaluator
|
||||
while (true) {
|
||||
try {
|
||||
loop();
|
||||
} catch (pqxx::broken_connection & e) {
|
||||
printError("Database connection broken: %s", e.what());
|
||||
std::_Exit(1);
|
||||
} catch (std::exception & e) {
|
||||
printError("exception in main loop: %s", e.what());
|
||||
sleep(30);
|
||||
|
||||
10
src/hydra-evaluator/meson.build
Normal file
10
src/hydra-evaluator/meson.build
Normal file
@@ -0,0 +1,10 @@
|
||||
hydra_evaluator = executable('hydra-evaluator',
|
||||
'hydra-evaluator.cc',
|
||||
dependencies: [
|
||||
libhydra_dep,
|
||||
nix_util_dep,
|
||||
nix_main_dep,
|
||||
pqxx_dep,
|
||||
],
|
||||
install: true,
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
bin_PROGRAMS = hydra-queue-runner
|
||||
|
||||
hydra_queue_runner_SOURCES = hydra-queue-runner.cc queue-monitor.cc dispatcher.cc \
|
||||
builder.cc build-result.cc build-remote.cc \
|
||||
hydra-build-result.hh counter.hh state.hh db.hh \
|
||||
nar-extractor.cc nar-extractor.hh
|
||||
hydra_queue_runner_LDADD = $(NIX_LIBS) -lpqxx -lprometheus-cpp-pull -lprometheus-cpp-core
|
||||
hydra_queue_runner_CXXFLAGS = $(NIX_CFLAGS) -Wall -I ../libhydra -Wno-deprecated-declarations
|
||||
@@ -5,107 +5,78 @@
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include "build-result.hh"
|
||||
#include "serve-protocol.hh"
|
||||
#include <nix/store/build-result.hh>
|
||||
#include <nix/store/path.hh>
|
||||
#include <nix/store/legacy-ssh-store.hh>
|
||||
#include <nix/store/serve-protocol.hh>
|
||||
#include <nix/store/serve-protocol-impl.hh>
|
||||
#include "state.hh"
|
||||
#include "util.hh"
|
||||
#include "worker-protocol.hh"
|
||||
#include "finally.hh"
|
||||
#include "url.hh"
|
||||
#include <nix/util/current-process.hh>
|
||||
#include <nix/util/processes.hh>
|
||||
#include <nix/util/util.hh>
|
||||
#include <nix/store/export-import.hh>
|
||||
#include <nix/store/serve-protocol.hh>
|
||||
#include <nix/store/serve-protocol-impl.hh>
|
||||
#include <nix/store/ssh.hh>
|
||||
#include <nix/util/finally.hh>
|
||||
#include <nix/util/url.hh>
|
||||
|
||||
using namespace nix;
|
||||
|
||||
|
||||
struct Child
|
||||
bool ::Machine::isLocalhost() const
|
||||
{
|
||||
Pid pid;
|
||||
AutoCloseFD to, from;
|
||||
};
|
||||
|
||||
|
||||
static void append(Strings & dst, const Strings & src)
|
||||
{
|
||||
dst.insert(dst.end(), src.begin(), src.end());
|
||||
return storeUri.params.empty() && std::visit(overloaded {
|
||||
[](const StoreReference::Auto &) {
|
||||
return true;
|
||||
},
|
||||
[](const StoreReference::Specified & s) {
|
||||
return
|
||||
(s.scheme == "local" || s.scheme == "unix") ||
|
||||
((s.scheme == "ssh" || s.scheme == "ssh-ng") &&
|
||||
s.authority == "localhost");
|
||||
},
|
||||
}, storeUri.variant);
|
||||
}
|
||||
|
||||
static Strings extraStoreArgs(std::string & machine)
|
||||
namespace nix::build_remote {
|
||||
|
||||
static std::unique_ptr<SSHMaster::Connection> openConnection(
|
||||
::Machine::ptr machine, SSHMaster & master)
|
||||
{
|
||||
Strings result;
|
||||
try {
|
||||
auto parsed = parseURL(machine);
|
||||
if (parsed.scheme != "ssh") {
|
||||
throw SysError("Currently, only (legacy-)ssh stores are supported!");
|
||||
}
|
||||
machine = parsed.authority.value_or("");
|
||||
auto remoteStore = parsed.query.find("remote-store");
|
||||
if (remoteStore != parsed.query.end()) {
|
||||
result = {"--store", shellEscape(remoteStore->second)};
|
||||
}
|
||||
} catch (BadURL &) {
|
||||
// We just try to continue with `machine->sshName` here for backwards compat.
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void openConnection(Machine::ptr machine, Path tmpDir, int stderrFD, Child & child)
|
||||
{
|
||||
std::string pgmName;
|
||||
Pipe to, from;
|
||||
to.create();
|
||||
from.create();
|
||||
|
||||
Strings argv;
|
||||
Strings command = {"nix-store", "--serve", "--write"};
|
||||
if (machine->isLocalhost()) {
|
||||
pgmName = "nix-store";
|
||||
argv = {"nix-store", "--builders", "", "--serve", "--write"};
|
||||
command.push_back("--builders");
|
||||
command.push_back("");
|
||||
} else {
|
||||
pgmName = "ssh";
|
||||
auto sshName = machine->sshName;
|
||||
Strings extraArgs = extraStoreArgs(sshName);
|
||||
argv = {"ssh", sshName};
|
||||
if (machine->sshKey != "") append(argv, {"-i", machine->sshKey});
|
||||
if (machine->sshPublicHostKey != "") {
|
||||
Path fileName = tmpDir + "/host-key";
|
||||
auto p = machine->sshName.find("@");
|
||||
std::string host = p != std::string::npos ? std::string(machine->sshName, p + 1) : machine->sshName;
|
||||
writeFile(fileName, host + " " + machine->sshPublicHostKey + "\n");
|
||||
append(argv, {"-oUserKnownHostsFile=" + fileName});
|
||||
auto remoteStore = machine->storeUri.params.find("remote-store");
|
||||
if (remoteStore != machine->storeUri.params.end()) {
|
||||
command.push_back("--store");
|
||||
command.push_back(escapeShellArgAlways(remoteStore->second));
|
||||
}
|
||||
append(argv,
|
||||
{ "-x", "-a", "-oBatchMode=yes", "-oConnectTimeout=60", "-oTCPKeepAlive=yes"
|
||||
, "--", "nix-store", "--serve", "--write" });
|
||||
append(argv, extraArgs);
|
||||
}
|
||||
|
||||
child.pid = startProcess([&]() {
|
||||
restoreProcessContext();
|
||||
|
||||
if (dup2(to.readSide.get(), STDIN_FILENO) == -1)
|
||||
throw SysError("cannot dup input pipe to stdin");
|
||||
|
||||
if (dup2(from.writeSide.get(), STDOUT_FILENO) == -1)
|
||||
throw SysError("cannot dup output pipe to stdout");
|
||||
|
||||
if (dup2(stderrFD, STDERR_FILENO) == -1)
|
||||
throw SysError("cannot dup stderr");
|
||||
|
||||
execvp(argv.front().c_str(), (char * *) stringsToCharPtrs(argv).data()); // FIXME: remove cast
|
||||
|
||||
throw SysError("cannot start %s", pgmName);
|
||||
auto ret = master.startCommand(std::move(command), {
|
||||
"-a", "-oBatchMode=yes", "-oConnectTimeout=60", "-oTCPKeepAlive=yes"
|
||||
});
|
||||
|
||||
to.readSide = -1;
|
||||
from.writeSide = -1;
|
||||
// XXX: determine the actual max value we can use from /proc.
|
||||
|
||||
child.to = to.writeSide.release();
|
||||
child.from = from.readSide.release();
|
||||
// FIXME: Should this be upstreamed into `startCommand` in Nix?
|
||||
|
||||
int pipesize = 1024 * 1024;
|
||||
|
||||
fcntl(ret->in.get(), F_SETPIPE_SZ, &pipesize);
|
||||
fcntl(ret->out.get(), F_SETPIPE_SZ, &pipesize);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
static void copyClosureTo(std::timed_mutex & sendMutex, Store & destStore,
|
||||
FdSource & from, FdSink & to, const StorePathSet & paths,
|
||||
bool useSubstitutes = false)
|
||||
static void copyClosureTo(
|
||||
::Machine::Connection & conn,
|
||||
Store & destStore,
|
||||
const StorePathSet & paths,
|
||||
SubstituteFlag useSubstitutes = NoSubstitute)
|
||||
{
|
||||
StorePathSet closure;
|
||||
destStore.computeFSClosure(paths, closure);
|
||||
@@ -115,13 +86,10 @@ static void copyClosureTo(std::timed_mutex & sendMutex, Store & destStore,
|
||||
garbage-collect paths that are already there. Optionally, ask
|
||||
the remote host to substitute missing paths. */
|
||||
// FIXME: substitute output pollutes our build log
|
||||
to << cmdQueryValidPaths << 1 << useSubstitutes;
|
||||
worker_proto::write(destStore, to, closure);
|
||||
to.flush();
|
||||
|
||||
/* Get back the set of paths that are already valid on the remote
|
||||
host. */
|
||||
auto present = worker_proto::read(destStore, from, Phantom<StorePathSet> {});
|
||||
auto present = conn.queryValidPaths(
|
||||
destStore, true, closure, useSubstitutes);
|
||||
|
||||
if (present.size() == closure.size()) return;
|
||||
|
||||
@@ -133,20 +101,20 @@ static void copyClosureTo(std::timed_mutex & sendMutex, Store & destStore,
|
||||
|
||||
printMsg(lvlDebug, "sending %d missing paths", missing.size());
|
||||
|
||||
std::unique_lock<std::timed_mutex> sendLock(sendMutex,
|
||||
std::unique_lock<std::timed_mutex> sendLock(conn.machine->state->sendLock,
|
||||
std::chrono::seconds(600));
|
||||
|
||||
to << cmdImportPaths;
|
||||
destStore.exportPaths(missing, to);
|
||||
to.flush();
|
||||
conn.to << ServeProto::Command::ImportPaths;
|
||||
exportPaths(destStore, missing, conn.to);
|
||||
conn.to.flush();
|
||||
|
||||
if (readInt(from) != 1)
|
||||
if (readInt(conn.from) != 1)
|
||||
throw Error("remote machine failed to import closure");
|
||||
}
|
||||
|
||||
|
||||
// FIXME: use Store::topoSortPaths().
|
||||
StorePaths reverseTopoSortPaths(const std::map<StorePath, ValidPathInfo> & paths)
|
||||
static StorePaths reverseTopoSortPaths(const std::map<StorePath, UnkeyedValidPathInfo> & paths)
|
||||
{
|
||||
StorePaths sorted;
|
||||
StorePathSet visited;
|
||||
@@ -174,40 +142,320 @@ StorePaths reverseTopoSortPaths(const std::map<StorePath, ValidPathInfo> & paths
|
||||
return sorted;
|
||||
}
|
||||
|
||||
static std::pair<Path, AutoCloseFD> openLogFile(const std::string & logDir, const StorePath & drvPath)
|
||||
{
|
||||
std::string base(drvPath.to_string());
|
||||
auto logFile = logDir + "/" + std::string(base, 0, 2) + "/" + std::string(base, 2);
|
||||
|
||||
createDirs(dirOf(logFile));
|
||||
|
||||
AutoCloseFD logFD = open(logFile.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0666);
|
||||
if (!logFD) throw SysError("creating log file ‘%s’", logFile);
|
||||
|
||||
return {std::move(logFile), std::move(logFD)};
|
||||
}
|
||||
|
||||
static BasicDerivation sendInputs(
|
||||
State & state,
|
||||
Step & step,
|
||||
Store & localStore,
|
||||
Store & destStore,
|
||||
::Machine::Connection & conn,
|
||||
unsigned int & overhead,
|
||||
counter & nrStepsWaiting,
|
||||
counter & nrStepsCopyingTo
|
||||
)
|
||||
{
|
||||
/* Replace the input derivations by their output paths to send a
|
||||
minimal closure to the builder.
|
||||
|
||||
`tryResolve` currently does *not* rewrite input addresses, so it
|
||||
is safe to do this in all cases. (It should probably have a mode
|
||||
to do that, however, but we would not use it here.)
|
||||
*/
|
||||
BasicDerivation basicDrv = ({
|
||||
auto maybeBasicDrv = step.drv->tryResolve(destStore, &localStore);
|
||||
if (!maybeBasicDrv)
|
||||
throw Error(
|
||||
"the derivation '%s' can’t be resolved. It’s probably "
|
||||
"missing some outputs",
|
||||
localStore.printStorePath(step.drvPath));
|
||||
*maybeBasicDrv;
|
||||
});
|
||||
|
||||
/* Ensure that the inputs exist in the destination store. This is
|
||||
a no-op for regular stores, but for the binary cache store,
|
||||
this will copy the inputs to the binary cache from the local
|
||||
store. */
|
||||
if (&localStore != &destStore) {
|
||||
copyClosure(localStore, destStore,
|
||||
step.drv->inputSrcs,
|
||||
NoRepair, NoCheckSigs, NoSubstitute);
|
||||
}
|
||||
|
||||
{
|
||||
auto mc1 = std::make_shared<MaintainCount<counter>>(nrStepsWaiting);
|
||||
mc1.reset();
|
||||
MaintainCount<counter> mc2(nrStepsCopyingTo);
|
||||
|
||||
printMsg(lvlDebug, "sending closure of ‘%s’ to ‘%s’",
|
||||
localStore.printStorePath(step.drvPath), conn.machine->storeUri.render());
|
||||
|
||||
auto now1 = std::chrono::steady_clock::now();
|
||||
|
||||
/* Copy the input closure. */
|
||||
if (conn.machine->isLocalhost()) {
|
||||
StorePathSet closure;
|
||||
destStore.computeFSClosure(basicDrv.inputSrcs, closure);
|
||||
copyPaths(destStore, localStore, closure, NoRepair, NoCheckSigs, NoSubstitute);
|
||||
} else {
|
||||
copyClosureTo(conn, destStore, basicDrv.inputSrcs, Substitute);
|
||||
}
|
||||
|
||||
auto now2 = std::chrono::steady_clock::now();
|
||||
|
||||
overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
||||
}
|
||||
|
||||
return basicDrv;
|
||||
}
|
||||
|
||||
static BuildResult performBuild(
|
||||
::Machine::Connection & conn,
|
||||
Store & localStore,
|
||||
StorePath drvPath,
|
||||
const BasicDerivation & drv,
|
||||
const ServeProto::BuildOptions & options,
|
||||
counter & nrStepsBuilding
|
||||
)
|
||||
{
|
||||
conn.putBuildDerivationRequest(localStore, drvPath, drv, options);
|
||||
|
||||
BuildResult result;
|
||||
|
||||
time_t startTime, stopTime;
|
||||
|
||||
startTime = time(0);
|
||||
{
|
||||
MaintainCount<counter> mc(nrStepsBuilding);
|
||||
result = ServeProto::Serialise<BuildResult>::read(localStore, conn);
|
||||
}
|
||||
stopTime = time(0);
|
||||
|
||||
if (!result.startTime) {
|
||||
// If the builder gave `startTime = 0`, use our measurements
|
||||
// instead of the builder's.
|
||||
//
|
||||
// Note: this represents the duration of a single round, rather
|
||||
// than all rounds.
|
||||
result.startTime = startTime;
|
||||
result.stopTime = stopTime;
|
||||
}
|
||||
|
||||
// If the protocol was too old to give us `builtOutputs`, initialize
|
||||
// it manually by introspecting the derivation.
|
||||
if (GET_PROTOCOL_MINOR(conn.remoteVersion) < 6)
|
||||
{
|
||||
// If the remote is too old to handle CA derivations, we can’t get this
|
||||
// far anyways
|
||||
assert(drv.type().hasKnownOutputPaths());
|
||||
DerivationOutputsAndOptPaths drvOutputs = drv.outputsAndOptPaths(localStore);
|
||||
// Since this a `BasicDerivation`, `staticOutputHashes` will not
|
||||
// do any real work.
|
||||
auto outputHashes = staticOutputHashes(localStore, drv);
|
||||
if (auto * successP = result.tryGetSuccess()) {
|
||||
for (auto & [outputName, output] : drvOutputs) {
|
||||
auto outputPath = output.second;
|
||||
// We’ve just asserted that the output paths of the derivation
|
||||
// were known
|
||||
assert(outputPath);
|
||||
auto outputHash = outputHashes.at(outputName);
|
||||
auto drvOutput = DrvOutput { outputHash, outputName };
|
||||
successP->builtOutputs.insert_or_assign(
|
||||
std::move(outputName),
|
||||
Realisation { drvOutput, *outputPath });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void copyPathFromRemote(
|
||||
::Machine::Connection & conn,
|
||||
NarMemberDatas & narMembers,
|
||||
Store & localStore,
|
||||
Store & destStore,
|
||||
const ValidPathInfo & info
|
||||
)
|
||||
{
|
||||
/* Receive the NAR from the remote and add it to the
|
||||
destination store. Meanwhile, extract all the info from the
|
||||
NAR that getBuildOutput() needs. */
|
||||
auto source2 = sinkToSource([&](Sink & sink)
|
||||
{
|
||||
/* Note: we should only send the command to dump the store
|
||||
path to the remote if the NAR is actually going to get read
|
||||
by the destination store, which won't happen if this path
|
||||
is already valid on the destination store. Since this
|
||||
lambda function only gets executed if someone tries to read
|
||||
from source2, we will send the command from here rather
|
||||
than outside the lambda. */
|
||||
conn.to << ServeProto::Command::DumpStorePath << localStore.printStorePath(info.path);
|
||||
conn.to.flush();
|
||||
|
||||
TeeSource tee(conn.from, sink);
|
||||
extractNarData(tee, localStore.printStorePath(info.path), narMembers);
|
||||
});
|
||||
|
||||
destStore.addToStore(info, *source2, NoRepair, NoCheckSigs);
|
||||
}
|
||||
|
||||
static void copyPathsFromRemote(
|
||||
::Machine::Connection & conn,
|
||||
NarMemberDatas & narMembers,
|
||||
Store & localStore,
|
||||
Store & destStore,
|
||||
const std::map<StorePath, UnkeyedValidPathInfo> & infos
|
||||
)
|
||||
{
|
||||
auto pathsSorted = reverseTopoSortPaths(infos);
|
||||
|
||||
for (auto & path : pathsSorted) {
|
||||
auto & info = infos.find(path)->second;
|
||||
copyPathFromRemote(
|
||||
conn, narMembers, localStore, destStore,
|
||||
ValidPathInfo { path, info });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* using namespace nix::build_remote; */
|
||||
|
||||
void RemoteResult::updateWithBuildResult(const nix::BuildResult & buildResult)
|
||||
{
|
||||
startTime = buildResult.startTime;
|
||||
stopTime = buildResult.stopTime;
|
||||
timesBuilt = buildResult.timesBuilt;
|
||||
|
||||
std::visit(overloaded{
|
||||
[&](const BuildResult::Success & success) {
|
||||
stepStatus = bsSuccess;
|
||||
switch (success.status) {
|
||||
case BuildResult::Success::Built:
|
||||
break;
|
||||
case BuildResult::Success::Substituted:
|
||||
case BuildResult::Success::AlreadyValid:
|
||||
case BuildResult::Success::ResolvesToAlreadyValid:
|
||||
isCached = true;
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
},
|
||||
[&](const BuildResult::Failure & failure) {
|
||||
errorMsg = failure.errorMsg;
|
||||
isNonDeterministic = failure.isNonDeterministic;
|
||||
switch (failure.status) {
|
||||
case BuildResult::Failure::PermanentFailure:
|
||||
stepStatus = bsFailed;
|
||||
canCache = true;
|
||||
errorMsg = "";
|
||||
break;
|
||||
case BuildResult::Failure::InputRejected:
|
||||
case BuildResult::Failure::OutputRejected:
|
||||
stepStatus = bsFailed;
|
||||
canCache = true;
|
||||
break;
|
||||
case BuildResult::Failure::TransientFailure:
|
||||
stepStatus = bsFailed;
|
||||
canRetry = true;
|
||||
errorMsg = "";
|
||||
break;
|
||||
case BuildResult::Failure::TimedOut:
|
||||
stepStatus = bsTimedOut;
|
||||
errorMsg = "";
|
||||
break;
|
||||
case BuildResult::Failure::MiscFailure:
|
||||
stepStatus = bsAborted;
|
||||
canRetry = true;
|
||||
break;
|
||||
case BuildResult::Failure::LogLimitExceeded:
|
||||
stepStatus = bsLogLimitExceeded;
|
||||
break;
|
||||
case BuildResult::Failure::NotDeterministic:
|
||||
stepStatus = bsNotDeterministic;
|
||||
canRetry = false;
|
||||
canCache = true;
|
||||
break;
|
||||
case BuildResult::Failure::CachedFailure:
|
||||
case BuildResult::Failure::DependencyFailed:
|
||||
case BuildResult::Failure::NoSubstituters:
|
||||
case BuildResult::Failure::HashMismatch:
|
||||
stepStatus = bsAborted;
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
},
|
||||
}, buildResult.inner);
|
||||
}
|
||||
|
||||
/* Utility guard object to auto-release a semaphore on destruction. */
|
||||
template <typename T>
|
||||
class SemaphoreReleaser {
|
||||
public:
|
||||
SemaphoreReleaser(T* s) : sem(s) {}
|
||||
~SemaphoreReleaser() { sem->release(); }
|
||||
|
||||
private:
|
||||
T* sem;
|
||||
};
|
||||
|
||||
void State::buildRemote(ref<Store> destStore,
|
||||
Machine::ptr machine, Step::ptr step,
|
||||
unsigned int maxSilentTime, unsigned int buildTimeout, unsigned int repeats,
|
||||
std::unique_ptr<MachineReservation> reservation,
|
||||
::Machine::ptr machine, Step::ptr step,
|
||||
const ServeProto::BuildOptions & buildOptions,
|
||||
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
|
||||
std::function<void(StepState)> updateStep,
|
||||
NarMemberDatas & narMembers)
|
||||
{
|
||||
assert(BuildResult::TimedOut == 8);
|
||||
assert(BuildResult::Failure::TimedOut == 8);
|
||||
|
||||
std::string base(step->drvPath.to_string());
|
||||
result.logFile = logDir + "/" + std::string(base, 0, 2) + "/" + std::string(base, 2);
|
||||
AutoDelete autoDelete(result.logFile, false);
|
||||
|
||||
createDirs(dirOf(result.logFile));
|
||||
|
||||
AutoCloseFD logFD = open(result.logFile.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0666);
|
||||
if (!logFD) throw SysError("creating log file ‘%s’", result.logFile);
|
||||
|
||||
nix::Path tmpDir = createTempDir();
|
||||
AutoDelete tmpDirDel(tmpDir, true);
|
||||
auto [logFile, logFD] = build_remote::openLogFile(logDir, step->drvPath);
|
||||
AutoDelete logFileDel(logFile, false);
|
||||
result.logFile = logFile;
|
||||
|
||||
try {
|
||||
|
||||
updateStep(ssConnecting);
|
||||
|
||||
auto storeRef = machine->completeStoreReference();
|
||||
|
||||
auto * pSpecified = std::get_if<StoreReference::Specified>(&storeRef.variant);
|
||||
if (!pSpecified || pSpecified->scheme != "ssh") {
|
||||
throw Error("Currently, only (legacy-)ssh stores are supported!");
|
||||
}
|
||||
|
||||
LegacySSHStoreConfig storeConfig {
|
||||
pSpecified->scheme,
|
||||
pSpecified->authority,
|
||||
storeRef.params
|
||||
};
|
||||
|
||||
auto master = storeConfig.createSSHMaster(
|
||||
false, // no SSH master yet
|
||||
logFD.get());
|
||||
|
||||
// FIXME: rewrite to use Store.
|
||||
Child child;
|
||||
openConnection(machine, tmpDir, logFD.get(), child);
|
||||
auto child = build_remote::openConnection(machine, master);
|
||||
|
||||
{
|
||||
auto activeStepState(activeStep->state_.lock());
|
||||
if (activeStepState->cancelled) throw Error("step cancelled");
|
||||
activeStepState->pid = child.pid;
|
||||
activeStepState->pid = child->sshPid;
|
||||
}
|
||||
|
||||
Finally clearPid([&]() {
|
||||
@@ -222,34 +470,33 @@ void State::buildRemote(ref<Store> destStore,
|
||||
process. Meh. */
|
||||
});
|
||||
|
||||
FdSource from(child.from.get());
|
||||
FdSink to(child.to.get());
|
||||
::Machine::Connection conn {
|
||||
{
|
||||
.to = child->in.get(),
|
||||
.from = child->out.get(),
|
||||
/* Handshake. */
|
||||
.remoteVersion = 0xdadbeef, // FIXME avoid dummy initialize
|
||||
},
|
||||
/*.machine =*/ machine,
|
||||
};
|
||||
|
||||
Finally updateStats([&]() {
|
||||
bytesReceived += from.read;
|
||||
bytesSent += to.written;
|
||||
bytesReceived += conn.from.read;
|
||||
bytesSent += conn.to.written;
|
||||
});
|
||||
|
||||
/* Handshake. */
|
||||
unsigned int remoteVersion;
|
||||
constexpr ServeProto::Version our_version = 0x206;
|
||||
|
||||
try {
|
||||
to << SERVE_MAGIC_1 << 0x206;
|
||||
to.flush();
|
||||
|
||||
unsigned int magic = readInt(from);
|
||||
if (magic != SERVE_MAGIC_2)
|
||||
throw Error("protocol mismatch with ‘nix-store --serve’ on ‘%1%’", machine->sshName);
|
||||
remoteVersion = readInt(from);
|
||||
if (GET_PROTOCOL_MAJOR(remoteVersion) != 0x200)
|
||||
throw Error("unsupported ‘nix-store --serve’ protocol version on ‘%1%’", machine->sshName);
|
||||
if (GET_PROTOCOL_MINOR(remoteVersion) < 3 && repeats > 0)
|
||||
throw Error("machine ‘%1%’ does not support repeating a build; please upgrade it to Nix 1.12", machine->sshName);
|
||||
|
||||
conn.remoteVersion = decltype(conn)::handshake(
|
||||
conn.to,
|
||||
conn.from,
|
||||
our_version,
|
||||
machine->storeUri.render());
|
||||
} catch (EndOfFile & e) {
|
||||
child.pid.wait();
|
||||
child->sshPid.wait();
|
||||
std::string s = chomp(readFile(result.logFile));
|
||||
throw Error("cannot connect to ‘%1%’: %2%", machine->sshName, s);
|
||||
throw Error("cannot connect to ‘%1%’: %2%", machine->storeUri.render(), s);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -263,62 +510,12 @@ void State::buildRemote(ref<Store> destStore,
|
||||
copy the immediate sources of the derivation and the required
|
||||
outputs of the input derivations. */
|
||||
updateStep(ssSendingInputs);
|
||||
BasicDerivation resolvedDrv = build_remote::sendInputs(*this, *step, *localStore, *destStore, conn, result.overhead, nrStepsWaiting, nrStepsCopyingTo);
|
||||
|
||||
StorePathSet inputs;
|
||||
BasicDerivation basicDrv(*step->drv);
|
||||
|
||||
for (auto & p : step->drv->inputSrcs)
|
||||
inputs.insert(p);
|
||||
|
||||
for (auto & input : step->drv->inputDrvs) {
|
||||
auto drv2 = localStore->readDerivation(input.first);
|
||||
for (auto & name : input.second) {
|
||||
if (auto i = get(drv2.outputs, name)) {
|
||||
auto outPath = i->path(*localStore, drv2.name, name);
|
||||
inputs.insert(*outPath);
|
||||
basicDrv.inputSrcs.insert(*outPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure that the inputs exist in the destination store. This is
|
||||
a no-op for regular stores, but for the binary cache store,
|
||||
this will copy the inputs to the binary cache from the local
|
||||
store. */
|
||||
if (localStore != std::shared_ptr<Store>(destStore)) {
|
||||
copyClosure(*localStore, *destStore,
|
||||
step->drv->inputSrcs,
|
||||
NoRepair, NoCheckSigs, NoSubstitute);
|
||||
}
|
||||
|
||||
{
|
||||
auto mc1 = std::make_shared<MaintainCount<counter>>(nrStepsWaiting);
|
||||
mc1.reset();
|
||||
MaintainCount<counter> mc2(nrStepsCopyingTo);
|
||||
|
||||
printMsg(lvlDebug, "sending closure of ‘%s’ to ‘%s’",
|
||||
localStore->printStorePath(step->drvPath), machine->sshName);
|
||||
|
||||
auto now1 = std::chrono::steady_clock::now();
|
||||
|
||||
/* Copy the input closure. */
|
||||
if (machine->isLocalhost()) {
|
||||
StorePathSet closure;
|
||||
destStore->computeFSClosure(inputs, closure);
|
||||
copyPaths(*destStore, *localStore, closure, NoRepair, NoCheckSigs, NoSubstitute);
|
||||
} else {
|
||||
copyClosureTo(machine->state->sendLock, *destStore, from, to, inputs, true);
|
||||
}
|
||||
|
||||
auto now2 = std::chrono::steady_clock::now();
|
||||
|
||||
result.overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
||||
}
|
||||
|
||||
autoDelete.cancel();
|
||||
logFileDel.cancel();
|
||||
|
||||
/* Truncate the log to get rid of messages about substitutions
|
||||
etc. on the remote system. */
|
||||
etc. on the remote system. */
|
||||
if (lseek(logFD.get(), SEEK_SET, 0) != 0)
|
||||
throw SysError("seeking to the start of log file ‘%s’", result.logFile);
|
||||
|
||||
@@ -330,89 +527,21 @@ void State::buildRemote(ref<Store> destStore,
|
||||
/* Do the build. */
|
||||
printMsg(lvlDebug, "building ‘%s’ on ‘%s’",
|
||||
localStore->printStorePath(step->drvPath),
|
||||
machine->sshName);
|
||||
machine->storeUri.render());
|
||||
|
||||
updateStep(ssBuilding);
|
||||
|
||||
to << cmdBuildDerivation << localStore->printStorePath(step->drvPath);
|
||||
writeDerivation(to, *localStore, basicDrv);
|
||||
to << maxSilentTime << buildTimeout;
|
||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 2)
|
||||
to << maxLogSize;
|
||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 3) {
|
||||
to << repeats // == build-repeat
|
||||
<< step->isDeterministic; // == enforce-determinism
|
||||
}
|
||||
to.flush();
|
||||
auto buildResult = build_remote::performBuild(
|
||||
conn,
|
||||
*localStore,
|
||||
step->drvPath,
|
||||
resolvedDrv,
|
||||
buildOptions,
|
||||
nrStepsBuilding
|
||||
);
|
||||
|
||||
result.startTime = time(0);
|
||||
int res;
|
||||
{
|
||||
MaintainCount<counter> mc(nrStepsBuilding);
|
||||
res = readInt(from);
|
||||
}
|
||||
result.stopTime = time(0);
|
||||
result.updateWithBuildResult(buildResult);
|
||||
|
||||
result.errorMsg = readString(from);
|
||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 3) {
|
||||
result.timesBuilt = readInt(from);
|
||||
result.isNonDeterministic = readInt(from);
|
||||
auto start = readInt(from);
|
||||
auto stop = readInt(from);
|
||||
if (start && start) {
|
||||
/* Note: this represents the duration of a single
|
||||
round, rather than all rounds. */
|
||||
result.startTime = start;
|
||||
result.stopTime = stop;
|
||||
}
|
||||
}
|
||||
if (GET_PROTOCOL_MINOR(remoteVersion) >= 6) {
|
||||
worker_proto::read(*localStore, from, Phantom<DrvOutputs> {});
|
||||
}
|
||||
switch ((BuildResult::Status) res) {
|
||||
case BuildResult::Built:
|
||||
result.stepStatus = bsSuccess;
|
||||
break;
|
||||
case BuildResult::Substituted:
|
||||
case BuildResult::AlreadyValid:
|
||||
result.stepStatus = bsSuccess;
|
||||
result.isCached = true;
|
||||
break;
|
||||
case BuildResult::PermanentFailure:
|
||||
result.stepStatus = bsFailed;
|
||||
result.canCache = true;
|
||||
result.errorMsg = "";
|
||||
break;
|
||||
case BuildResult::InputRejected:
|
||||
case BuildResult::OutputRejected:
|
||||
result.stepStatus = bsFailed;
|
||||
result.canCache = true;
|
||||
break;
|
||||
case BuildResult::TransientFailure:
|
||||
result.stepStatus = bsFailed;
|
||||
result.canRetry = true;
|
||||
result.errorMsg = "";
|
||||
break;
|
||||
case BuildResult::TimedOut:
|
||||
result.stepStatus = bsTimedOut;
|
||||
result.errorMsg = "";
|
||||
break;
|
||||
case BuildResult::MiscFailure:
|
||||
result.stepStatus = bsAborted;
|
||||
result.canRetry = true;
|
||||
break;
|
||||
case BuildResult::LogLimitExceeded:
|
||||
result.stepStatus = bsLogLimitExceeded;
|
||||
break;
|
||||
case BuildResult::NotDeterministic:
|
||||
result.stepStatus = bsNotDeterministic;
|
||||
result.canRetry = false;
|
||||
result.canCache = true;
|
||||
break;
|
||||
default:
|
||||
result.stepStatus = bsAborted;
|
||||
break;
|
||||
}
|
||||
if (result.stepStatus != bsSuccess) return;
|
||||
|
||||
result.errorMsg = "";
|
||||
@@ -421,11 +550,33 @@ void State::buildRemote(ref<Store> destStore,
|
||||
get a build log. */
|
||||
if (result.isCached) {
|
||||
printMsg(lvlInfo, "outputs of ‘%s’ substituted or already valid on ‘%s’",
|
||||
localStore->printStorePath(step->drvPath), machine->sshName);
|
||||
localStore->printStorePath(step->drvPath), machine->storeUri.render());
|
||||
unlink(result.logFile.c_str());
|
||||
result.logFile = "";
|
||||
}
|
||||
|
||||
/* Throttle CPU-bound work. Opportunistically skip updating the current
|
||||
* step, since this requires a DB roundtrip. */
|
||||
if (!localWorkThrottler.try_acquire()) {
|
||||
MaintainCount<counter> mc(nrStepsWaitingForDownloadSlot);
|
||||
updateStep(ssWaitingForLocalSlot);
|
||||
localWorkThrottler.acquire();
|
||||
}
|
||||
SemaphoreReleaser releaser(&localWorkThrottler);
|
||||
|
||||
/* Once we've started copying outputs, release the machine reservation
|
||||
* so further builds can happen. We do not release the machine earlier
|
||||
* to avoid situations where the queue runner is bottlenecked on
|
||||
* copying outputs and we end up building too many things that we
|
||||
* haven't been able to allow copy slots for. */
|
||||
reservation.reset();
|
||||
wakeDispatcher();
|
||||
|
||||
StorePathSet outputs;
|
||||
if (auto * successP = buildResult.tryGetSuccess())
|
||||
for (auto & [_, realisation] : successP->builtOutputs)
|
||||
outputs.insert(realisation.outPath);
|
||||
|
||||
/* Copy the output paths. */
|
||||
if (!machine->isLocalhost() || localStore != std::shared_ptr<Store>(destStore)) {
|
||||
updateStep(ssReceivingOutputs);
|
||||
@@ -434,39 +585,10 @@ void State::buildRemote(ref<Store> destStore,
|
||||
|
||||
auto now1 = std::chrono::steady_clock::now();
|
||||
|
||||
StorePathSet outputs;
|
||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore)) {
|
||||
if (i.second.second)
|
||||
outputs.insert(*i.second.second);
|
||||
}
|
||||
auto infos = conn.queryPathInfos(*localStore, outputs);
|
||||
|
||||
/* Get info about each output path. */
|
||||
std::map<StorePath, ValidPathInfo> infos;
|
||||
size_t totalNarSize = 0;
|
||||
to << cmdQueryPathInfos;
|
||||
worker_proto::write(*localStore, to, outputs);
|
||||
to.flush();
|
||||
while (true) {
|
||||
auto storePathS = readString(from);
|
||||
if (storePathS == "") break;
|
||||
auto deriver = readString(from); // deriver
|
||||
auto references = worker_proto::read(*localStore, from, Phantom<StorePathSet> {});
|
||||
readLongLong(from); // download size
|
||||
auto narSize = readLongLong(from);
|
||||
auto narHash = Hash::parseAny(readString(from), htSHA256);
|
||||
auto ca = parseContentAddressOpt(readString(from));
|
||||
readStrings<StringSet>(from); // sigs
|
||||
ValidPathInfo info(localStore->parseStorePath(storePathS), narHash);
|
||||
assert(outputs.count(info.path));
|
||||
info.references = references;
|
||||
info.narSize = narSize;
|
||||
totalNarSize += info.narSize;
|
||||
info.narHash = narHash;
|
||||
info.ca = ca;
|
||||
if (deriver != "")
|
||||
info.deriver = localStore->parseStorePath(deriver);
|
||||
infos.insert_or_assign(info.path, info);
|
||||
}
|
||||
for (auto & [_, info] : infos) totalNarSize += info.narSize;
|
||||
|
||||
if (totalNarSize > maxOutputSize) {
|
||||
result.stepStatus = bsNarSizeLimitExceeded;
|
||||
@@ -475,43 +597,34 @@ void State::buildRemote(ref<Store> destStore,
|
||||
|
||||
/* Copy each path. */
|
||||
printMsg(lvlDebug, "copying outputs of ‘%s’ from ‘%s’ (%d bytes)",
|
||||
localStore->printStorePath(step->drvPath), machine->sshName, totalNarSize);
|
||||
|
||||
auto pathsSorted = reverseTopoSortPaths(infos);
|
||||
|
||||
for (auto & path : pathsSorted) {
|
||||
auto & info = infos.find(path)->second;
|
||||
|
||||
/* Receive the NAR from the remote and add it to the
|
||||
destination store. Meanwhile, extract all the info from the
|
||||
NAR that getBuildOutput() needs. */
|
||||
auto source2 = sinkToSource([&](Sink & sink)
|
||||
{
|
||||
/* Note: we should only send the command to dump the store
|
||||
path to the remote if the NAR is actually going to get read
|
||||
by the destination store, which won't happen if this path
|
||||
is already valid on the destination store. Since this
|
||||
lambda function only gets executed if someone tries to read
|
||||
from source2, we will send the command from here rather
|
||||
than outside the lambda. */
|
||||
to << cmdDumpStorePath << localStore->printStorePath(path);
|
||||
to.flush();
|
||||
|
||||
TeeSource tee(from, sink);
|
||||
extractNarData(tee, localStore->printStorePath(path), narMembers);
|
||||
});
|
||||
|
||||
destStore->addToStore(info, *source2, NoRepair, NoCheckSigs);
|
||||
}
|
||||
localStore->printStorePath(step->drvPath), machine->storeUri.render(), totalNarSize);
|
||||
|
||||
build_remote::copyPathsFromRemote(conn, narMembers, *localStore, *destStore, infos);
|
||||
auto now2 = std::chrono::steady_clock::now();
|
||||
|
||||
result.overhead += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
||||
}
|
||||
|
||||
/* Register the outputs of the newly built drv */
|
||||
if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) {
|
||||
auto outputHashes = staticOutputHashes(*localStore, *step->drv);
|
||||
if (auto * successP = buildResult.tryGetSuccess()) {
|
||||
for (auto & [outputName, realisation] : successP->builtOutputs) {
|
||||
// Register the resolved drv output
|
||||
destStore->registerDrvOutput(realisation);
|
||||
|
||||
// Also register the unresolved one
|
||||
auto unresolvedRealisation = realisation;
|
||||
unresolvedRealisation.signatures.clear();
|
||||
unresolvedRealisation.id.drvHash = outputHashes.at(outputName);
|
||||
destStore->registerDrvOutput(unresolvedRealisation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Shut down the connection. */
|
||||
child.to = -1;
|
||||
child.pid.wait();
|
||||
child->in = -1;
|
||||
child->sshPid.wait();
|
||||
|
||||
} catch (Error & e) {
|
||||
/* Disable this machine until a certain period of time has
|
||||
@@ -525,7 +638,7 @@ void State::buildRemote(ref<Store> destStore,
|
||||
info->consecutiveFailures = std::min(info->consecutiveFailures + 1, (unsigned int) 4);
|
||||
info->lastFailure = now;
|
||||
int delta = retryInterval * std::pow(retryBackoff, info->consecutiveFailures - 1) + (rand() % 30);
|
||||
printMsg(lvlInfo, "will disable machine ‘%1%’ for %2%s", machine->sshName, delta);
|
||||
printMsg(lvlInfo, "will disable machine ‘%1%’ for %2%s", machine->storeUri.render(), delta);
|
||||
info->disabledUntil = now + std::chrono::seconds(delta);
|
||||
}
|
||||
throw;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "hydra-build-result.hh"
|
||||
#include "store-api.hh"
|
||||
#include "util.hh"
|
||||
#include "fs-accessor.hh"
|
||||
#include <nix/store/store-api.hh>
|
||||
#include <nix/util/util.hh>
|
||||
#include <nix/util/source-accessor.hh>
|
||||
|
||||
#include <regex>
|
||||
|
||||
@@ -11,18 +11,18 @@ using namespace nix;
|
||||
BuildOutput getBuildOutput(
|
||||
nix::ref<Store> store,
|
||||
NarMemberDatas & narMembers,
|
||||
const Derivation & drv)
|
||||
const OutputPathMap derivationOutputs)
|
||||
{
|
||||
BuildOutput res;
|
||||
|
||||
/* Compute the closure size. */
|
||||
StorePathSet outputs;
|
||||
StorePathSet closure;
|
||||
for (auto & i : drv.outputsAndOptPaths(*store))
|
||||
if (i.second.second) {
|
||||
store->computeFSClosure(*i.second.second, closure);
|
||||
outputs.insert(*i.second.second);
|
||||
}
|
||||
for (auto& [outputName, outputPath] : derivationOutputs) {
|
||||
store->computeFSClosure(outputPath, closure);
|
||||
outputs.insert(outputPath);
|
||||
res.outputs.insert({outputName, outputPath});
|
||||
}
|
||||
for (auto & path : closure) {
|
||||
auto info = store->queryPathInfo(path);
|
||||
res.closureSize += info->narSize;
|
||||
@@ -51,8 +51,8 @@ BuildOutput getBuildOutput(
|
||||
"[[:space:]]+"
|
||||
"([a-zA-Z0-9_-]+)" // subtype (e.g. "readme")
|
||||
"[[:space:]]+"
|
||||
"(\"[^\"]+\"|[^[:space:]\"]+)" // path (may be quoted)
|
||||
"([[:space:]]+([^[:space:]]+))?" // entry point
|
||||
"(\"[^\"]+\"|[^[:space:]<>\"]+)" // path (may be quoted)
|
||||
"([[:space:]]+([^[:space:]<>]+))?" // entry point
|
||||
, std::regex::extended);
|
||||
|
||||
for (auto & output : outputs) {
|
||||
@@ -63,7 +63,7 @@ BuildOutput getBuildOutput(
|
||||
|
||||
auto productsFile = narMembers.find(outputS + "/nix-support/hydra-build-products");
|
||||
if (productsFile == narMembers.end() ||
|
||||
productsFile->second.type != FSAccessor::Type::tRegular)
|
||||
productsFile->second.type != SourceAccessor::Type::tRegular)
|
||||
continue;
|
||||
assert(productsFile->second.contents);
|
||||
|
||||
@@ -78,7 +78,7 @@ BuildOutput getBuildOutput(
|
||||
product.type = match[1];
|
||||
product.subtype = match[2];
|
||||
std::string s(match[3]);
|
||||
product.path = s[0] == '"' ? std::string(s, 1, s.size() - 2) : s;
|
||||
product.path = s[0] == '"' && s.back() == '"' ? std::string(s, 1, s.size() - 2) : s;
|
||||
product.defaultPath = match[5];
|
||||
|
||||
/* Ensure that the path exists and points into the Nix
|
||||
@@ -93,8 +93,10 @@ BuildOutput getBuildOutput(
|
||||
if (file == narMembers.end()) continue;
|
||||
|
||||
product.name = product.path == store->printStorePath(output) ? "" : baseNameOf(product.path);
|
||||
if (!std::regex_match(product.name, std::regex("[a-zA-Z0-9.@:_ -]*")))
|
||||
product.name = "";
|
||||
|
||||
if (file->second.type == FSAccessor::Type::tRegular) {
|
||||
if (file->second.type == SourceAccessor::Type::tRegular) {
|
||||
product.isRegular = true;
|
||||
product.fileSize = file->second.fileSize.value();
|
||||
product.sha256hash = file->second.sha256.value();
|
||||
@@ -107,17 +109,16 @@ BuildOutput getBuildOutput(
|
||||
/* If no build products were explicitly declared, then add all
|
||||
outputs as a product of type "nix-build". */
|
||||
if (!explicitProducts) {
|
||||
for (auto & [name, output] : drv.outputs) {
|
||||
for (auto & [name, output] : derivationOutputs) {
|
||||
BuildProduct product;
|
||||
auto outPath = output.path(*store, drv.name, name);
|
||||
product.path = store->printStorePath(*outPath);
|
||||
product.path = store->printStorePath(output);
|
||||
product.type = "nix-build";
|
||||
product.subtype = name == "out" ? "" : name;
|
||||
product.name = outPath->name();
|
||||
product.name = output.name();
|
||||
|
||||
auto file = narMembers.find(product.path);
|
||||
assert(file != narMembers.end());
|
||||
if (file->second.type == FSAccessor::Type::tDirectory)
|
||||
if (file->second.type == SourceAccessor::Type::tDirectory)
|
||||
res.products.push_back(product);
|
||||
}
|
||||
}
|
||||
@@ -126,25 +127,34 @@ BuildOutput getBuildOutput(
|
||||
for (auto & output : outputs) {
|
||||
auto file = narMembers.find(store->printStorePath(output) + "/nix-support/hydra-release-name");
|
||||
if (file == narMembers.end() ||
|
||||
file->second.type != FSAccessor::Type::tRegular)
|
||||
file->second.type != SourceAccessor::Type::tRegular)
|
||||
continue;
|
||||
res.releaseName = trim(file->second.contents.value());
|
||||
// FIXME: validate release name
|
||||
auto contents = trim(file->second.contents.value());
|
||||
if (std::regex_match(contents, std::regex("[a-zA-Z0-9.@:_-]+")))
|
||||
res.releaseName = contents;
|
||||
}
|
||||
|
||||
/* Get metrics. */
|
||||
for (auto & output : outputs) {
|
||||
auto file = narMembers.find(store->printStorePath(output) + "/nix-support/hydra-metrics");
|
||||
if (file == narMembers.end() ||
|
||||
file->second.type != FSAccessor::Type::tRegular)
|
||||
file->second.type != SourceAccessor::Type::tRegular)
|
||||
continue;
|
||||
for (auto & line : tokenizeString<Strings>(file->second.contents.value(), "\n")) {
|
||||
auto fields = tokenizeString<std::vector<std::string>>(line);
|
||||
if (fields.size() < 2) continue;
|
||||
if (!std::regex_match(fields[0], std::regex("[a-zA-Z0-9._-]+")))
|
||||
continue;
|
||||
BuildMetric metric;
|
||||
metric.name = fields[0]; // FIXME: validate
|
||||
metric.value = atof(fields[1].c_str()); // FIXME
|
||||
metric.name = fields[0];
|
||||
try {
|
||||
metric.value = std::stod(fields[1]);
|
||||
} catch (...) {
|
||||
continue; // skip this metric
|
||||
}
|
||||
metric.unit = fields.size() >= 3 ? fields[2] : "";
|
||||
if (!std::regex_match(metric.unit, std::regex("[a-zA-Z0-9._%-]+")))
|
||||
metric.unit = "";
|
||||
res.metrics[metric.name] = metric;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "state.hh"
|
||||
#include "hydra-build-result.hh"
|
||||
#include "finally.hh"
|
||||
#include "binary-cache-store.hh"
|
||||
#include <nix/util/finally.hh>
|
||||
#include <nix/store/binary-cache-store.hh>
|
||||
|
||||
using namespace nix;
|
||||
|
||||
@@ -16,7 +16,7 @@ void setThreadName(const std::string & name)
|
||||
}
|
||||
|
||||
|
||||
void State::builder(MachineReservation::ptr reservation)
|
||||
void State::builder(std::unique_ptr<MachineReservation> reservation)
|
||||
{
|
||||
setThreadName("bld~" + std::string(reservation->step->drvPath.to_string()));
|
||||
|
||||
@@ -35,22 +35,20 @@ void State::builder(MachineReservation::ptr reservation)
|
||||
activeSteps_.lock()->erase(activeStep);
|
||||
});
|
||||
|
||||
std::string machine = reservation->machine->storeUri.render();
|
||||
|
||||
try {
|
||||
auto destStore = getDestStore();
|
||||
res = doBuildStep(destStore, reservation, activeStep);
|
||||
// Might release the reservation.
|
||||
res = doBuildStep(destStore, std::move(reservation), activeStep);
|
||||
} catch (std::exception & e) {
|
||||
printMsg(lvlError, "uncaught exception building ‘%s’ on ‘%s’: %s",
|
||||
localStore->printStorePath(reservation->step->drvPath),
|
||||
reservation->machine->sshName,
|
||||
localStore->printStorePath(activeStep->step->drvPath),
|
||||
machine,
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
|
||||
/* Release the machine and wake up the dispatcher. */
|
||||
assert(reservation.unique());
|
||||
reservation = 0;
|
||||
wakeDispatcher();
|
||||
|
||||
/* If there was a temporary failure, retry the step after an
|
||||
exponentially increasing interval. */
|
||||
Step::ptr step = wstep.lock();
|
||||
@@ -72,11 +70,11 @@ void State::builder(MachineReservation::ptr reservation)
|
||||
|
||||
|
||||
State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
MachineReservation::ptr reservation,
|
||||
std::unique_ptr<MachineReservation> reservation,
|
||||
std::shared_ptr<ActiveStep> activeStep)
|
||||
{
|
||||
auto & step(reservation->step);
|
||||
auto & machine(reservation->machine);
|
||||
auto step(reservation->step);
|
||||
auto machine(reservation->machine);
|
||||
|
||||
{
|
||||
auto step_(step->state.lock());
|
||||
@@ -98,8 +96,13 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
it). */
|
||||
BuildID buildId;
|
||||
std::optional<StorePath> buildDrvPath;
|
||||
unsigned int maxSilentTime, buildTimeout;
|
||||
unsigned int repeats = step->isDeterministic ? 1 : 0;
|
||||
// Other fields set below
|
||||
nix::ServeProto::BuildOptions buildOptions {
|
||||
.maxLogSize = maxLogSize,
|
||||
.nrRepeats = step->isDeterministic ? 1u : 0u,
|
||||
.enforceDeterminism = step->isDeterministic,
|
||||
.keepFailed = false,
|
||||
};
|
||||
|
||||
auto conn(dbPool.get());
|
||||
|
||||
@@ -134,18 +137,18 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
{
|
||||
auto i = jobsetRepeats.find(std::make_pair(build2->projectName, build2->jobsetName));
|
||||
if (i != jobsetRepeats.end())
|
||||
repeats = std::max(repeats, i->second);
|
||||
buildOptions.nrRepeats = std::max(buildOptions.nrRepeats, i->second);
|
||||
}
|
||||
}
|
||||
if (!build) build = *dependents.begin();
|
||||
|
||||
buildId = build->id;
|
||||
buildDrvPath = build->drvPath;
|
||||
maxSilentTime = build->maxSilentTime;
|
||||
buildTimeout = build->buildTimeout;
|
||||
buildOptions.maxSilentTime = build->maxSilentTime;
|
||||
buildOptions.buildTimeout = build->buildTimeout;
|
||||
|
||||
printInfo("performing step ‘%s’ %d times on ‘%s’ (needed by build %d and %d others)",
|
||||
localStore->printStorePath(step->drvPath), repeats + 1, machine->sshName, buildId, (dependents.size() - 1));
|
||||
localStore->printStorePath(step->drvPath), buildOptions.nrRepeats + 1, machine->storeUri.render(), buildId, (dependents.size() - 1));
|
||||
}
|
||||
|
||||
if (!buildOneDone)
|
||||
@@ -173,7 +176,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
unlink(result.logFile.c_str());
|
||||
}
|
||||
} catch (...) {
|
||||
ignoreException();
|
||||
ignoreExceptionInDestructor();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -191,7 +194,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
{
|
||||
auto mc = startDbUpdate();
|
||||
pqxx::work txn(*conn);
|
||||
stepNr = createBuildStep(txn, result.startTime, buildId, step, machine->sshName, bsBusy);
|
||||
stepNr = createBuildStep(txn, result.startTime, buildId, step, machine->storeUri.render(), bsBusy);
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
@@ -206,7 +209,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
|
||||
try {
|
||||
/* FIXME: referring builds may have conflicting timeouts. */
|
||||
buildRemote(destStore, machine, step, maxSilentTime, buildTimeout, repeats, result, activeStep, updateStep, narMembers);
|
||||
buildRemote(destStore, std::move(reservation), machine, step, buildOptions, result, activeStep, updateStep, narMembers);
|
||||
} catch (Error & e) {
|
||||
if (activeStep->state_.lock()->cancelled) {
|
||||
printInfo("marking step %d of build %d as cancelled", stepNr, buildId);
|
||||
@@ -221,7 +224,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
|
||||
if (result.stepStatus == bsSuccess) {
|
||||
updateStep(ssPostProcessing);
|
||||
res = getBuildOutput(destStore, narMembers, *step->drv);
|
||||
res = getBuildOutput(destStore, narMembers, destStore->queryDerivationOutputMap(step->drvPath, &*localStore));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +251,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
/* Finish the step in the database. */
|
||||
if (stepNr) {
|
||||
pqxx::work txn(*conn);
|
||||
finishBuildStep(txn, result, buildId, stepNr, machine->sshName);
|
||||
finishBuildStep(txn, result, buildId, stepNr, machine->storeUri.render());
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
@@ -256,7 +259,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
issue). Retry a number of times. */
|
||||
if (result.canRetry) {
|
||||
printMsg(lvlError, "possibly transient failure building ‘%s’ on ‘%s’: %s",
|
||||
localStore->printStorePath(step->drvPath), machine->sshName, result.errorMsg);
|
||||
localStore->printStorePath(step->drvPath), machine->storeUri.render(), result.errorMsg);
|
||||
assert(stepNr);
|
||||
bool retry;
|
||||
{
|
||||
@@ -275,9 +278,12 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
|
||||
assert(stepNr);
|
||||
|
||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore)) {
|
||||
if (i.second.second)
|
||||
addRoot(*i.second.second);
|
||||
for (auto & [outputName, optOutputPath] : destStore->queryPartialDerivationOutputMap(step->drvPath, &*localStore)) {
|
||||
if (!optOutputPath)
|
||||
throw Error(
|
||||
"Missing output %s for derivation %d which was supposed to have succeeded",
|
||||
outputName, localStore->printStorePath(step->drvPath));
|
||||
addRoot(*optOutputPath);
|
||||
}
|
||||
|
||||
/* Register success in the database for all Build objects that
|
||||
@@ -323,7 +329,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
|
||||
pqxx::work txn(*conn);
|
||||
|
||||
for (auto & b : direct) {
|
||||
printMsg(lvlInfo, format("marking build %1% as succeeded") % b->id);
|
||||
printInfo("marking build %1% as succeeded", b->id);
|
||||
markSucceededBuild(txn, b, res, buildId != b->id || result.isCached,
|
||||
result.startTime, result.stopTime);
|
||||
}
|
||||
@@ -398,7 +404,7 @@ void State::failStep(
|
||||
Step::ptr step,
|
||||
BuildID buildId,
|
||||
const RemoteResult & result,
|
||||
Machine::ptr machine,
|
||||
::Machine::ptr machine,
|
||||
bool & stepFinished)
|
||||
{
|
||||
/* Register failure in the database for all Build objects that
|
||||
@@ -444,21 +450,20 @@ void State::failStep(
|
||||
build->finishedInDB)
|
||||
continue;
|
||||
createBuildStep(txn,
|
||||
0, build->id, step, machine ? machine->sshName : "",
|
||||
0, build->id, step, machine ? machine->storeUri.render() : "",
|
||||
result.stepStatus, result.errorMsg, buildId == build->id ? 0 : buildId);
|
||||
}
|
||||
|
||||
/* Mark all builds that depend on this derivation as failed. */
|
||||
for (auto & build : indirect) {
|
||||
if (build->finishedInDB) continue;
|
||||
printMsg(lvlError, format("marking build %1% as failed") % build->id);
|
||||
txn.exec_params0
|
||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, isCachedBuild = $5, notificationPendingSince = $4 where id = $1 and finished = 0",
|
||||
build->id,
|
||||
printError("marking build %1% as failed", build->id);
|
||||
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, isCachedBuild = $5, notificationPendingSince = $4 where id = $1 and finished = 0",
|
||||
pqxx::params{build->id,
|
||||
(int) (build->drvPath != step->drvPath && result.buildStatus() == bsFailed ? bsDepFailed : result.buildStatus()),
|
||||
result.startTime,
|
||||
result.stopTime,
|
||||
result.stepStatus == bsCachedFailure ? 1 : 0);
|
||||
result.stepStatus == bsCachedFailure ? 1 : 0}).no_rows();
|
||||
nrBuildsDone++;
|
||||
}
|
||||
|
||||
@@ -467,7 +472,7 @@ void State::failStep(
|
||||
if (result.stepStatus != bsCachedFailure && result.canCache)
|
||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore))
|
||||
if (i.second.second)
|
||||
txn.exec_params0("insert into FailedPaths values ($1)", localStore->printStorePath(*i.second.second));
|
||||
txn.exec("insert into FailedPaths values ($1)", pqxx::params{localStore->printStorePath(*i.second.second)}).no_rows();
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <cmath>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "state.hh"
|
||||
|
||||
@@ -39,28 +40,34 @@ void State::dispatcher()
|
||||
printMsg(lvlDebug, "dispatcher woken up");
|
||||
nrDispatcherWakeups++;
|
||||
|
||||
auto now1 = std::chrono::steady_clock::now();
|
||||
auto t_before_work = std::chrono::steady_clock::now();
|
||||
|
||||
auto sleepUntil = doDispatch();
|
||||
|
||||
auto now2 = std::chrono::steady_clock::now();
|
||||
auto t_after_work = std::chrono::steady_clock::now();
|
||||
|
||||
dispatchTimeMs += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
|
||||
prom.dispatcher_time_spent_running.Increment(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(t_after_work - t_before_work).count());
|
||||
dispatchTimeMs += std::chrono::duration_cast<std::chrono::milliseconds>(t_after_work - t_before_work).count();
|
||||
|
||||
/* Sleep until we're woken up (either because a runnable build
|
||||
is added, or because a build finishes). */
|
||||
{
|
||||
auto dispatcherWakeup_(dispatcherWakeup.lock());
|
||||
if (!*dispatcherWakeup_) {
|
||||
printMsg(lvlDebug, format("dispatcher sleeping for %1%s") %
|
||||
debug("dispatcher sleeping for %1%s",
|
||||
std::chrono::duration_cast<std::chrono::seconds>(sleepUntil - std::chrono::system_clock::now()).count());
|
||||
dispatcherWakeup_.wait_until(dispatcherWakeupCV, sleepUntil);
|
||||
}
|
||||
*dispatcherWakeup_ = false;
|
||||
}
|
||||
|
||||
auto t_after_sleep = std::chrono::steady_clock::now();
|
||||
prom.dispatcher_time_spent_waiting.Increment(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(t_after_sleep - t_after_work).count());
|
||||
|
||||
} catch (std::exception & e) {
|
||||
printMsg(lvlError, format("dispatcher: %1%") % e.what());
|
||||
printError("dispatcher: %s", e.what());
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
@@ -80,17 +87,124 @@ system_time State::doDispatch()
|
||||
jobset.second->pruneSteps();
|
||||
auto s2 = jobset.second->shareUsed();
|
||||
if (s1 != s2)
|
||||
printMsg(lvlDebug, format("pruned scheduling window of ‘%1%:%2%’ from %3% to %4%")
|
||||
% jobset.first.first % jobset.first.second % s1 % s2);
|
||||
debug("pruned scheduling window of ‘%1%:%2%’ from %3% to %4%",
|
||||
jobset.first.first, jobset.first.second, s1, s2);
|
||||
}
|
||||
}
|
||||
|
||||
system_time now = std::chrono::system_clock::now();
|
||||
|
||||
/* Start steps until we're out of steps or slots. */
|
||||
auto sleepUntil = system_time::max();
|
||||
bool keepGoing;
|
||||
|
||||
/* Sort the runnable steps by priority. Priority is establised
|
||||
as follows (in order of precedence):
|
||||
|
||||
- The global priority of the builds that depend on the
|
||||
step. This allows admins to bump a build to the front of
|
||||
the queue.
|
||||
|
||||
- The lowest used scheduling share of the jobsets depending
|
||||
on the step.
|
||||
|
||||
- The local priority of the build, as set via the build's
|
||||
meta.schedulingPriority field. Note that this is not
|
||||
quite correct: the local priority should only be used to
|
||||
establish priority between builds in the same jobset, but
|
||||
here it's used between steps in different jobsets if they
|
||||
happen to have the same lowest used scheduling share. But
|
||||
that's not very likely.
|
||||
|
||||
- The lowest ID of the builds depending on the step;
|
||||
i.e. older builds take priority over new ones.
|
||||
|
||||
FIXME: O(n lg n); obviously, it would be better to keep a
|
||||
runnable queue sorted by priority. */
|
||||
struct StepInfo
|
||||
{
|
||||
Step::ptr step;
|
||||
bool alreadyScheduled = false;
|
||||
|
||||
/* The lowest share used of any jobset depending on this
|
||||
step. */
|
||||
double lowestShareUsed = 1e9;
|
||||
|
||||
/* Info copied from step->state to ensure that the
|
||||
comparator is a partial ordering (see MachineInfo). */
|
||||
int highestGlobalPriority;
|
||||
int highestLocalPriority;
|
||||
size_t numRequiredSystemFeatures;
|
||||
size_t numRevDeps;
|
||||
BuildID lowestBuildID;
|
||||
|
||||
StepInfo(Step::ptr step, Step::State & step_) : step(step)
|
||||
{
|
||||
for (auto & jobset : step_.jobsets)
|
||||
lowestShareUsed = std::min(lowestShareUsed, jobset->shareUsed());
|
||||
highestGlobalPriority = step_.highestGlobalPriority;
|
||||
highestLocalPriority = step_.highestLocalPriority;
|
||||
numRequiredSystemFeatures = step->requiredSystemFeatures.size();
|
||||
numRevDeps = step_.rdeps.size();
|
||||
lowestBuildID = step_.lowestBuildID;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<StepInfo> runnableSorted;
|
||||
|
||||
struct RunnablePerType
|
||||
{
|
||||
unsigned int count{0};
|
||||
std::chrono::seconds waitTime{0};
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, RunnablePerType> runnablePerType;
|
||||
|
||||
{
|
||||
auto runnable_(runnable.lock());
|
||||
runnableSorted.reserve(runnable_->size());
|
||||
for (auto i = runnable_->begin(); i != runnable_->end(); ) {
|
||||
auto step = i->lock();
|
||||
|
||||
/* Remove dead steps. */
|
||||
if (!step) {
|
||||
i = runnable_->erase(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
auto & r = runnablePerType[step->systemType];
|
||||
r.count++;
|
||||
|
||||
/* Skip previously failed steps that aren't ready
|
||||
to be retried. */
|
||||
auto step_(step->state.lock());
|
||||
r.waitTime += std::chrono::duration_cast<std::chrono::seconds>(now - step_->runnableSince);
|
||||
if (step_->tries > 0 && step_->after > now) {
|
||||
if (step_->after < sleepUntil)
|
||||
sleepUntil = step_->after;
|
||||
continue;
|
||||
}
|
||||
|
||||
runnableSorted.emplace_back(step, *step_);
|
||||
}
|
||||
}
|
||||
|
||||
sort(runnableSorted.begin(), runnableSorted.end(),
|
||||
[](const StepInfo & a, const StepInfo & b)
|
||||
{
|
||||
return
|
||||
a.highestGlobalPriority != b.highestGlobalPriority ? a.highestGlobalPriority > b.highestGlobalPriority :
|
||||
a.lowestShareUsed != b.lowestShareUsed ? a.lowestShareUsed < b.lowestShareUsed :
|
||||
a.highestLocalPriority != b.highestLocalPriority ? a.highestLocalPriority > b.highestLocalPriority :
|
||||
a.numRequiredSystemFeatures != b.numRequiredSystemFeatures ? a.numRequiredSystemFeatures > b.numRequiredSystemFeatures :
|
||||
a.numRevDeps != b.numRevDeps ? a.numRevDeps > b.numRevDeps :
|
||||
a.lowestBuildID < b.lowestBuildID;
|
||||
});
|
||||
|
||||
do {
|
||||
system_time now = std::chrono::system_clock::now();
|
||||
now = std::chrono::system_clock::now();
|
||||
|
||||
/* Copy the currentJobs field of each machine. This is
|
||||
necessary to ensure that the sort comparator below is
|
||||
@@ -98,7 +212,7 @@ system_time State::doDispatch()
|
||||
filter out temporarily disabled machines. */
|
||||
struct MachineInfo
|
||||
{
|
||||
Machine::ptr machine;
|
||||
::Machine::ptr machine;
|
||||
unsigned long currentJobs;
|
||||
};
|
||||
std::vector<MachineInfo> machinesSorted;
|
||||
@@ -138,104 +252,6 @@ system_time State::doDispatch()
|
||||
a.currentJobs > b.currentJobs;
|
||||
});
|
||||
|
||||
/* Sort the runnable steps by priority. Priority is establised
|
||||
as follows (in order of precedence):
|
||||
|
||||
- The global priority of the builds that depend on the
|
||||
step. This allows admins to bump a build to the front of
|
||||
the queue.
|
||||
|
||||
- The lowest used scheduling share of the jobsets depending
|
||||
on the step.
|
||||
|
||||
- The local priority of the build, as set via the build's
|
||||
meta.schedulingPriority field. Note that this is not
|
||||
quite correct: the local priority should only be used to
|
||||
establish priority between builds in the same jobset, but
|
||||
here it's used between steps in different jobsets if they
|
||||
happen to have the same lowest used scheduling share. But
|
||||
that's not very likely.
|
||||
|
||||
- The lowest ID of the builds depending on the step;
|
||||
i.e. older builds take priority over new ones.
|
||||
|
||||
FIXME: O(n lg n); obviously, it would be better to keep a
|
||||
runnable queue sorted by priority. */
|
||||
struct StepInfo
|
||||
{
|
||||
Step::ptr step;
|
||||
|
||||
/* The lowest share used of any jobset depending on this
|
||||
step. */
|
||||
double lowestShareUsed = 1e9;
|
||||
|
||||
/* Info copied from step->state to ensure that the
|
||||
comparator is a partial ordering (see MachineInfo). */
|
||||
int highestGlobalPriority;
|
||||
int highestLocalPriority;
|
||||
BuildID lowestBuildID;
|
||||
|
||||
StepInfo(Step::ptr step, Step::State & step_) : step(step)
|
||||
{
|
||||
for (auto & jobset : step_.jobsets)
|
||||
lowestShareUsed = std::min(lowestShareUsed, jobset->shareUsed());
|
||||
highestGlobalPriority = step_.highestGlobalPriority;
|
||||
highestLocalPriority = step_.highestLocalPriority;
|
||||
lowestBuildID = step_.lowestBuildID;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<StepInfo> runnableSorted;
|
||||
|
||||
struct RunnablePerType
|
||||
{
|
||||
unsigned int count{0};
|
||||
std::chrono::seconds waitTime{0};
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, RunnablePerType> runnablePerType;
|
||||
|
||||
{
|
||||
auto runnable_(runnable.lock());
|
||||
runnableSorted.reserve(runnable_->size());
|
||||
for (auto i = runnable_->begin(); i != runnable_->end(); ) {
|
||||
auto step = i->lock();
|
||||
|
||||
/* Remove dead steps. */
|
||||
if (!step) {
|
||||
i = runnable_->erase(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
auto & r = runnablePerType[step->systemType];
|
||||
r.count++;
|
||||
|
||||
/* Skip previously failed steps that aren't ready
|
||||
to be retried. */
|
||||
auto step_(step->state.lock());
|
||||
r.waitTime += std::chrono::duration_cast<std::chrono::seconds>(now - step_->runnableSince);
|
||||
if (step_->tries > 0 && step_->after > now) {
|
||||
if (step_->after < sleepUntil)
|
||||
sleepUntil = step_->after;
|
||||
continue;
|
||||
}
|
||||
|
||||
runnableSorted.emplace_back(step, *step_);
|
||||
}
|
||||
}
|
||||
|
||||
sort(runnableSorted.begin(), runnableSorted.end(),
|
||||
[](const StepInfo & a, const StepInfo & b)
|
||||
{
|
||||
return
|
||||
a.highestGlobalPriority != b.highestGlobalPriority ? a.highestGlobalPriority > b.highestGlobalPriority :
|
||||
a.lowestShareUsed != b.lowestShareUsed ? a.lowestShareUsed < b.lowestShareUsed :
|
||||
a.highestLocalPriority != b.highestLocalPriority ? a.highestLocalPriority > b.highestLocalPriority :
|
||||
a.lowestBuildID < b.lowestBuildID;
|
||||
});
|
||||
|
||||
/* Find a machine with a free slot and find a step to run
|
||||
on it. Once we find such a pair, we restart the outer
|
||||
loop because the machine sorting will have changed. */
|
||||
@@ -245,12 +261,14 @@ system_time State::doDispatch()
|
||||
if (mi.machine->state->currentJobs >= mi.machine->maxJobs) continue;
|
||||
|
||||
for (auto & stepInfo : runnableSorted) {
|
||||
if (stepInfo.alreadyScheduled) continue;
|
||||
|
||||
auto & step(stepInfo.step);
|
||||
|
||||
/* Can this machine do this step? */
|
||||
if (!mi.machine->supportsStep(step)) {
|
||||
debug("machine '%s' does not support step '%s' (system type '%s')",
|
||||
mi.machine->sshName, localStore->printStorePath(step->drvPath), step->drv->platform);
|
||||
mi.machine->storeUri.render(), localStore->printStorePath(step->drvPath), step->drv->platform);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -271,10 +289,12 @@ system_time State::doDispatch()
|
||||
r.count--;
|
||||
}
|
||||
|
||||
stepInfo.alreadyScheduled = true;
|
||||
|
||||
/* Make a slot reservation and start a thread to
|
||||
do the build. */
|
||||
auto builderThread = std::thread(&State::builder, this,
|
||||
std::make_shared<MachineReservation>(*this, step, mi.machine));
|
||||
std::make_unique<MachineReservation>(*this, step, mi.machine));
|
||||
builderThread.detach(); // FIXME?
|
||||
|
||||
keepGoing = true;
|
||||
@@ -428,7 +448,7 @@ void Jobset::pruneSteps()
|
||||
}
|
||||
|
||||
|
||||
State::MachineReservation::MachineReservation(State & state, Step::ptr step, Machine::ptr machine)
|
||||
State::MachineReservation::MachineReservation(State & state, Step::ptr step, ::Machine::ptr machine)
|
||||
: state(state), step(step), machine(machine)
|
||||
{
|
||||
machine->state->currentJobs++;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "hash.hh"
|
||||
#include "derivations.hh"
|
||||
#include "store-api.hh"
|
||||
#include <nix/util/hash.hh>
|
||||
#include <nix/store/derivations.hh>
|
||||
#include <nix/store/store-api.hh>
|
||||
#include "nar-extractor.hh"
|
||||
|
||||
struct BuildProduct
|
||||
@@ -36,10 +36,12 @@ struct BuildOutput
|
||||
|
||||
std::list<BuildProduct> products;
|
||||
|
||||
std::map<std::string, nix::StorePath> outputs;
|
||||
|
||||
std::map<std::string, BuildMetric> metrics;
|
||||
};
|
||||
|
||||
BuildOutput getBuildOutput(
|
||||
nix::ref<nix::Store> store,
|
||||
NarMemberDatas & narMembers,
|
||||
const nix::Derivation & drv);
|
||||
const nix::OutputPathMap derivationOutputs);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -8,27 +9,21 @@
|
||||
|
||||
#include <prometheus/exposer.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <nix/util/signals.hh>
|
||||
#include "state.hh"
|
||||
#include "hydra-build-result.hh"
|
||||
#include "store-api.hh"
|
||||
#include "remote-store.hh"
|
||||
#include <nix/store/store-open.hh>
|
||||
#include <nix/store/remote-store.hh>
|
||||
|
||||
#include "globals.hh"
|
||||
#include <nix/store/globals.hh>
|
||||
#include "hydra-config.hh"
|
||||
#include "json.hh"
|
||||
#include "s3-binary-cache-store.hh"
|
||||
#include "shared.hh"
|
||||
#include <nix/store/s3-binary-cache-store.hh>
|
||||
#include <nix/main/shared.hh>
|
||||
|
||||
using namespace nix;
|
||||
|
||||
|
||||
namespace nix {
|
||||
|
||||
template<> void toJSON<std::atomic<long>>(std::ostream & str, const std::atomic<long> & n) { str << n; }
|
||||
template<> void toJSON<std::atomic<uint64_t>>(std::ostream & str, const std::atomic<uint64_t> & n) { str << n; }
|
||||
template<> void toJSON<double>(std::ostream & str, const double & n) { str << n; }
|
||||
|
||||
}
|
||||
using nlohmann::json;
|
||||
|
||||
|
||||
std::string getEnvOrDie(const std::string & key)
|
||||
@@ -75,10 +70,31 @@ State::PromMetrics::PromMetrics()
|
||||
.Register(*registry)
|
||||
.Add({})
|
||||
)
|
||||
, queue_max_id(
|
||||
prometheus::BuildGauge()
|
||||
.Name("hydraqueuerunner_queue_max_build_id_info")
|
||||
.Help("Maximum build record ID in the queue")
|
||||
, dispatcher_time_spent_running(
|
||||
prometheus::BuildCounter()
|
||||
.Name("hydraqueuerunner_dispatcher_time_spent_running")
|
||||
.Help("Time (in micros) spent running the dispatcher")
|
||||
.Register(*registry)
|
||||
.Add({})
|
||||
)
|
||||
, dispatcher_time_spent_waiting(
|
||||
prometheus::BuildCounter()
|
||||
.Name("hydraqueuerunner_dispatcher_time_spent_waiting")
|
||||
.Help("Time (in micros) spent waiting for the dispatcher to obtain work")
|
||||
.Register(*registry)
|
||||
.Add({})
|
||||
)
|
||||
, queue_monitor_time_spent_running(
|
||||
prometheus::BuildCounter()
|
||||
.Name("hydraqueuerunner_queue_monitor_time_spent_running")
|
||||
.Help("Time (in micros) spent running the queue monitor")
|
||||
.Register(*registry)
|
||||
.Add({})
|
||||
)
|
||||
, queue_monitor_time_spent_waiting(
|
||||
prometheus::BuildCounter()
|
||||
.Name("hydraqueuerunner_queue_monitor_time_spent_waiting")
|
||||
.Help("Time (in micros) spent waiting for the queue monitor to obtain work")
|
||||
.Register(*registry)
|
||||
.Add({})
|
||||
)
|
||||
@@ -90,6 +106,7 @@ State::State(std::optional<std::string> metricsAddrOpt)
|
||||
: config(std::make_unique<HydraConfig>())
|
||||
, maxUnsupportedTime(config->getIntOption("max_unsupported_time", 0))
|
||||
, dbPool(config->getIntOption("max_db_connections", 128))
|
||||
, localWorkThrottler(config->getIntOption("max_local_worker_threads", std::min(maxSupportedLocalWorkers, std::max(4u, std::thread::hardware_concurrency()) - 2)))
|
||||
, maxOutputSize(config->getIntOption("max_output_size", 2ULL << 30))
|
||||
, maxLogSize(config->getIntOption("max_log_size", 64ULL << 20))
|
||||
, uploadLogsToBinaryCache(config->getBoolOption("upload_logs_to_binary_cache", false))
|
||||
@@ -140,50 +157,29 @@ void State::parseMachines(const std::string & contents)
|
||||
oldMachines = *machines_;
|
||||
}
|
||||
|
||||
for (auto line : tokenizeString<Strings>(contents, "\n")) {
|
||||
line = trim(std::string(line, 0, line.find('#')));
|
||||
auto tokens = tokenizeString<std::vector<std::string>>(line);
|
||||
if (tokens.size() < 3) continue;
|
||||
tokens.resize(8);
|
||||
|
||||
auto machine = std::make_shared<Machine>();
|
||||
machine->sshName = tokens[0];
|
||||
machine->systemTypes = tokenizeString<StringSet>(tokens[1], ",");
|
||||
machine->sshKey = tokens[2] == "-" ? std::string("") : tokens[2];
|
||||
if (tokens[3] != "")
|
||||
machine->maxJobs = string2Int<decltype(machine->maxJobs)>(tokens[3]).value();
|
||||
else
|
||||
machine->maxJobs = 1;
|
||||
machine->speedFactor = atof(tokens[4].c_str());
|
||||
if (tokens[5] == "-") tokens[5] = "";
|
||||
machine->supportedFeatures = tokenizeString<StringSet>(tokens[5], ",");
|
||||
if (tokens[6] == "-") tokens[6] = "";
|
||||
machine->mandatoryFeatures = tokenizeString<StringSet>(tokens[6], ",");
|
||||
for (auto & f : machine->mandatoryFeatures)
|
||||
machine->supportedFeatures.insert(f);
|
||||
if (tokens[7] != "" && tokens[7] != "-")
|
||||
machine->sshPublicHostKey = base64Decode(tokens[7]);
|
||||
for (auto && machine_ : nix::Machine::parseConfig({}, contents)) {
|
||||
auto machine = std::make_shared<::Machine>(std::move(machine_));
|
||||
|
||||
/* Re-use the State object of the previous machine with the
|
||||
same name. */
|
||||
auto i = oldMachines.find(machine->sshName);
|
||||
auto i = oldMachines.find(machine->storeUri.variant);
|
||||
if (i == oldMachines.end())
|
||||
printMsg(lvlChatty, format("adding new machine ‘%1%’") % machine->sshName);
|
||||
printMsg(lvlChatty, "adding new machine ‘%1%’", machine->storeUri.render());
|
||||
else
|
||||
printMsg(lvlChatty, format("updating machine ‘%1%’") % machine->sshName);
|
||||
printMsg(lvlChatty, "updating machine ‘%1%’", machine->storeUri.render());
|
||||
machine->state = i == oldMachines.end()
|
||||
? std::make_shared<Machine::State>()
|
||||
? std::make_shared<::Machine::State>()
|
||||
: i->second->state;
|
||||
newMachines[machine->sshName] = machine;
|
||||
newMachines[machine->storeUri.variant] = machine;
|
||||
}
|
||||
|
||||
for (auto & m : oldMachines)
|
||||
if (newMachines.find(m.first) == newMachines.end()) {
|
||||
if (m.second->enabled)
|
||||
printMsg(lvlInfo, format("removing machine ‘%1%’") % m.first);
|
||||
/* Add a disabled Machine object to make sure stats are
|
||||
printInfo("removing machine ‘%1%’", m.second->storeUri.render());
|
||||
/* Add a disabled ::Machine object to make sure stats are
|
||||
maintained. */
|
||||
auto machine = std::make_shared<Machine>(*(m.second));
|
||||
auto machine = std::make_shared<::Machine>(*(m.second));
|
||||
machine->enabled = false;
|
||||
newMachines[m.first] = machine;
|
||||
}
|
||||
@@ -211,7 +207,7 @@ void State::monitorMachinesFile()
|
||||
parseMachines("localhost " +
|
||||
(settings.thisSystem == "x86_64-linux" ? "x86_64-linux,i686-linux" : settings.thisSystem.get())
|
||||
+ " - " + std::to_string(settings.maxBuildJobs) + " 1 "
|
||||
+ concatStringsSep(",", settings.systemFeatures.get()));
|
||||
+ concatStringsSep(",", StoreConfig::getDefaultSystemFeatures()));
|
||||
machinesReadyLock.unlock();
|
||||
return;
|
||||
}
|
||||
@@ -280,17 +276,16 @@ void State::monitorMachinesFile()
|
||||
void State::clearBusy(Connection & conn, time_t stopTime)
|
||||
{
|
||||
pqxx::work txn(conn);
|
||||
txn.exec_params0
|
||||
("update BuildSteps set busy = 0, status = $1, stopTime = $2 where busy != 0",
|
||||
(int) bsAborted,
|
||||
stopTime != 0 ? std::make_optional(stopTime) : std::nullopt);
|
||||
txn.exec("update BuildSteps set busy = 0, status = $1, stopTime = $2 where busy != 0",
|
||||
pqxx::params{(int) bsAborted,
|
||||
stopTime != 0 ? std::make_optional(stopTime) : std::nullopt}).no_rows();
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
|
||||
unsigned int State::allocBuildStep(pqxx::work & txn, BuildID buildId)
|
||||
{
|
||||
auto res = txn.exec_params1("select max(stepnr) from BuildSteps where build = $1", buildId);
|
||||
auto res = txn.exec("select max(stepnr) from BuildSteps where build = $1", buildId).one_row();
|
||||
return res[0].is_null() ? 1 : res[0].as<int>() + 1;
|
||||
}
|
||||
|
||||
@@ -301,9 +296,8 @@ unsigned int State::createBuildStep(pqxx::work & txn, time_t startTime, BuildID
|
||||
restart:
|
||||
auto stepNr = allocBuildStep(txn, buildId);
|
||||
|
||||
auto r = txn.exec_params
|
||||
("insert into BuildSteps (build, stepnr, type, drvPath, busy, startTime, system, status, propagatedFrom, errorMsg, stopTime, machine) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) on conflict do nothing",
|
||||
buildId,
|
||||
auto r = txn.exec("insert into BuildSteps (build, stepnr, type, drvPath, busy, startTime, system, status, propagatedFrom, errorMsg, stopTime, machine) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) on conflict do nothing",
|
||||
pqxx::params{buildId,
|
||||
stepNr,
|
||||
0, // == build
|
||||
localStore->printStorePath(step->drvPath),
|
||||
@@ -314,14 +308,16 @@ unsigned int State::createBuildStep(pqxx::work & txn, time_t startTime, BuildID
|
||||
propagatedFrom != 0 ? std::make_optional(propagatedFrom) : std::nullopt, // internal::params
|
||||
errorMsg != "" ? std::make_optional(errorMsg) : std::nullopt,
|
||||
startTime != 0 && status != bsBusy ? std::make_optional(startTime) : std::nullopt,
|
||||
machine);
|
||||
machine});
|
||||
|
||||
if (r.affected_rows() == 0) goto restart;
|
||||
|
||||
for (auto & [name, output] : step->drv->outputs)
|
||||
txn.exec_params0
|
||||
("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
||||
buildId, stepNr, name, localStore->printStorePath(*output.path(*localStore, step->drv->name, name)));
|
||||
for (auto & [name, output] : getDestStore()->queryPartialDerivationOutputMap(step->drvPath, &*localStore))
|
||||
txn.exec("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
||||
pqxx::params{buildId, stepNr, name,
|
||||
output
|
||||
? std::optional { localStore->printStorePath(*output)}
|
||||
: std::nullopt}).no_rows();
|
||||
|
||||
if (status == bsBusy)
|
||||
txn.exec(fmt("notify step_started, '%d\t%d'", buildId, stepNr));
|
||||
@@ -332,11 +328,10 @@ unsigned int State::createBuildStep(pqxx::work & txn, time_t startTime, BuildID
|
||||
|
||||
void State::updateBuildStep(pqxx::work & txn, BuildID buildId, unsigned int stepNr, StepState stepState)
|
||||
{
|
||||
if (txn.exec_params
|
||||
("update BuildSteps set busy = $1 where build = $2 and stepnr = $3 and busy != 0 and status is null",
|
||||
(int) stepState,
|
||||
if (txn.exec("update BuildSteps set busy = $1 where build = $2 and stepnr = $3 and busy != 0 and status is null",
|
||||
pqxx::params{(int) stepState,
|
||||
buildId,
|
||||
stepNr).affected_rows() != 1)
|
||||
stepNr}).affected_rows() != 1)
|
||||
throw Error("step %d of build %d is in an unexpected state", stepNr, buildId);
|
||||
}
|
||||
|
||||
@@ -346,44 +341,52 @@ void State::finishBuildStep(pqxx::work & txn, const RemoteResult & result,
|
||||
{
|
||||
assert(result.startTime);
|
||||
assert(result.stopTime);
|
||||
txn.exec_params0
|
||||
("update BuildSteps set busy = 0, status = $1, errorMsg = $4, startTime = $5, stopTime = $6, machine = $7, overhead = $8, timesBuilt = $9, isNonDeterministic = $10 where build = $2 and stepnr = $3",
|
||||
(int) result.stepStatus, buildId, stepNr,
|
||||
txn.exec("update BuildSteps set busy = 0, status = $1, errorMsg = $4, startTime = $5, stopTime = $6, machine = $7, overhead = $8, timesBuilt = $9, isNonDeterministic = $10 where build = $2 and stepnr = $3",
|
||||
pqxx::params{(int) result.stepStatus, buildId, stepNr,
|
||||
result.errorMsg != "" ? std::make_optional(result.errorMsg) : std::nullopt,
|
||||
result.startTime, result.stopTime,
|
||||
machine != "" ? std::make_optional(machine) : std::nullopt,
|
||||
result.overhead != 0 ? std::make_optional(result.overhead) : std::nullopt,
|
||||
result.timesBuilt > 0 ? std::make_optional(result.timesBuilt) : std::nullopt,
|
||||
result.timesBuilt > 1 ? std::make_optional(result.isNonDeterministic) : std::nullopt);
|
||||
result.timesBuilt > 1 ? std::make_optional(result.isNonDeterministic) : std::nullopt}).no_rows();
|
||||
assert(result.logFile.find('\t') == std::string::npos);
|
||||
txn.exec(fmt("notify step_finished, '%d\t%d\t%s'",
|
||||
buildId, stepNr, result.logFile));
|
||||
|
||||
if (result.stepStatus == bsSuccess) {
|
||||
// Update the corresponding `BuildStepOutputs` row to add the output path
|
||||
auto res = txn.exec("select drvPath from BuildSteps where build = $1 and stepnr = $2", pqxx::params{buildId, stepNr}).one_row();
|
||||
assert(res.size());
|
||||
StorePath drvPath = localStore->parseStorePath(res[0].as<std::string>());
|
||||
// If we've finished building, all the paths should be known
|
||||
for (auto & [name, output] : getDestStore()->queryDerivationOutputMap(drvPath, &*localStore))
|
||||
txn.exec("update BuildStepOutputs set path = $4 where build = $1 and stepnr = $2 and name = $3",
|
||||
pqxx::params{buildId, stepNr, name, localStore->printStorePath(output)}).no_rows();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int State::createSubstitutionStep(pqxx::work & txn, time_t startTime, time_t stopTime,
|
||||
Build::ptr build, const StorePath & drvPath, const std::string & outputName, const StorePath & storePath)
|
||||
Build::ptr build, const StorePath & drvPath, const nix::Derivation drv, const std::string & outputName, const StorePath & storePath)
|
||||
{
|
||||
restart:
|
||||
auto stepNr = allocBuildStep(txn, build->id);
|
||||
|
||||
auto r = txn.exec_params
|
||||
("insert into BuildSteps (build, stepnr, type, drvPath, busy, status, startTime, stopTime) values ($1, $2, $3, $4, $5, $6, $7, $8) on conflict do nothing",
|
||||
build->id,
|
||||
auto r = txn.exec("insert into BuildSteps (build, stepnr, type, drvPath, busy, status, startTime, stopTime) values ($1, $2, $3, $4, $5, $6, $7, $8) on conflict do nothing",
|
||||
pqxx::params{build->id,
|
||||
stepNr,
|
||||
1, // == substitution
|
||||
(localStore->printStorePath(drvPath)),
|
||||
0,
|
||||
0,
|
||||
startTime,
|
||||
stopTime);
|
||||
stopTime});
|
||||
|
||||
if (r.affected_rows() == 0) goto restart;
|
||||
|
||||
txn.exec_params0
|
||||
("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
||||
build->id, stepNr, outputName,
|
||||
localStore->printStorePath(storePath));
|
||||
txn.exec("insert into BuildStepOutputs (build, stepnr, name, path) values ($1, $2, $3, $4)",
|
||||
pqxx::params{build->id, stepNr, outputName,
|
||||
localStore->printStorePath(storePath)}).no_rows();
|
||||
|
||||
return stepNr;
|
||||
}
|
||||
@@ -450,49 +453,54 @@ void State::markSucceededBuild(pqxx::work & txn, Build::ptr build,
|
||||
{
|
||||
if (build->finishedInDB) return;
|
||||
|
||||
if (txn.exec_params("select 1 from Builds where id = $1 and finished = 0", build->id).empty()) return;
|
||||
if (txn.exec("select 1 from Builds where id = $1 and finished = 0", pqxx::params{build->id}).empty()) return;
|
||||
|
||||
txn.exec_params0
|
||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, size = $5, closureSize = $6, releaseName = $7, isCachedBuild = $8, notificationPendingSince = $4 where id = $1",
|
||||
build->id,
|
||||
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $4, size = $5, closureSize = $6, releaseName = $7, isCachedBuild = $8, notificationPendingSince = $4 where id = $1",
|
||||
pqxx::params{build->id,
|
||||
(int) (res.failed ? bsFailedWithOutput : bsSuccess),
|
||||
startTime,
|
||||
stopTime,
|
||||
res.size,
|
||||
res.closureSize,
|
||||
res.releaseName != "" ? std::make_optional(res.releaseName) : std::nullopt,
|
||||
isCachedBuild ? 1 : 0);
|
||||
isCachedBuild ? 1 : 0}).no_rows();
|
||||
|
||||
txn.exec_params0("delete from BuildProducts where build = $1", build->id);
|
||||
for (auto & [outputName, outputPath] : res.outputs) {
|
||||
txn.exec("update BuildOutputs set path = $3 where build = $1 and name = $2",
|
||||
pqxx::params{build->id,
|
||||
outputName,
|
||||
localStore->printStorePath(outputPath)}
|
||||
).no_rows();
|
||||
}
|
||||
|
||||
txn.exec("delete from BuildProducts where build = $1", pqxx::params{build->id}).no_rows();
|
||||
|
||||
unsigned int productNr = 1;
|
||||
for (auto & product : res.products) {
|
||||
txn.exec_params0
|
||||
("insert into BuildProducts (build, productnr, type, subtype, fileSize, sha256hash, path, name, defaultPath) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
build->id,
|
||||
txn.exec("insert into BuildProducts (build, productnr, type, subtype, fileSize, sha256hash, path, name, defaultPath) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
pqxx::params{build->id,
|
||||
productNr++,
|
||||
product.type,
|
||||
product.subtype,
|
||||
product.fileSize ? std::make_optional(*product.fileSize) : std::nullopt,
|
||||
product.sha256hash ? std::make_optional(product.sha256hash->to_string(Base16, false)) : std::nullopt,
|
||||
product.sha256hash ? std::make_optional(product.sha256hash->to_string(HashFormat::Base16, false)) : std::nullopt,
|
||||
product.path,
|
||||
product.name,
|
||||
product.defaultPath);
|
||||
product.defaultPath}).no_rows();
|
||||
}
|
||||
|
||||
txn.exec_params0("delete from BuildMetrics where build = $1", build->id);
|
||||
txn.exec("delete from BuildMetrics where build = $1", pqxx::params{build->id}).no_rows();
|
||||
|
||||
for (auto & metric : res.metrics) {
|
||||
txn.exec_params0
|
||||
("insert into BuildMetrics (build, name, unit, value, project, jobset, job, timestamp) values ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
build->id,
|
||||
txn.exec("insert into BuildMetrics (build, name, unit, value, project, jobset, job, timestamp) values ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
pqxx::params{build->id,
|
||||
metric.second.name,
|
||||
metric.second.unit != "" ? std::make_optional(metric.second.unit) : std::nullopt,
|
||||
metric.second.value,
|
||||
build->projectName,
|
||||
build->jobsetName,
|
||||
build->jobName,
|
||||
build->timestamp);
|
||||
build->timestamp}).no_rows();
|
||||
}
|
||||
|
||||
nrBuildsDone++;
|
||||
@@ -504,7 +512,7 @@ bool State::checkCachedFailure(Step::ptr step, Connection & conn)
|
||||
pqxx::work txn(conn);
|
||||
for (auto & i : step->drv->outputsAndOptPaths(*localStore))
|
||||
if (i.second.second)
|
||||
if (!txn.exec_params("select 1 from FailedPaths where path = $1", localStore->printStorePath(*i.second.second)).empty())
|
||||
if (!txn.exec("select 1 from FailedPaths where path = $1", pqxx::params{localStore->printStorePath(*i.second.second)}).empty())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -542,190 +550,182 @@ std::shared_ptr<PathLocks> State::acquireGlobalLock()
|
||||
|
||||
void State::dumpStatus(Connection & conn)
|
||||
{
|
||||
std::ostringstream out;
|
||||
time_t now = time(0);
|
||||
json statusJson = {
|
||||
{"status", "up"},
|
||||
{"time", time(0)},
|
||||
{"uptime", now - startedAt},
|
||||
{"pid", getpid()},
|
||||
|
||||
{"nrQueuedBuilds", builds.lock()->size()},
|
||||
{"nrActiveSteps", activeSteps_.lock()->size()},
|
||||
{"nrStepsBuilding", nrStepsBuilding.load()},
|
||||
{"nrStepsCopyingTo", nrStepsCopyingTo.load()},
|
||||
{"nrStepsWaitingForDownloadSlot", nrStepsWaitingForDownloadSlot.load()},
|
||||
{"nrStepsCopyingFrom", nrStepsCopyingFrom.load()},
|
||||
{"nrStepsWaiting", nrStepsWaiting.load()},
|
||||
{"nrUnsupportedSteps", nrUnsupportedSteps.load()},
|
||||
{"bytesSent", bytesSent.load()},
|
||||
{"bytesReceived", bytesReceived.load()},
|
||||
{"nrBuildsRead", nrBuildsRead.load()},
|
||||
{"buildReadTimeMs", buildReadTimeMs.load()},
|
||||
{"buildReadTimeAvgMs", nrBuildsRead == 0 ? 0.0 : (float) buildReadTimeMs / nrBuildsRead},
|
||||
{"nrBuildsDone", nrBuildsDone.load()},
|
||||
{"nrStepsStarted", nrStepsStarted.load()},
|
||||
{"nrStepsDone", nrStepsDone.load()},
|
||||
{"nrRetries", nrRetries.load()},
|
||||
{"maxNrRetries", maxNrRetries.load()},
|
||||
{"nrQueueWakeups", nrQueueWakeups.load()},
|
||||
{"nrDispatcherWakeups", nrDispatcherWakeups.load()},
|
||||
{"dispatchTimeMs", dispatchTimeMs.load()},
|
||||
{"dispatchTimeAvgMs", nrDispatcherWakeups == 0 ? 0.0 : (float) dispatchTimeMs / nrDispatcherWakeups},
|
||||
{"nrDbConnections", dbPool.count()},
|
||||
{"nrActiveDbUpdates", nrActiveDbUpdates.load()},
|
||||
};
|
||||
{
|
||||
JSONObject root(out);
|
||||
time_t now = time(0);
|
||||
root.attr("status", "up");
|
||||
root.attr("time", time(0));
|
||||
root.attr("uptime", now - startedAt);
|
||||
root.attr("pid", getpid());
|
||||
{
|
||||
auto builds_(builds.lock());
|
||||
root.attr("nrQueuedBuilds", builds_->size());
|
||||
}
|
||||
{
|
||||
auto steps_(steps.lock());
|
||||
for (auto i = steps_->begin(); i != steps_->end(); )
|
||||
if (i->second.lock()) ++i; else i = steps_->erase(i);
|
||||
root.attr("nrUnfinishedSteps", steps_->size());
|
||||
statusJson["nrUnfinishedSteps"] = steps_->size();
|
||||
}
|
||||
{
|
||||
auto runnable_(runnable.lock());
|
||||
for (auto i = runnable_->begin(); i != runnable_->end(); )
|
||||
if (i->lock()) ++i; else i = runnable_->erase(i);
|
||||
root.attr("nrRunnableSteps", runnable_->size());
|
||||
statusJson["nrRunnableSteps"] = runnable_->size();
|
||||
}
|
||||
root.attr("nrActiveSteps", activeSteps_.lock()->size());
|
||||
root.attr("nrStepsBuilding", nrStepsBuilding);
|
||||
root.attr("nrStepsCopyingTo", nrStepsCopyingTo);
|
||||
root.attr("nrStepsCopyingFrom", nrStepsCopyingFrom);
|
||||
root.attr("nrStepsWaiting", nrStepsWaiting);
|
||||
root.attr("nrUnsupportedSteps", nrUnsupportedSteps);
|
||||
root.attr("bytesSent", bytesSent);
|
||||
root.attr("bytesReceived", bytesReceived);
|
||||
root.attr("nrBuildsRead", nrBuildsRead);
|
||||
root.attr("buildReadTimeMs", buildReadTimeMs);
|
||||
root.attr("buildReadTimeAvgMs", nrBuildsRead == 0 ? 0.0 : (float) buildReadTimeMs / nrBuildsRead);
|
||||
root.attr("nrBuildsDone", nrBuildsDone);
|
||||
root.attr("nrStepsStarted", nrStepsStarted);
|
||||
root.attr("nrStepsDone", nrStepsDone);
|
||||
root.attr("nrRetries", nrRetries);
|
||||
root.attr("maxNrRetries", maxNrRetries);
|
||||
if (nrStepsDone) {
|
||||
root.attr("totalStepTime", totalStepTime);
|
||||
root.attr("totalStepBuildTime", totalStepBuildTime);
|
||||
root.attr("avgStepTime", (float) totalStepTime / nrStepsDone);
|
||||
root.attr("avgStepBuildTime", (float) totalStepBuildTime / nrStepsDone);
|
||||
statusJson["totalStepTime"] = totalStepTime.load();
|
||||
statusJson["totalStepBuildTime"] = totalStepBuildTime.load();
|
||||
statusJson["avgStepTime"] = (float) totalStepTime / nrStepsDone;
|
||||
statusJson["avgStepBuildTime"] = (float) totalStepBuildTime / nrStepsDone;
|
||||
}
|
||||
root.attr("nrQueueWakeups", nrQueueWakeups);
|
||||
root.attr("nrDispatcherWakeups", nrDispatcherWakeups);
|
||||
root.attr("dispatchTimeMs", dispatchTimeMs);
|
||||
root.attr("dispatchTimeAvgMs", nrDispatcherWakeups == 0 ? 0.0 : (float) dispatchTimeMs / nrDispatcherWakeups);
|
||||
root.attr("nrDbConnections", dbPool.count());
|
||||
root.attr("nrActiveDbUpdates", nrActiveDbUpdates);
|
||||
|
||||
{
|
||||
auto nested = root.object("machines");
|
||||
auto machines_json = json::object();
|
||||
auto machines_(machines.lock());
|
||||
for (auto & i : *machines_) {
|
||||
auto & m(i.second);
|
||||
auto & s(m->state);
|
||||
auto nested2 = nested.object(m->sshName);
|
||||
nested2.attr("enabled", m->enabled);
|
||||
|
||||
{
|
||||
auto list = nested2.list("systemTypes");
|
||||
for (auto & s : m->systemTypes)
|
||||
list.elem(s);
|
||||
}
|
||||
|
||||
{
|
||||
auto list = nested2.list("supportedFeatures");
|
||||
for (auto & s : m->supportedFeatures)
|
||||
list.elem(s);
|
||||
}
|
||||
|
||||
{
|
||||
auto list = nested2.list("mandatoryFeatures");
|
||||
for (auto & s : m->mandatoryFeatures)
|
||||
list.elem(s);
|
||||
}
|
||||
|
||||
nested2.attr("currentJobs", s->currentJobs);
|
||||
if (s->currentJobs == 0)
|
||||
nested2.attr("idleSince", s->idleSince);
|
||||
nested2.attr("nrStepsDone", s->nrStepsDone);
|
||||
if (m->state->nrStepsDone) {
|
||||
nested2.attr("totalStepTime", s->totalStepTime);
|
||||
nested2.attr("totalStepBuildTime", s->totalStepBuildTime);
|
||||
nested2.attr("avgStepTime", (float) s->totalStepTime / s->nrStepsDone);
|
||||
nested2.attr("avgStepBuildTime", (float) s->totalStepBuildTime / s->nrStepsDone);
|
||||
}
|
||||
|
||||
auto info(m->state->connectInfo.lock());
|
||||
nested2.attr("disabledUntil", std::chrono::system_clock::to_time_t(info->disabledUntil));
|
||||
nested2.attr("lastFailure", std::chrono::system_clock::to_time_t(info->lastFailure));
|
||||
nested2.attr("consecutiveFailures", info->consecutiveFailures);
|
||||
|
||||
json machine = {
|
||||
{"enabled", m->enabled},
|
||||
{"systemTypes", m->systemTypes},
|
||||
{"supportedFeatures", m->supportedFeatures},
|
||||
{"mandatoryFeatures", m->mandatoryFeatures},
|
||||
{"nrStepsDone", s->nrStepsDone.load()},
|
||||
{"currentJobs", s->currentJobs.load()},
|
||||
{"disabledUntil", std::chrono::system_clock::to_time_t(info->disabledUntil)},
|
||||
{"lastFailure", std::chrono::system_clock::to_time_t(info->lastFailure)},
|
||||
{"consecutiveFailures", info->consecutiveFailures},
|
||||
};
|
||||
|
||||
if (s->currentJobs == 0)
|
||||
machine["idleSince"] = s->idleSince.load();
|
||||
if (m->state->nrStepsDone) {
|
||||
machine["totalStepTime"] = s->totalStepTime.load();
|
||||
machine["totalStepBuildTime"] = s->totalStepBuildTime.load();
|
||||
machine["avgStepTime"] = (float) s->totalStepTime / s->nrStepsDone;
|
||||
machine["avgStepBuildTime"] = (float) s->totalStepBuildTime / s->nrStepsDone;
|
||||
}
|
||||
machines_json[m->storeUri.render()] = machine;
|
||||
}
|
||||
statusJson["machines"] = machines_json;
|
||||
}
|
||||
|
||||
{
|
||||
auto nested = root.object("jobsets");
|
||||
auto jobsets_json = json::object();
|
||||
auto jobsets_(jobsets.lock());
|
||||
for (auto & jobset : *jobsets_) {
|
||||
auto nested2 = nested.object(jobset.first.first + ":" + jobset.first.second);
|
||||
nested2.attr("shareUsed", jobset.second->shareUsed());
|
||||
nested2.attr("seconds", jobset.second->getSeconds());
|
||||
jobsets_json[jobset.first.first + ":" + jobset.first.second] = {
|
||||
{"shareUsed", jobset.second->shareUsed()},
|
||||
{"seconds", jobset.second->getSeconds()},
|
||||
};
|
||||
}
|
||||
statusJson["jobsets"] = jobsets_json;
|
||||
}
|
||||
|
||||
{
|
||||
auto nested = root.object("machineTypes");
|
||||
auto machineTypesJson = json::object();
|
||||
auto machineTypes_(machineTypes.lock());
|
||||
for (auto & i : *machineTypes_) {
|
||||
auto nested2 = nested.object(i.first);
|
||||
nested2.attr("runnable", i.second.runnable);
|
||||
nested2.attr("running", i.second.running);
|
||||
auto machineTypeJson = machineTypesJson[i.first] = {
|
||||
{"runnable", i.second.runnable},
|
||||
{"running", i.second.running},
|
||||
};
|
||||
if (i.second.runnable > 0)
|
||||
nested2.attr("waitTime", i.second.waitTime.count() +
|
||||
i.second.runnable * (time(0) - lastDispatcherCheck));
|
||||
machineTypeJson["waitTime"] = i.second.waitTime.count() +
|
||||
i.second.runnable * (time(0) - lastDispatcherCheck);
|
||||
if (i.second.running == 0)
|
||||
nested2.attr("lastActive", std::chrono::system_clock::to_time_t(i.second.lastActive));
|
||||
machineTypeJson["lastActive"] = std::chrono::system_clock::to_time_t(i.second.lastActive);
|
||||
}
|
||||
statusJson["machineTypes"] = machineTypesJson;
|
||||
}
|
||||
|
||||
auto store = getDestStore();
|
||||
|
||||
auto nested = root.object("store");
|
||||
|
||||
auto & stats = store->getStats();
|
||||
nested.attr("narInfoRead", stats.narInfoRead);
|
||||
nested.attr("narInfoReadAverted", stats.narInfoReadAverted);
|
||||
nested.attr("narInfoMissing", stats.narInfoMissing);
|
||||
nested.attr("narInfoWrite", stats.narInfoWrite);
|
||||
nested.attr("narInfoCacheSize", stats.pathInfoCacheSize);
|
||||
nested.attr("narRead", stats.narRead);
|
||||
nested.attr("narReadBytes", stats.narReadBytes);
|
||||
nested.attr("narReadCompressedBytes", stats.narReadCompressedBytes);
|
||||
nested.attr("narWrite", stats.narWrite);
|
||||
nested.attr("narWriteAverted", stats.narWriteAverted);
|
||||
nested.attr("narWriteBytes", stats.narWriteBytes);
|
||||
nested.attr("narWriteCompressedBytes", stats.narWriteCompressedBytes);
|
||||
nested.attr("narWriteCompressionTimeMs", stats.narWriteCompressionTimeMs);
|
||||
nested.attr("narCompressionSavings",
|
||||
stats.narWriteBytes
|
||||
? 1.0 - (double) stats.narWriteCompressedBytes / stats.narWriteBytes
|
||||
: 0.0);
|
||||
nested.attr("narCompressionSpeed", // MiB/s
|
||||
statusJson["store"] = {
|
||||
{"narInfoRead", stats.narInfoRead.load()},
|
||||
{"narInfoReadAverted", stats.narInfoReadAverted.load()},
|
||||
{"narInfoMissing", stats.narInfoMissing.load()},
|
||||
{"narInfoWrite", stats.narInfoWrite.load()},
|
||||
{"narInfoCacheSize", stats.pathInfoCacheSize.load()},
|
||||
{"narRead", stats.narRead.load()},
|
||||
{"narReadBytes", stats.narReadBytes.load()},
|
||||
{"narReadCompressedBytes", stats.narReadCompressedBytes.load()},
|
||||
{"narWrite", stats.narWrite.load()},
|
||||
{"narWriteAverted", stats.narWriteAverted.load()},
|
||||
{"narWriteBytes", stats.narWriteBytes.load()},
|
||||
{"narWriteCompressedBytes", stats.narWriteCompressedBytes.load()},
|
||||
{"narWriteCompressionTimeMs", stats.narWriteCompressionTimeMs.load()},
|
||||
{"narCompressionSavings",
|
||||
stats.narWriteBytes
|
||||
? 1.0 - (double) stats.narWriteCompressedBytes / stats.narWriteBytes
|
||||
: 0.0},
|
||||
{"narCompressionSpeed", // MiB/s
|
||||
stats.narWriteCompressionTimeMs
|
||||
? (double) stats.narWriteBytes / stats.narWriteCompressionTimeMs * 1000.0 / (1024.0 * 1024.0)
|
||||
: 0.0);
|
||||
: 0.0},
|
||||
};
|
||||
|
||||
#if NIX_WITH_S3_SUPPORT
|
||||
auto s3Store = dynamic_cast<S3BinaryCacheStore *>(&*store);
|
||||
if (s3Store) {
|
||||
auto nested2 = nested.object("s3");
|
||||
auto & s3Stats = s3Store->getS3Stats();
|
||||
nested2.attr("put", s3Stats.put);
|
||||
nested2.attr("putBytes", s3Stats.putBytes);
|
||||
nested2.attr("putTimeMs", s3Stats.putTimeMs);
|
||||
nested2.attr("putSpeed",
|
||||
s3Stats.putTimeMs
|
||||
? (double) s3Stats.putBytes / s3Stats.putTimeMs * 1000.0 / (1024.0 * 1024.0)
|
||||
: 0.0);
|
||||
nested2.attr("get", s3Stats.get);
|
||||
nested2.attr("getBytes", s3Stats.getBytes);
|
||||
nested2.attr("getTimeMs", s3Stats.getTimeMs);
|
||||
nested2.attr("getSpeed",
|
||||
s3Stats.getTimeMs
|
||||
? (double) s3Stats.getBytes / s3Stats.getTimeMs * 1000.0 / (1024.0 * 1024.0)
|
||||
: 0.0);
|
||||
nested2.attr("head", s3Stats.head);
|
||||
nested2.attr("costDollarApprox",
|
||||
(s3Stats.get + s3Stats.head) / 10000.0 * 0.004
|
||||
+ s3Stats.put / 1000.0 * 0.005 +
|
||||
+ s3Stats.getBytes / (1024.0 * 1024.0 * 1024.0) * 0.09);
|
||||
auto jsonS3 = statusJson["s3"] = {
|
||||
{"put", s3Stats.put.load()},
|
||||
{"putBytes", s3Stats.putBytes.load()},
|
||||
{"putTimeMs", s3Stats.putTimeMs.load()},
|
||||
{"putSpeed",
|
||||
s3Stats.putTimeMs
|
||||
? (double) s3Stats.putBytes / s3Stats.putTimeMs * 1000.0 / (1024.0 * 1024.0)
|
||||
: 0.0},
|
||||
{"get", s3Stats.get.load()},
|
||||
{"getBytes", s3Stats.getBytes.load()},
|
||||
{"getTimeMs", s3Stats.getTimeMs.load()},
|
||||
{"getSpeed",
|
||||
s3Stats.getTimeMs
|
||||
? (double) s3Stats.getBytes / s3Stats.getTimeMs * 1000.0 / (1024.0 * 1024.0)
|
||||
: 0.0},
|
||||
{"head", s3Stats.head.load()},
|
||||
{"costDollarApprox",
|
||||
(s3Stats.get + s3Stats.head) / 10000.0 * 0.004
|
||||
+ s3Stats.put / 1000.0 * 0.005 +
|
||||
+ s3Stats.getBytes / (1024.0 * 1024.0 * 1024.0) * 0.09},
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
{
|
||||
auto mc = startDbUpdate();
|
||||
pqxx::work txn(conn);
|
||||
// FIXME: use PostgreSQL 9.5 upsert.
|
||||
txn.exec("delete from SystemStatus where what = 'queue-runner'");
|
||||
txn.exec_params0("insert into SystemStatus values ('queue-runner', $1)", out.str());
|
||||
txn.exec("delete from SystemStatus where what = 'queue-runner'").no_rows();
|
||||
txn.exec("insert into SystemStatus values ('queue-runner', $1)", pqxx::params{statusJson.dump()}).no_rows();
|
||||
txn.exec("notify status_dumped");
|
||||
txn.commit();
|
||||
}
|
||||
@@ -790,7 +790,7 @@ void State::unlock()
|
||||
|
||||
{
|
||||
pqxx::work txn(*conn);
|
||||
txn.exec("delete from SystemStatus where what = 'queue-runner'");
|
||||
txn.exec("delete from SystemStatus where what = 'queue-runner'").no_rows();
|
||||
txn.commit();
|
||||
}
|
||||
}
|
||||
@@ -820,7 +820,7 @@ void State::run(BuildID buildOne)
|
||||
<< metricsAddr << "/metrics (port " << exposerPort << ")"
|
||||
<< std::endl;
|
||||
|
||||
Store::Params localParams;
|
||||
Store::Config::Params localParams;
|
||||
localParams["max-connections"] = "16";
|
||||
localParams["max-connection-age"] = "600";
|
||||
localStore = openStore(getEnv("NIX_REMOTE").value_or(""), localParams);
|
||||
@@ -868,11 +868,10 @@ void State::run(BuildID buildOne)
|
||||
pqxx::work txn(*conn);
|
||||
for (auto & step : steps) {
|
||||
printMsg(lvlError, "cleaning orphaned step %d of build %d", step.second, step.first);
|
||||
txn.exec_params0
|
||||
("update BuildSteps set busy = 0, status = $1 where build = $2 and stepnr = $3 and busy != 0",
|
||||
(int) bsAborted,
|
||||
txn.exec("update BuildSteps set busy = 0, status = $1 where build = $2 and stepnr = $3 and busy != 0",
|
||||
pqxx::params{(int) bsAborted,
|
||||
step.first,
|
||||
step.second);
|
||||
step.second}).no_rows();
|
||||
}
|
||||
txn.commit();
|
||||
} catch (std::exception & e) {
|
||||
@@ -902,10 +901,17 @@ void State::run(BuildID buildOne)
|
||||
while (true) {
|
||||
try {
|
||||
auto conn(dbPool.get());
|
||||
receiver dumpStatus_(*conn, "dump_status");
|
||||
while (true) {
|
||||
conn->await_notification();
|
||||
dumpStatus(*conn);
|
||||
try {
|
||||
receiver dumpStatus_(*conn, "dump_status");
|
||||
while (true) {
|
||||
conn->await_notification();
|
||||
dumpStatus(*conn);
|
||||
}
|
||||
} catch (pqxx::broken_connection & connEx) {
|
||||
printMsg(lvlError, "main thread: %s", connEx.what());
|
||||
printMsg(lvlError, "main thread: Reconnecting in 10s");
|
||||
conn.markBad();
|
||||
sleep(10);
|
||||
}
|
||||
} catch (std::exception & e) {
|
||||
printMsg(lvlError, "main thread: %s", e.what());
|
||||
@@ -950,7 +956,6 @@ int main(int argc, char * * argv)
|
||||
});
|
||||
|
||||
settings.verboseBuild = true;
|
||||
settings.lockCPU = false;
|
||||
|
||||
State state{metricsAddrOpt};
|
||||
if (status)
|
||||
|
||||
24
src/hydra-queue-runner/meson.build
Normal file
24
src/hydra-queue-runner/meson.build
Normal file
@@ -0,0 +1,24 @@
|
||||
srcs = files(
|
||||
'builder.cc',
|
||||
'build-remote.cc',
|
||||
'build-result.cc',
|
||||
'dispatcher.cc',
|
||||
'hydra-queue-runner.cc',
|
||||
'nar-extractor.cc',
|
||||
'queue-monitor.cc',
|
||||
)
|
||||
|
||||
hydra_queue_runner = executable('hydra-queue-runner',
|
||||
'hydra-queue-runner.cc',
|
||||
srcs,
|
||||
dependencies: [
|
||||
libhydra_dep,
|
||||
nix_util_dep,
|
||||
nix_store_dep,
|
||||
nix_main_dep,
|
||||
pqxx_dep,
|
||||
prom_cpp_core_dep,
|
||||
prom_cpp_pull_dep,
|
||||
],
|
||||
install: true,
|
||||
)
|
||||
@@ -1,12 +1,51 @@
|
||||
#include "nar-extractor.hh"
|
||||
|
||||
#include "archive.hh"
|
||||
#include <nix/util/archive.hh>
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace nix;
|
||||
|
||||
struct Extractor : ParseSink
|
||||
|
||||
struct NarMemberConstructor : CreateRegularFileSink
|
||||
{
|
||||
NarMemberData & curMember;
|
||||
|
||||
HashSink hashSink = HashSink { HashAlgorithm::SHA256 };
|
||||
|
||||
std::optional<uint64_t> expectedSize;
|
||||
|
||||
NarMemberConstructor(NarMemberData & curMember)
|
||||
: curMember(curMember)
|
||||
{ }
|
||||
|
||||
void isExecutable() override
|
||||
{
|
||||
}
|
||||
|
||||
void preallocateContents(uint64_t size) override
|
||||
{
|
||||
expectedSize = size;
|
||||
}
|
||||
|
||||
void operator () (std::string_view data) override
|
||||
{
|
||||
assert(expectedSize);
|
||||
*curMember.fileSize += data.size();
|
||||
hashSink(data);
|
||||
if (curMember.contents) {
|
||||
curMember.contents->append(data);
|
||||
}
|
||||
assert(curMember.fileSize <= expectedSize);
|
||||
if (curMember.fileSize == expectedSize) {
|
||||
auto [hash, len] = hashSink.finish();
|
||||
assert(curMember.fileSize == len);
|
||||
curMember.sha256 = hash;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Extractor : FileSystemObjectSink
|
||||
{
|
||||
std::unordered_set<Path> filesToKeep {
|
||||
"/nix-support/hydra-build-products",
|
||||
@@ -15,58 +54,40 @@ struct Extractor : ParseSink
|
||||
};
|
||||
|
||||
NarMemberDatas & members;
|
||||
NarMemberData * curMember = nullptr;
|
||||
Path prefix;
|
||||
std::filesystem::path prefix;
|
||||
|
||||
Path toKey(const CanonPath & path)
|
||||
{
|
||||
std::filesystem::path p = prefix;
|
||||
// Conditional to avoid trailing slash
|
||||
if (!path.isRoot()) p /= path.rel();
|
||||
return p;
|
||||
}
|
||||
|
||||
Extractor(NarMemberDatas & members, const Path & prefix)
|
||||
: members(members), prefix(prefix)
|
||||
{ }
|
||||
|
||||
void createDirectory(const Path & path) override
|
||||
void createDirectory(const CanonPath & path) override
|
||||
{
|
||||
members.insert_or_assign(prefix + path, NarMemberData { .type = FSAccessor::Type::tDirectory });
|
||||
members.insert_or_assign(toKey(path), NarMemberData { .type = SourceAccessor::Type::tDirectory });
|
||||
}
|
||||
|
||||
void createRegularFile(const Path & path) override
|
||||
void createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)> func) override
|
||||
{
|
||||
curMember = &members.insert_or_assign(prefix + path, NarMemberData {
|
||||
.type = FSAccessor::Type::tRegular,
|
||||
.fileSize = 0,
|
||||
.contents = filesToKeep.count(path) ? std::optional("") : std::nullopt,
|
||||
}).first->second;
|
||||
NarMemberConstructor nmc {
|
||||
members.insert_or_assign(toKey(path), NarMemberData {
|
||||
.type = SourceAccessor::Type::tRegular,
|
||||
.fileSize = 0,
|
||||
.contents = filesToKeep.count(path.abs()) ? std::optional("") : std::nullopt,
|
||||
}).first->second,
|
||||
};
|
||||
func(nmc);
|
||||
}
|
||||
|
||||
std::optional<uint64_t> expectedSize;
|
||||
std::unique_ptr<HashSink> hashSink;
|
||||
|
||||
void preallocateContents(uint64_t size) override
|
||||
void createSymlink(const CanonPath & path, const std::string & target) override
|
||||
{
|
||||
expectedSize = size;
|
||||
hashSink = std::make_unique<HashSink>(htSHA256);
|
||||
}
|
||||
|
||||
void receiveContents(std::string_view data) override
|
||||
{
|
||||
assert(expectedSize);
|
||||
assert(curMember);
|
||||
assert(hashSink);
|
||||
*curMember->fileSize += data.size();
|
||||
(*hashSink)(data);
|
||||
if (curMember->contents) {
|
||||
curMember->contents->append(data);
|
||||
}
|
||||
assert(curMember->fileSize <= expectedSize);
|
||||
if (curMember->fileSize == expectedSize) {
|
||||
auto [hash, len] = hashSink->finish();
|
||||
assert(curMember->fileSize == len);
|
||||
curMember->sha256 = hash;
|
||||
hashSink.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void createSymlink(const Path & path, const std::string & target) override
|
||||
{
|
||||
members.insert_or_assign(prefix + path, NarMemberData { .type = FSAccessor::Type::tSymlink });
|
||||
members.insert_or_assign(toKey(path), NarMemberData { .type = SourceAccessor::Type::tSymlink });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs-accessor.hh"
|
||||
#include "types.hh"
|
||||
#include "serialise.hh"
|
||||
#include "hash.hh"
|
||||
#include <nix/util/source-accessor.hh>
|
||||
#include <nix/util/types.hh>
|
||||
#include <nix/util/serialise.hh>
|
||||
#include <nix/util/hash.hh>
|
||||
|
||||
struct NarMemberData
|
||||
{
|
||||
nix::FSAccessor::Type type;
|
||||
nix::SourceAccessor::Type type;
|
||||
std::optional<uint64_t> fileSize;
|
||||
std::optional<std::string> contents;
|
||||
std::optional<nix::Hash> sha256;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "state.hh"
|
||||
#include "hydra-build-result.hh"
|
||||
#include "globals.hh"
|
||||
#include <nix/store/globals.hh>
|
||||
#include <nix/store/parsed-derivations.hh>
|
||||
#include <nix/util/thread-pool.hh>
|
||||
|
||||
#include <cstring>
|
||||
#include <signal.h>
|
||||
|
||||
using namespace nix;
|
||||
|
||||
@@ -10,63 +13,74 @@ using namespace nix;
|
||||
void State::queueMonitor()
|
||||
{
|
||||
while (true) {
|
||||
auto conn(dbPool.get());
|
||||
try {
|
||||
queueMonitorLoop();
|
||||
queueMonitorLoop(*conn);
|
||||
} catch (pqxx::broken_connection & e) {
|
||||
printMsg(lvlError, "queue monitor: %s", e.what());
|
||||
printMsg(lvlError, "queue monitor: Reconnecting in 10s");
|
||||
conn.markBad();
|
||||
sleep(10);
|
||||
} catch (std::exception & e) {
|
||||
printMsg(lvlError, format("queue monitor: %1%") % e.what());
|
||||
printError("queue monitor: %s", e.what());
|
||||
sleep(10); // probably a DB problem, so don't retry right away
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void State::queueMonitorLoop()
|
||||
void State::queueMonitorLoop(Connection & conn)
|
||||
{
|
||||
auto conn(dbPool.get());
|
||||
|
||||
receiver buildsAdded(*conn, "builds_added");
|
||||
receiver buildsRestarted(*conn, "builds_restarted");
|
||||
receiver buildsCancelled(*conn, "builds_cancelled");
|
||||
receiver buildsDeleted(*conn, "builds_deleted");
|
||||
receiver buildsBumped(*conn, "builds_bumped");
|
||||
receiver jobsetSharesChanged(*conn, "jobset_shares_changed");
|
||||
receiver buildsAdded(conn, "builds_added");
|
||||
receiver buildsRestarted(conn, "builds_restarted");
|
||||
receiver buildsCancelled(conn, "builds_cancelled");
|
||||
receiver buildsDeleted(conn, "builds_deleted");
|
||||
receiver buildsBumped(conn, "builds_bumped");
|
||||
receiver jobsetSharesChanged(conn, "jobset_shares_changed");
|
||||
|
||||
auto destStore = getDestStore();
|
||||
|
||||
unsigned int lastBuildId = 0;
|
||||
|
||||
bool quit = false;
|
||||
while (!quit) {
|
||||
auto t_before_work = std::chrono::steady_clock::now();
|
||||
|
||||
localStore->clearPathInfoCache();
|
||||
|
||||
bool done = getQueuedBuilds(*conn, destStore, lastBuildId);
|
||||
bool done = getQueuedBuilds(conn, destStore);
|
||||
|
||||
if (buildOne && buildOneDone) quit = true;
|
||||
|
||||
auto t_after_work = std::chrono::steady_clock::now();
|
||||
|
||||
prom.queue_monitor_time_spent_running.Increment(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(t_after_work - t_before_work).count());
|
||||
|
||||
/* Sleep until we get notification from the database about an
|
||||
event. */
|
||||
if (done && !quit) {
|
||||
conn->await_notification();
|
||||
conn.await_notification();
|
||||
nrQueueWakeups++;
|
||||
} else
|
||||
conn->get_notifs();
|
||||
conn.get_notifs();
|
||||
|
||||
if (auto lowestId = buildsAdded.get()) {
|
||||
lastBuildId = std::min(lastBuildId, static_cast<unsigned>(std::stoul(*lowestId) - 1));
|
||||
printMsg(lvlTalkative, "got notification: new builds added to the queue");
|
||||
}
|
||||
if (buildsRestarted.get()) {
|
||||
printMsg(lvlTalkative, "got notification: builds restarted");
|
||||
lastBuildId = 0; // check all builds
|
||||
}
|
||||
if (buildsCancelled.get() || buildsDeleted.get() || buildsBumped.get()) {
|
||||
printMsg(lvlTalkative, "got notification: builds cancelled or bumped");
|
||||
processQueueChange(*conn);
|
||||
processQueueChange(conn);
|
||||
}
|
||||
if (jobsetSharesChanged.get()) {
|
||||
printMsg(lvlTalkative, "got notification: jobset shares changed");
|
||||
processJobsetSharesChange(*conn);
|
||||
processJobsetSharesChange(conn);
|
||||
}
|
||||
|
||||
auto t_after_sleep = std::chrono::steady_clock::now();
|
||||
prom.queue_monitor_time_spent_waiting.Increment(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(t_after_sleep - t_after_work).count());
|
||||
}
|
||||
|
||||
exit(0);
|
||||
@@ -80,39 +94,31 @@ struct PreviousFailure : public std::exception {
|
||||
|
||||
|
||||
bool State::getQueuedBuilds(Connection & conn,
|
||||
ref<Store> destStore, unsigned int & lastBuildId)
|
||||
ref<Store> destStore)
|
||||
{
|
||||
prom.queue_checks_started.Increment();
|
||||
|
||||
printInfo("checking the queue for builds > %d...", lastBuildId);
|
||||
printInfo("checking the queue for builds...");
|
||||
|
||||
/* Grab the queued builds from the database, but don't process
|
||||
them yet (since we don't want a long-running transaction). */
|
||||
std::vector<BuildID> newIDs;
|
||||
std::map<BuildID, Build::ptr> newBuildsByID;
|
||||
std::unordered_map<BuildID, Build::ptr> newBuildsByID;
|
||||
std::multimap<StorePath, BuildID> newBuildsByPath;
|
||||
|
||||
unsigned int newLastBuildId = lastBuildId;
|
||||
|
||||
{
|
||||
pqxx::work txn(conn);
|
||||
|
||||
auto res = txn.exec_params
|
||||
("select builds.id, builds.jobset_id, jobsets.project as project, "
|
||||
auto res = txn.exec("select builds.id, builds.jobset_id, jobsets.project as project, "
|
||||
"jobsets.name as jobset, job, drvPath, maxsilent, timeout, timestamp, "
|
||||
"globalPriority, priority from Builds "
|
||||
"inner join jobsets on builds.jobset_id = jobsets.id "
|
||||
"where builds.id > $1 and finished = 0 order by globalPriority desc, builds.id",
|
||||
lastBuildId);
|
||||
"where finished = 0 order by globalPriority desc, random()");
|
||||
|
||||
for (auto const & row : res) {
|
||||
auto builds_(builds.lock());
|
||||
BuildID id = row["id"].as<BuildID>();
|
||||
if (buildOne && id != buildOne) continue;
|
||||
if (id > newLastBuildId) {
|
||||
newLastBuildId = id;
|
||||
prom.queue_max_id.Set(id);
|
||||
}
|
||||
if (builds_->count(id)) continue;
|
||||
|
||||
auto build = std::make_shared<Build>(
|
||||
@@ -142,21 +148,20 @@ bool State::getQueuedBuilds(Connection & conn,
|
||||
|
||||
createBuild = [&](Build::ptr build) {
|
||||
prom.queue_build_loads.Increment();
|
||||
printMsg(lvlTalkative, format("loading build %1% (%2%)") % build->id % build->fullJobName());
|
||||
printMsg(lvlTalkative, "loading build %1% (%2%)", build->id, build->fullJobName());
|
||||
nrAdded++;
|
||||
newBuildsByID.erase(build->id);
|
||||
|
||||
if (!localStore->isValidPath(build->drvPath)) {
|
||||
/* Derivation has been GC'ed prematurely. */
|
||||
printMsg(lvlError, format("aborting GC'ed build %1%") % build->id);
|
||||
printError("aborting GC'ed build %1%", build->id);
|
||||
if (!build->finishedInDB) {
|
||||
auto mc = startDbUpdate();
|
||||
pqxx::work txn(conn);
|
||||
txn.exec_params0
|
||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3 where id = $1 and finished = 0",
|
||||
build->id,
|
||||
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3 where id = $1 and finished = 0",
|
||||
pqxx::params{build->id,
|
||||
(int) bsAborted,
|
||||
time(0));
|
||||
time(0)}).no_rows();
|
||||
txn.commit();
|
||||
build->finishedInDB = true;
|
||||
nrBuildsDone++;
|
||||
@@ -186,32 +191,33 @@ bool State::getQueuedBuilds(Connection & conn,
|
||||
derivation path, then by output path. */
|
||||
BuildID propagatedFrom = 0;
|
||||
|
||||
auto res = txn.exec_params1
|
||||
("select max(build) from BuildSteps where drvPath = $1 and startTime != 0 and stopTime != 0 and status = 1",
|
||||
localStore->printStorePath(ex.step->drvPath));
|
||||
auto res = txn.exec("select max(build) from BuildSteps where drvPath = $1 and startTime != 0 and stopTime != 0 and status = 1",
|
||||
pqxx::params{localStore->printStorePath(ex.step->drvPath)}).one_row();
|
||||
if (!res[0].is_null()) propagatedFrom = res[0].as<BuildID>();
|
||||
|
||||
if (!propagatedFrom) {
|
||||
for (auto & i : ex.step->drv->outputsAndOptPaths(*localStore)) {
|
||||
if (i.second.second) {
|
||||
auto res = txn.exec_params
|
||||
("select max(s.build) from BuildSteps s join BuildStepOutputs o on s.build = o.build where path = $1 and startTime != 0 and stopTime != 0 and status = 1",
|
||||
localStore->printStorePath(*i.second.second));
|
||||
if (!res[0][0].is_null()) {
|
||||
propagatedFrom = res[0][0].as<BuildID>();
|
||||
break;
|
||||
}
|
||||
for (auto & [outputName, optOutputPath] : destStore->queryPartialDerivationOutputMap(ex.step->drvPath, &*localStore)) {
|
||||
constexpr std::string_view common = "select max(s.build) from BuildSteps s join BuildStepOutputs o on s.build = o.build where startTime != 0 and stopTime != 0 and status = 1";
|
||||
auto res = optOutputPath
|
||||
? txn.exec(
|
||||
std::string { common } + " and path = $1",
|
||||
pqxx::params{localStore->printStorePath(*optOutputPath)})
|
||||
: txn.exec(
|
||||
std::string { common } + " and drvPath = $1 and name = $2",
|
||||
pqxx::params{localStore->printStorePath(ex.step->drvPath), outputName});
|
||||
if (!res[0][0].is_null()) {
|
||||
propagatedFrom = res[0][0].as<BuildID>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createBuildStep(txn, 0, build->id, ex.step, "", bsCachedFailure, "", propagatedFrom);
|
||||
txn.exec_params
|
||||
("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3, isCachedBuild = 1, notificationPendingSince = $3 "
|
||||
txn.exec("update Builds set finished = 1, buildStatus = $2, startTime = $3, stopTime = $3, isCachedBuild = 1, notificationPendingSince = $3 "
|
||||
"where id = $1 and finished = 0",
|
||||
build->id,
|
||||
pqxx::params{build->id,
|
||||
(int) (ex.step->drvPath == build->drvPath ? bsFailed : bsDepFailed),
|
||||
time(0));
|
||||
time(0)}).no_rows();
|
||||
notifyBuildFinished(txn, build->id, {});
|
||||
txn.commit();
|
||||
build->finishedInDB = true;
|
||||
@@ -236,12 +242,10 @@ bool State::getQueuedBuilds(Connection & conn,
|
||||
/* If we didn't get a step, it means the step's outputs are
|
||||
all valid. So we mark this as a finished, cached build. */
|
||||
if (!step) {
|
||||
auto drv = localStore->readDerivation(build->drvPath);
|
||||
BuildOutput res = getBuildOutputCached(conn, destStore, drv);
|
||||
BuildOutput res = getBuildOutputCached(conn, destStore, build->drvPath);
|
||||
|
||||
for (auto & i : drv.outputsAndOptPaths(*localStore))
|
||||
if (i.second.second)
|
||||
addRoot(*i.second.second);
|
||||
for (auto & i : destStore->queryDerivationOutputMap(build->drvPath, &*localStore))
|
||||
addRoot(i.second);
|
||||
|
||||
{
|
||||
auto mc = startDbUpdate();
|
||||
@@ -292,7 +296,7 @@ bool State::getQueuedBuilds(Connection & conn,
|
||||
try {
|
||||
createBuild(build);
|
||||
} catch (Error & e) {
|
||||
e.addTrace({}, hintfmt("while loading build %d: ", build->id));
|
||||
e.addTrace({}, HintFmt("while loading build %d: ", build->id));
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -302,7 +306,7 @@ bool State::getQueuedBuilds(Connection & conn,
|
||||
|
||||
/* Add the new runnable build steps to ‘runnable’ and wake up
|
||||
the builder threads. */
|
||||
printMsg(lvlChatty, format("got %1% new runnable steps from %2% new builds") % newRunnable.size() % nrAdded);
|
||||
printMsg(lvlChatty, "got %1% new runnable steps from %2% new builds", newRunnable.size(), nrAdded);
|
||||
for (auto & r : newRunnable)
|
||||
makeRunnable(r);
|
||||
|
||||
@@ -312,15 +316,13 @@ bool State::getQueuedBuilds(Connection & conn,
|
||||
|
||||
/* Stop after a certain time to allow priority bumps to be
|
||||
processed. */
|
||||
if (std::chrono::system_clock::now() > start + std::chrono::seconds(600)) {
|
||||
if (std::chrono::system_clock::now() > start + std::chrono::seconds(60)) {
|
||||
prom.queue_checks_early_exits.Increment();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prom.queue_checks_finished.Increment();
|
||||
|
||||
lastBuildId = newBuildsByID.empty() ? newLastBuildId : newBuildsByID.begin()->first - 1;
|
||||
return newBuildsByID.empty();
|
||||
}
|
||||
|
||||
@@ -358,13 +360,13 @@ void State::processQueueChange(Connection & conn)
|
||||
for (auto i = builds_->begin(); i != builds_->end(); ) {
|
||||
auto b = currentIds.find(i->first);
|
||||
if (b == currentIds.end()) {
|
||||
printMsg(lvlInfo, format("discarding cancelled build %1%") % i->first);
|
||||
printInfo("discarding cancelled build %1%", i->first);
|
||||
i = builds_->erase(i);
|
||||
// FIXME: ideally we would interrupt active build steps here.
|
||||
continue;
|
||||
}
|
||||
if (i->second->globalPriority < b->second) {
|
||||
printMsg(lvlInfo, format("priority of build %1% increased") % i->first);
|
||||
printInfo("priority of build %1% increased", i->first);
|
||||
i->second->globalPriority = b->second;
|
||||
i->second->propagatePriorities();
|
||||
}
|
||||
@@ -399,6 +401,34 @@ void State::processQueueChange(Connection & conn)
|
||||
}
|
||||
|
||||
|
||||
std::map<DrvOutput, std::optional<StorePath>> State::getMissingRemotePaths(
|
||||
ref<Store> destStore,
|
||||
const std::map<DrvOutput, std::optional<StorePath>> & paths)
|
||||
{
|
||||
Sync<std::map<DrvOutput, std::optional<StorePath>>> missing_;
|
||||
ThreadPool tp;
|
||||
|
||||
for (auto & [output, maybeOutputPath] : paths) {
|
||||
if (!maybeOutputPath) {
|
||||
auto missing(missing_.lock());
|
||||
missing->insert({output, maybeOutputPath});
|
||||
} else {
|
||||
tp.enqueue([&] {
|
||||
if (!destStore->isValidPath(*maybeOutputPath)) {
|
||||
auto missing(missing_.lock());
|
||||
missing->insert({output, maybeOutputPath});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tp.process();
|
||||
|
||||
auto missing(missing_.lock());
|
||||
return *missing;
|
||||
}
|
||||
|
||||
|
||||
Step::ptr State::createStep(ref<Store> destStore,
|
||||
Connection & conn, Build::ptr build, const StorePath & drvPath,
|
||||
Build::ptr referringBuild, Step::ptr referringStep, std::set<StorePath> & finishedDrvs,
|
||||
@@ -457,17 +487,24 @@ Step::ptr State::createStep(ref<Store> destStore,
|
||||
it's not runnable yet, and other threads won't make it
|
||||
runnable while step->created == false. */
|
||||
step->drv = std::make_unique<Derivation>(localStore->readDerivation(drvPath));
|
||||
step->parsedDrv = std::make_unique<ParsedDerivation>(drvPath, *step->drv);
|
||||
{
|
||||
try {
|
||||
step->drvOptions = std::make_unique<DerivationOptions>(
|
||||
DerivationOptions::fromStructuredAttrs(
|
||||
step->drv->env,
|
||||
step->drv->structuredAttrs ? &*step->drv->structuredAttrs : nullptr));
|
||||
} catch (Error & e) {
|
||||
e.addTrace({}, "while parsing derivation '%s'", localStore->printStorePath(drvPath));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
step->preferLocalBuild = step->parsedDrv->willBuildLocally(*localStore);
|
||||
step->preferLocalBuild = step->drvOptions->willBuildLocally(*localStore, *step->drv);
|
||||
step->isDeterministic = getOr(step->drv->env, "isDetermistic", "0") == "1";
|
||||
|
||||
step->systemType = step->drv->platform;
|
||||
{
|
||||
auto i = step->drv->env.find("requiredSystemFeatures");
|
||||
StringSet features;
|
||||
if (i != step->drv->env.end())
|
||||
features = step->requiredSystemFeatures = tokenizeString<std::set<std::string>>(i->second);
|
||||
StringSet features = step->requiredSystemFeatures = step->drvOptions->getRequiredSystemFeatures(*step->drv);
|
||||
if (step->preferLocalBuild)
|
||||
features.insert("local");
|
||||
if (!features.empty()) {
|
||||
@@ -481,26 +518,40 @@ Step::ptr State::createStep(ref<Store> destStore,
|
||||
throw PreviousFailure{step};
|
||||
|
||||
/* Are all outputs valid? */
|
||||
bool valid = true;
|
||||
DerivationOutputs missing;
|
||||
for (auto & i : step->drv->outputs)
|
||||
if (!destStore->isValidPath(*i.second.path(*localStore, step->drv->name, i.first))) {
|
||||
valid = false;
|
||||
missing.insert_or_assign(i.first, i.second);
|
||||
}
|
||||
auto outputHashes = staticOutputHashes(*localStore, *(step->drv));
|
||||
std::map<DrvOutput, std::optional<StorePath>> paths;
|
||||
for (auto & [outputName, maybeOutputPath] : destStore->queryPartialDerivationOutputMap(drvPath, &*localStore)) {
|
||||
auto outputHash = outputHashes.at(outputName);
|
||||
paths.insert({{outputHash, outputName}, maybeOutputPath});
|
||||
}
|
||||
|
||||
auto missing = getMissingRemotePaths(destStore, paths);
|
||||
bool valid = missing.empty();
|
||||
|
||||
/* Try to copy the missing paths from the local store or from
|
||||
substitutes. */
|
||||
if (!missing.empty()) {
|
||||
|
||||
size_t avail = 0;
|
||||
for (auto & i : missing) {
|
||||
auto path = i.second.path(*localStore, step->drv->name, i.first);
|
||||
if (/* localStore != destStore && */ localStore->isValidPath(*path))
|
||||
for (auto & [i, pathOpt] : missing) {
|
||||
// If we don't know the output path from the destination
|
||||
// store, see if the local store can tell us.
|
||||
if (/* localStore != destStore && */ !pathOpt && experimentalFeatureSettings.isEnabled(Xp::CaDerivations))
|
||||
if (auto maybeRealisation = localStore->queryRealisation(i))
|
||||
pathOpt = maybeRealisation->outPath;
|
||||
|
||||
if (!pathOpt) {
|
||||
// No hope of getting the store object if we don't know
|
||||
// the path.
|
||||
continue;
|
||||
}
|
||||
auto & path = *pathOpt;
|
||||
|
||||
if (/* localStore != destStore && */ localStore->isValidPath(path))
|
||||
avail++;
|
||||
else if (useSubstitutes) {
|
||||
SubstitutablePathInfos infos;
|
||||
localStore->querySubstitutablePathInfos({{*path, {}}}, infos);
|
||||
localStore->querySubstitutablePathInfos({{path, {}}}, infos);
|
||||
if (infos.size() == 1)
|
||||
avail++;
|
||||
}
|
||||
@@ -508,26 +559,29 @@ Step::ptr State::createStep(ref<Store> destStore,
|
||||
|
||||
if (missing.size() == avail) {
|
||||
valid = true;
|
||||
for (auto & i : missing) {
|
||||
auto path = i.second.path(*localStore, step->drv->name, i.first);
|
||||
for (auto & [i, pathOpt] : missing) {
|
||||
// If we found everything, then we should know the path
|
||||
// to every missing store object now.
|
||||
assert(pathOpt);
|
||||
auto & path = *pathOpt;
|
||||
|
||||
try {
|
||||
time_t startTime = time(0);
|
||||
|
||||
if (localStore->isValidPath(*path))
|
||||
if (localStore->isValidPath(path))
|
||||
printInfo("copying output ‘%1%’ of ‘%2%’ from local store",
|
||||
localStore->printStorePath(*path),
|
||||
localStore->printStorePath(path),
|
||||
localStore->printStorePath(drvPath));
|
||||
else {
|
||||
printInfo("substituting output ‘%1%’ of ‘%2%’",
|
||||
localStore->printStorePath(*path),
|
||||
localStore->printStorePath(path),
|
||||
localStore->printStorePath(drvPath));
|
||||
localStore->ensurePath(*path);
|
||||
localStore->ensurePath(path);
|
||||
// FIXME: should copy directly from substituter to destStore.
|
||||
}
|
||||
|
||||
copyClosure(*localStore, *destStore,
|
||||
StorePathSet { *path },
|
||||
StorePathSet { path },
|
||||
NoRepair, CheckSigs, NoSubstitute);
|
||||
|
||||
time_t stopTime = time(0);
|
||||
@@ -535,13 +589,13 @@ Step::ptr State::createStep(ref<Store> destStore,
|
||||
{
|
||||
auto mc = startDbUpdate();
|
||||
pqxx::work txn(conn);
|
||||
createSubstitutionStep(txn, startTime, stopTime, build, drvPath, "out", *path);
|
||||
createSubstitutionStep(txn, startTime, stopTime, build, drvPath, *(step->drv), "out", path);
|
||||
txn.commit();
|
||||
}
|
||||
|
||||
} catch (Error & e) {
|
||||
printError("while copying/substituting output ‘%s’ of ‘%s’: %s",
|
||||
localStore->printStorePath(*path),
|
||||
localStore->printStorePath(path),
|
||||
localStore->printStorePath(drvPath),
|
||||
e.what());
|
||||
valid = false;
|
||||
@@ -561,7 +615,7 @@ Step::ptr State::createStep(ref<Store> destStore,
|
||||
printMsg(lvlDebug, "creating build step ‘%1%’", localStore->printStorePath(drvPath));
|
||||
|
||||
/* Create steps for the dependencies. */
|
||||
for (auto & i : step->drv->inputDrvs) {
|
||||
for (auto & i : step->drv->inputDrvs.map) {
|
||||
auto dep = createStep(destStore, conn, build, i.first, 0, step, finishedDrvs, newSteps, newRunnable);
|
||||
if (dep) {
|
||||
auto step_(step->state.lock());
|
||||
@@ -596,10 +650,8 @@ Jobset::ptr State::createJobset(pqxx::work & txn,
|
||||
if (i != jobsets_->end()) return i->second;
|
||||
}
|
||||
|
||||
auto res = txn.exec_params1
|
||||
("select schedulingShares from Jobsets where id = $1",
|
||||
jobsetID);
|
||||
if (res.empty()) throw Error("missing jobset - can't happen");
|
||||
auto res = txn.exec("select schedulingShares from Jobsets where id = $1",
|
||||
pqxx::params{jobsetID}).one_row();
|
||||
|
||||
auto shares = res["schedulingShares"].as<unsigned int>();
|
||||
|
||||
@@ -607,11 +659,10 @@ Jobset::ptr State::createJobset(pqxx::work & txn,
|
||||
jobset->setShares(shares);
|
||||
|
||||
/* Load the build steps from the last 24 hours. */
|
||||
auto res2 = txn.exec_params
|
||||
("select s.startTime, s.stopTime from BuildSteps s join Builds b on build = id "
|
||||
auto res2 = txn.exec("select s.startTime, s.stopTime from BuildSteps s join Builds b on build = id "
|
||||
"where s.startTime is not null and s.stopTime > $1 and jobset_id = $2",
|
||||
time(0) - Jobset::schedulingWindow * 10,
|
||||
jobsetID);
|
||||
pqxx::params{time(0) - Jobset::schedulingWindow * 10,
|
||||
jobsetID});
|
||||
for (auto const & row : res2) {
|
||||
time_t startTime = row["startTime"].as<time_t>();
|
||||
time_t stopTime = row["stopTime"].as<time_t>();
|
||||
@@ -640,21 +691,22 @@ void State::processJobsetSharesChange(Connection & conn)
|
||||
}
|
||||
|
||||
|
||||
BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore, const nix::Derivation & drv)
|
||||
BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore, const nix::StorePath & drvPath)
|
||||
{
|
||||
auto derivationOutputs = destStore->queryDerivationOutputMap(drvPath, &*localStore);
|
||||
|
||||
{
|
||||
pqxx::work txn(conn);
|
||||
|
||||
for (auto & [name, output] : drv.outputsAndOptPaths(*localStore)) {
|
||||
auto r = txn.exec_params
|
||||
("select id, buildStatus, releaseName, closureSize, size from Builds b "
|
||||
for (auto & [name, output] : derivationOutputs) {
|
||||
auto r = txn.exec("select id, buildStatus, releaseName, closureSize, size from Builds b "
|
||||
"join BuildOutputs o on b.id = o.build "
|
||||
"where finished = 1 and (buildStatus = 0 or buildStatus = 6) and path = $1",
|
||||
localStore->printStorePath(*output.second));
|
||||
pqxx::params{localStore->printStorePath(output)});
|
||||
if (r.empty()) continue;
|
||||
BuildID id = r[0][0].as<BuildID>();
|
||||
|
||||
printMsg(lvlInfo, format("reusing build %d") % id);
|
||||
printInfo("reusing build %d", id);
|
||||
|
||||
BuildOutput res;
|
||||
res.failed = r[0][1].as<int>() == bsFailedWithOutput;
|
||||
@@ -662,9 +714,8 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
||||
res.closureSize = r[0][3].is_null() ? 0 : r[0][3].as<uint64_t>();
|
||||
res.size = r[0][4].is_null() ? 0 : r[0][4].as<uint64_t>();
|
||||
|
||||
auto products = txn.exec_params
|
||||
("select type, subtype, fileSize, sha256hash, path, name, defaultPath from BuildProducts where build = $1 order by productnr",
|
||||
id);
|
||||
auto products = txn.exec("select type, subtype, fileSize, sha256hash, path, name, defaultPath from BuildProducts where build = $1 order by productnr",
|
||||
pqxx::params{id});
|
||||
|
||||
for (auto row : products) {
|
||||
BuildProduct product;
|
||||
@@ -677,7 +728,7 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
||||
product.fileSize = row[2].as<off_t>();
|
||||
}
|
||||
if (!row[3].is_null())
|
||||
product.sha256hash = Hash::parseAny(row[3].as<std::string>(), htSHA256);
|
||||
product.sha256hash = Hash::parseAny(row[3].as<std::string>(), HashAlgorithm::SHA256);
|
||||
if (!row[4].is_null())
|
||||
product.path = row[4].as<std::string>();
|
||||
product.name = row[5].as<std::string>();
|
||||
@@ -686,9 +737,8 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
||||
res.products.emplace_back(product);
|
||||
}
|
||||
|
||||
auto metrics = txn.exec_params
|
||||
("select name, unit, value from BuildMetrics where build = $1",
|
||||
id);
|
||||
auto metrics = txn.exec("select name, unit, value from BuildMetrics where build = $1",
|
||||
pqxx::params{id});
|
||||
|
||||
for (auto row : metrics) {
|
||||
BuildMetric metric;
|
||||
@@ -704,5 +754,5 @@ BuildOutput State::getBuildOutputCached(Connection & conn, nix::ref<nix::Store>
|
||||
}
|
||||
|
||||
NarMemberDatas narMembers;
|
||||
return getBuildOutput(destStore, narMembers, drv);
|
||||
return getBuildOutput(destStore, narMembers, derivationOutputs);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <regex>
|
||||
#include <semaphore>
|
||||
|
||||
#include <prometheus/counter.h>
|
||||
#include <prometheus/gauge.h>
|
||||
@@ -14,13 +15,19 @@
|
||||
|
||||
#include "db.hh"
|
||||
|
||||
#include "parsed-derivations.hh"
|
||||
#include "pathlocks.hh"
|
||||
#include "pool.hh"
|
||||
#include "build-result.hh"
|
||||
#include "store-api.hh"
|
||||
#include "sync.hh"
|
||||
#include <nix/store/derivations.hh>
|
||||
#include <nix/store/derivation-options.hh>
|
||||
#include <nix/store/pathlocks.hh>
|
||||
#include <nix/util/pool.hh>
|
||||
#include <nix/store/build-result.hh>
|
||||
#include <nix/store/store-api.hh>
|
||||
#include <nix/util/sync.hh>
|
||||
#include "nar-extractor.hh"
|
||||
#include <nix/store/serve-protocol.hh>
|
||||
#include <nix/store/serve-protocol-impl.hh>
|
||||
#include <nix/store/serve-protocol-connection.hh>
|
||||
#include <nix/store/machines.hh>
|
||||
#include <nix/store/globals.hh>
|
||||
|
||||
|
||||
typedef unsigned int BuildID;
|
||||
@@ -54,6 +61,7 @@ typedef enum {
|
||||
ssConnecting = 10,
|
||||
ssSendingInputs = 20,
|
||||
ssBuilding = 30,
|
||||
ssWaitingForLocalSlot = 35,
|
||||
ssReceivingOutputs = 40,
|
||||
ssPostProcessing = 50,
|
||||
} StepState;
|
||||
@@ -78,6 +86,8 @@ struct RemoteResult
|
||||
{
|
||||
return stepStatus == bsCachedFailure ? bsFailed : stepStatus;
|
||||
}
|
||||
|
||||
void updateWithBuildResult(const nix::BuildResult &);
|
||||
};
|
||||
|
||||
|
||||
@@ -162,8 +172,8 @@ struct Step
|
||||
|
||||
nix::StorePath drvPath;
|
||||
std::unique_ptr<nix::Derivation> drv;
|
||||
std::unique_ptr<nix::ParsedDerivation> parsedDrv;
|
||||
std::set<std::string> requiredSystemFeatures;
|
||||
std::unique_ptr<nix::DerivationOptions> drvOptions;
|
||||
nix::StringSet requiredSystemFeatures;
|
||||
bool preferLocalBuild;
|
||||
bool isDeterministic;
|
||||
std::string systemType; // concatenation of drv.platform and requiredSystemFeatures
|
||||
@@ -231,18 +241,10 @@ void getDependents(Step::ptr step, std::set<Build::ptr> & builds, std::set<Step:
|
||||
void visitDependencies(std::function<void(Step::ptr)> visitor, Step::ptr step);
|
||||
|
||||
|
||||
struct Machine
|
||||
struct Machine : nix::Machine
|
||||
{
|
||||
typedef std::shared_ptr<Machine> ptr;
|
||||
|
||||
bool enabled{true};
|
||||
|
||||
std::string sshName, sshKey;
|
||||
std::set<std::string> systemTypes, supportedFeatures, mandatoryFeatures;
|
||||
unsigned int maxJobs = 1;
|
||||
float speedFactor = 1.0;
|
||||
std::string sshPublicHostKey;
|
||||
|
||||
struct State {
|
||||
typedef std::shared_ptr<State> ptr;
|
||||
counter currentJobs{0};
|
||||
@@ -292,11 +294,13 @@ struct Machine
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isLocalhost()
|
||||
{
|
||||
std::regex r("^(ssh://|ssh-ng://)?localhost$");
|
||||
return std::regex_search(sshName, r);
|
||||
}
|
||||
bool isLocalhost() const;
|
||||
|
||||
// A connection to a machine
|
||||
struct Connection : nix::ServeProto::BasicClientConnection {
|
||||
// Backpointer to the machine
|
||||
ptr machine;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -350,9 +354,13 @@ private:
|
||||
|
||||
/* The build machines. */
|
||||
std::mutex machinesReadyLock;
|
||||
typedef std::map<std::string, Machine::ptr> Machines;
|
||||
typedef std::map<nix::StoreReference::Variant, Machine::ptr> Machines;
|
||||
nix::Sync<Machines> machines; // FIXME: use atomic_shared_ptr
|
||||
|
||||
/* Throttler for CPU-bound local work. */
|
||||
static constexpr unsigned int maxSupportedLocalWorkers = 1024;
|
||||
std::counting_semaphore<maxSupportedLocalWorkers> localWorkThrottler;
|
||||
|
||||
/* Various stats. */
|
||||
time_t startedAt;
|
||||
counter nrBuildsRead{0};
|
||||
@@ -362,6 +370,7 @@ private:
|
||||
counter nrStepsDone{0};
|
||||
counter nrStepsBuilding{0};
|
||||
counter nrStepsCopyingTo{0};
|
||||
counter nrStepsWaitingForDownloadSlot{0};
|
||||
counter nrStepsCopyingFrom{0};
|
||||
counter nrStepsWaiting{0};
|
||||
counter nrUnsupportedSteps{0};
|
||||
@@ -392,7 +401,6 @@ private:
|
||||
|
||||
struct MachineReservation
|
||||
{
|
||||
typedef std::shared_ptr<MachineReservation> ptr;
|
||||
State & state;
|
||||
Step::ptr step;
|
||||
Machine::ptr machine;
|
||||
@@ -430,7 +438,7 @@ private:
|
||||
|
||||
/* How often the build steps of a jobset should be repeated in
|
||||
order to detect non-determinism. */
|
||||
std::map<std::pair<std::string, std::string>, unsigned int> jobsetRepeats;
|
||||
std::map<std::pair<std::string, std::string>, size_t> jobsetRepeats;
|
||||
|
||||
bool uploadLogsToBinaryCache;
|
||||
|
||||
@@ -450,7 +458,12 @@ private:
|
||||
prometheus::Counter& queue_steps_created;
|
||||
prometheus::Counter& queue_checks_early_exits;
|
||||
prometheus::Counter& queue_checks_finished;
|
||||
prometheus::Gauge& queue_max_id;
|
||||
|
||||
prometheus::Counter& dispatcher_time_spent_running;
|
||||
prometheus::Counter& dispatcher_time_spent_waiting;
|
||||
|
||||
prometheus::Counter& queue_monitor_time_spent_running;
|
||||
prometheus::Counter& queue_monitor_time_spent_waiting;
|
||||
|
||||
PromMetrics();
|
||||
};
|
||||
@@ -485,23 +498,28 @@ private:
|
||||
const std::string & machine);
|
||||
|
||||
int createSubstitutionStep(pqxx::work & txn, time_t startTime, time_t stopTime,
|
||||
Build::ptr build, const nix::StorePath & drvPath, const std::string & outputName, const nix::StorePath & storePath);
|
||||
Build::ptr build, const nix::StorePath & drvPath, const nix::Derivation drv, const std::string & outputName, const nix::StorePath & storePath);
|
||||
|
||||
void updateBuild(pqxx::work & txn, Build::ptr build, BuildStatus status);
|
||||
|
||||
void queueMonitor();
|
||||
|
||||
void queueMonitorLoop();
|
||||
void queueMonitorLoop(Connection & conn);
|
||||
|
||||
/* Check the queue for new builds. */
|
||||
bool getQueuedBuilds(Connection & conn,
|
||||
nix::ref<nix::Store> destStore, unsigned int & lastBuildId);
|
||||
bool getQueuedBuilds(Connection & conn, nix::ref<nix::Store> destStore);
|
||||
|
||||
/* Handle cancellation, deletion and priority bumps. */
|
||||
void processQueueChange(Connection & conn);
|
||||
|
||||
BuildOutput getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore,
|
||||
const nix::Derivation & drv);
|
||||
const nix::StorePath & drvPath);
|
||||
|
||||
/* Returns paths missing from the remote store. Paths are processed in
|
||||
* parallel to work around the possible latency of remote stores. */
|
||||
std::map<nix::DrvOutput, std::optional<nix::StorePath>> getMissingRemotePaths(
|
||||
nix::ref<nix::Store> destStore,
|
||||
const std::map<nix::DrvOutput, std::optional<nix::StorePath>> & paths);
|
||||
|
||||
Step::ptr createStep(nix::ref<nix::Store> store,
|
||||
Connection & conn, Build::ptr build, const nix::StorePath & drvPath,
|
||||
@@ -532,19 +550,19 @@ private:
|
||||
|
||||
void abortUnsupported();
|
||||
|
||||
void builder(MachineReservation::ptr reservation);
|
||||
void builder(std::unique_ptr<MachineReservation> reservation);
|
||||
|
||||
/* Perform the given build step. Return true if the step is to be
|
||||
retried. */
|
||||
enum StepResult { sDone, sRetry, sMaybeCancelled };
|
||||
StepResult doBuildStep(nix::ref<nix::Store> destStore,
|
||||
MachineReservation::ptr reservation,
|
||||
std::unique_ptr<MachineReservation> reservation,
|
||||
std::shared_ptr<ActiveStep> activeStep);
|
||||
|
||||
void buildRemote(nix::ref<nix::Store> destStore,
|
||||
std::unique_ptr<MachineReservation> reservation,
|
||||
Machine::ptr machine, Step::ptr step,
|
||||
unsigned int maxSilentTime, unsigned int buildTimeout,
|
||||
unsigned int repeats,
|
||||
const nix::ServeProto::BuildOptions & buildOptions,
|
||||
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
|
||||
std::function<void(StepState)> updateStep,
|
||||
NarMemberDatas & narMembers);
|
||||
|
||||
@@ -4,7 +4,6 @@ use strict;
|
||||
use warnings;
|
||||
use base 'Hydra::Base::Controller::REST';
|
||||
use List::SomeUtils qw(any);
|
||||
use Nix::Store;
|
||||
use Hydra::Helper::Nix;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
|
||||
@@ -30,7 +29,7 @@ sub getChannelData {
|
||||
my $outputs = {};
|
||||
foreach my $output (@outputs) {
|
||||
my $outPath = $output->get_column("outpath");
|
||||
next if $checkValidity && !isValidPath($outPath);
|
||||
next if $checkValidity && !$MACHINE_LOCAL_STORE->isValidPath($outPath);
|
||||
$outputs->{$output->get_column("outname")} = $outPath;
|
||||
push @storePaths, $outPath;
|
||||
# Put the system type in the manifest (for top-level
|
||||
|
||||
@@ -95,6 +95,7 @@ sub get_legacy_ldap_config {
|
||||
"hydra_bump-to-front" => [ "bump-to-front" ],
|
||||
"hydra_cancel-build" => [ "cancel-build" ],
|
||||
"hydra_create-projects" => [ "create-projects" ],
|
||||
"hydra_eval-jobset" => [ "eval-jobset" ],
|
||||
"hydra_restart-jobs" => [ "restart-jobs" ],
|
||||
},
|
||||
};
|
||||
@@ -159,6 +160,7 @@ sub valid_roles {
|
||||
"bump-to-front",
|
||||
"cancel-build",
|
||||
"create-projects",
|
||||
"eval-jobset",
|
||||
"restart-jobs",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ use DateTime;
|
||||
use Digest::SHA qw(sha256_hex);
|
||||
use Text::Diff;
|
||||
use IPC::Run qw(run);
|
||||
use Digest::SHA qw(hmac_sha256_hex);
|
||||
use String::Compare::ConstantTime qw(equals);
|
||||
use IPC::Run3;
|
||||
|
||||
|
||||
sub api : Chained('/') PathPart('api') CaptureArgs(0) {
|
||||
@@ -216,8 +219,13 @@ sub scmdiff : Path('/api/scmdiff') Args(0) {
|
||||
} elsif ($type eq "git") {
|
||||
my $clonePath = getSCMCacheDir . "/git/" . sha256_hex($uri);
|
||||
die if ! -d $clonePath;
|
||||
$diff .= `(cd $clonePath; git log $rev1..$rev2)`;
|
||||
$diff .= `(cd $clonePath; git diff $rev1..$rev2)`;
|
||||
my ($stdout1, $stderr1);
|
||||
run3(['git', '-C', $clonePath, 'log', "$rev1..$rev2"], \undef, \$stdout1, \$stderr1);
|
||||
$diff .= $stdout1 if $? == 0;
|
||||
|
||||
my ($stdout2, $stderr2);
|
||||
run3(['git', '-C', $clonePath, 'diff', "$rev1..$rev2"], \undef, \$stdout2, \$stderr2);
|
||||
$diff .= $stdout2 if $? == 0;
|
||||
}
|
||||
|
||||
$c->stash->{'plain'} = { data => (scalar $diff) || " " };
|
||||
@@ -239,6 +247,8 @@ sub triggerJobset {
|
||||
sub push : Chained('api') PathPart('push') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
requirePost($c);
|
||||
|
||||
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||||
|
||||
my $force = exists $c->request->query_params->{force};
|
||||
@@ -246,19 +256,24 @@ sub push : Chained('api') PathPart('push') Args(0) {
|
||||
foreach my $s (@jobsets) {
|
||||
my ($p, $j) = parseJobsetName($s);
|
||||
my $jobset = $c->model('DB::Jobsets')->find($p, $j);
|
||||
requireEvalJobsetPrivileges($c, $jobset->project);
|
||||
next unless defined $jobset && ($force || ($jobset->project->enabled && $jobset->enabled));
|
||||
triggerJobset($self, $c, $jobset, $force);
|
||||
}
|
||||
|
||||
my @repos = split /,/, ($c->request->query_params->{repos} // "");
|
||||
foreach my $r (@repos) {
|
||||
triggerJobset($self, $c, $_, $force) foreach $c->model('DB::Jobsets')->search(
|
||||
my @jobsets = $c->model('DB::Jobsets')->search(
|
||||
{ 'project.enabled' => 1, 'me.enabled' => 1 },
|
||||
{
|
||||
join => 'project',
|
||||
where => \ [ 'exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value = ?)', [ 'value', $r ] ],
|
||||
order_by => 'me.id DESC'
|
||||
});
|
||||
foreach my $jobset (@jobsets) {
|
||||
requireEvalJobsetPrivileges($c, $jobset->project);
|
||||
triggerJobset($self, $c, $jobset, $force)
|
||||
}
|
||||
}
|
||||
|
||||
$self->status_ok(
|
||||
@@ -267,13 +282,84 @@ sub push : Chained('api') PathPart('push') Args(0) {
|
||||
);
|
||||
}
|
||||
|
||||
sub verifyWebhookSignature {
|
||||
my ($c, $platform, $header_name, $signature_prefix) = @_;
|
||||
|
||||
# Get secrets from config
|
||||
my $webhook_config = $c->config->{webhooks} // {};
|
||||
my $platform_config = $webhook_config->{$platform} // {};
|
||||
my $secrets = $platform_config->{secret};
|
||||
|
||||
# Normalize to array
|
||||
$secrets = [] unless defined $secrets;
|
||||
$secrets = [$secrets] unless ref($secrets) eq 'ARRAY';
|
||||
|
||||
# Trim whitespace from secrets
|
||||
my @secrets = grep { defined && length } map { s/^\s+|\s+$//gr } @$secrets;
|
||||
|
||||
if (@secrets) {
|
||||
my $signature = $c->request->header($header_name);
|
||||
|
||||
if (!$signature) {
|
||||
$c->log->warn("Webhook authentication failed for $platform: Missing signature from IP " . $c->request->address);
|
||||
$c->response->status(401);
|
||||
$c->stash->{json} = { error => "Missing webhook signature" };
|
||||
$c->forward('View::JSON');
|
||||
return 0;
|
||||
}
|
||||
|
||||
# Get the raw body content from the buffered PSGI input
|
||||
# For JSON requests, Catalyst will have already read and buffered the body
|
||||
my $input = $c->request->env->{'psgi.input'};
|
||||
$input->seek(0, 0);
|
||||
local $/;
|
||||
my $payload = <$input>;
|
||||
$input->seek(0, 0); # Reset for any other consumers
|
||||
|
||||
unless (defined $payload && length $payload) {
|
||||
$c->log->warn("Webhook authentication failed for $platform: Empty request body from IP " . $c->request->address);
|
||||
$c->response->status(400);
|
||||
$c->stash->{json} = { error => "Empty request body" };
|
||||
$c->forward('View::JSON');
|
||||
return 0;
|
||||
}
|
||||
|
||||
my $valid = 0;
|
||||
for my $secret (@secrets) {
|
||||
my $expected = $signature_prefix . hmac_sha256_hex($payload, $secret);
|
||||
if (equals($signature, $expected)) {
|
||||
$valid = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$valid) {
|
||||
$c->log->warn("Webhook authentication failed for $platform: Invalid signature from IP " . $c->request->address);
|
||||
$c->response->status(401);
|
||||
$c->stash->{json} = { error => "Invalid webhook signature" };
|
||||
$c->forward('View::JSON');
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
} else {
|
||||
$c->log->warn("Webhook authentication failed for $platform: Unable to validate signature from IP " . $c->request->address . " because no secrets are configured");
|
||||
$c->response->status(401);
|
||||
$c->stash->{json} = { error => "Invalid webhook signature" };
|
||||
$c->forward('View::JSON');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
sub push_github : Chained('api') PathPart('push-github') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||||
|
||||
return unless verifyWebhookSignature($c, 'github', 'X-Hub-Signature-256', 'sha256=');
|
||||
|
||||
my $in = $c->request->{data};
|
||||
my $owner = $in->{repository}->{owner}->{name} or die;
|
||||
my $owner = ($in->{repository}->{owner}->{name} // $in->{repository}->{owner}->{login}) or die;
|
||||
my $repo = $in->{repository}->{name} or die;
|
||||
print STDERR "got push from GitHub repository $owner/$repo\n";
|
||||
|
||||
@@ -285,6 +371,26 @@ sub push_github : Chained('api') PathPart('push-github') Args(0) {
|
||||
$c->response->body("");
|
||||
}
|
||||
|
||||
sub push_gitea : Chained('api') PathPart('push-gitea') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
$c->{stash}->{json}->{jobsetsTriggered} = [];
|
||||
|
||||
# Note: Gitea doesn't use sha256= prefix
|
||||
return unless verifyWebhookSignature($c, 'gitea', 'X-Gitea-Signature', '');
|
||||
|
||||
my $in = $c->request->{data};
|
||||
my $url = $in->{repository}->{clone_url} or die;
|
||||
$url =~ s/.git$//;
|
||||
print STDERR "got push from Gitea repository $url\n";
|
||||
|
||||
triggerJobset($self, $c, $_, 0) foreach $c->model('DB::Jobsets')->search(
|
||||
{ 'project.enabled' => 1, 'me.enabled' => 1 },
|
||||
{ join => 'project'
|
||||
, where => \ [ 'me.flake like ? or exists (select 1 from JobsetInputAlts where project = me.project and jobset = me.name and value like ?)', [ 'flake', "%$url%"], [ 'value', "%$url%" ] ]
|
||||
});
|
||||
$c->response->body("");
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
||||
@@ -10,11 +10,12 @@ use File::Basename;
|
||||
use File::LibMagic;
|
||||
use File::stat;
|
||||
use Data::Dump qw(dump);
|
||||
use Nix::Store;
|
||||
use Nix::Config;
|
||||
use List::SomeUtils qw(all);
|
||||
use Encode;
|
||||
use JSON::PP;
|
||||
use IPC::Run qw(run);
|
||||
use IPC::Run3;
|
||||
use WWW::Form::UrlEncoded::PP qw();
|
||||
|
||||
use feature 'state';
|
||||
|
||||
@@ -78,14 +79,16 @@ sub build_GET {
|
||||
|
||||
$c->stash->{template} = 'build.tt';
|
||||
$c->stash->{isLocalStore} = isLocalStore();
|
||||
# XXX: If the derivation is content-addressed then this will always return
|
||||
# false because `$_->path` will be empty
|
||||
$c->stash->{available} =
|
||||
$c->stash->{isLocalStore}
|
||||
? all { isValidPath($_->path) } $build->buildoutputs->all
|
||||
? all { $_->path && $MACHINE_LOCAL_STORE->isValidPath($_->path) } $build->buildoutputs->all
|
||||
: 1;
|
||||
$c->stash->{drvAvailable} = isValidPath $build->drvpath;
|
||||
$c->stash->{drvAvailable} = $MACHINE_LOCAL_STORE->isValidPath($build->drvpath);
|
||||
|
||||
if ($build->finished && $build->iscachedbuild) {
|
||||
my $path = ($build->buildoutputs)[0]->path or die;
|
||||
my $path = ($build->buildoutputs)[0]->path or undef;
|
||||
my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path);
|
||||
if (defined $cachedBuildStep) {
|
||||
$c->stash->{cachedBuild} = $cachedBuildStep->build;
|
||||
@@ -139,7 +142,7 @@ sub view_nixlog : Chained('buildChain') PathPart('nixlog') {
|
||||
$c->stash->{step} = $step;
|
||||
|
||||
my $drvPath = $step->drvpath;
|
||||
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [basename($drvPath)]);
|
||||
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath))]);
|
||||
showLog($c, $mode, $log_uri);
|
||||
}
|
||||
|
||||
@@ -148,7 +151,7 @@ sub view_log : Chained('buildChain') PathPart('log') {
|
||||
my ($self, $c, $mode) = @_;
|
||||
|
||||
my $drvPath = $c->stash->{build}->drvpath;
|
||||
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [basename($drvPath)]);
|
||||
my $log_uri = $c->uri_for($c->controller('Root')->action_for("log"), [WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath))]);
|
||||
showLog($c, $mode, $log_uri);
|
||||
}
|
||||
|
||||
@@ -209,7 +212,7 @@ sub checkPath {
|
||||
sub serveFile {
|
||||
my ($c, $path) = @_;
|
||||
|
||||
my $res = run(cmd => ["nix", "--experimental-features", "nix-command",
|
||||
my $res = runCommand(cmd => ["nix", "--experimental-features", "nix-command",
|
||||
"ls-store", "--store", getStoreUri(), "--json", "$path"]);
|
||||
|
||||
if ($res->{status}) {
|
||||
@@ -233,14 +236,25 @@ sub serveFile {
|
||||
}
|
||||
|
||||
elsif ($ls->{type} eq "regular") {
|
||||
# Have the hosted data considered its own origin to avoid being a giant
|
||||
# XSS hole.
|
||||
$c->response->header('Content-Security-Policy' => 'sandbox allow-scripts');
|
||||
|
||||
$c->stash->{'plain'} = { data => grab(cmd => ["nix", "--experimental-features", "nix-command",
|
||||
$c->stash->{'plain'} = { data => readIntoSocket(cmd => ["nix", "--experimental-features", "nix-command",
|
||||
"store", "cat", "--store", getStoreUri(), "$path"]) };
|
||||
|
||||
# Detect MIME type.
|
||||
state $magic = File::LibMagic->new(follow_symlinks => 1);
|
||||
my $info = $magic->info_from_filename($path);
|
||||
my $type = $info->{mime_with_encoding};
|
||||
my $type = "text/plain";
|
||||
if ($path =~ /.*\.(\S{1,})$/xms) {
|
||||
my $ext = $1;
|
||||
my $mimeTypes = MIME::Types->new(only_complete => 1);
|
||||
my $t = $mimeTypes->mimeTypeOf($ext);
|
||||
$type = ref $t ? $t->type : $t if $t;
|
||||
} else {
|
||||
state $magic = File::LibMagic->new(follow_symlinks => 1);
|
||||
my $info = $magic->info_from_filename($path);
|
||||
$type = $info->{mime_with_encoding};
|
||||
}
|
||||
$c->response->content_type($type);
|
||||
$c->forward('Hydra::View::Plain');
|
||||
}
|
||||
@@ -298,7 +312,7 @@ sub output : Chained('buildChain') PathPart Args(1) {
|
||||
error($c, "This build is not finished yet.") unless $build->finished;
|
||||
my $output = $build->buildoutputs->find({name => $outputName});
|
||||
notFound($c, "This build has no output named ‘$outputName’") unless defined $output;
|
||||
gone($c, "Output is no longer available.") unless isValidPath $output->path;
|
||||
gone($c, "Output is no longer available.") unless $MACHINE_LOCAL_STORE->isValidPath($output->path);
|
||||
|
||||
$c->response->header('Content-Disposition', "attachment; filename=\"build-${\$build->id}-${\$outputName}.nar.bz2\"");
|
||||
$c->stash->{current_view} = 'NixNAR';
|
||||
@@ -336,19 +350,21 @@ sub contents : Chained('buildChain') PathPart Args(1) {
|
||||
|
||||
notFound($c, "Product $path has disappeared.") unless -e $path;
|
||||
|
||||
# Sanitize $path to prevent shell injection attacks.
|
||||
$path =~ /^\/[\/[A-Za-z0-9_\-\.=+:]+$/ or die "Filename contains illegal characters.\n";
|
||||
|
||||
# FIXME: don't use shell invocations below.
|
||||
|
||||
# FIXME: use nix store cat
|
||||
|
||||
my $res;
|
||||
|
||||
if ($product->type eq "nix-build" && -d $path) {
|
||||
# FIXME: use nix ls-store -R --json
|
||||
$res = `cd '$path' && find . -print0 | xargs -0 ls -ld --`;
|
||||
error($c, "`ls -lR' error: $?") if $? != 0;
|
||||
# We need to use a pipe between find and xargs, so we'll use IPC::Run
|
||||
my $error;
|
||||
# Run find with absolute path and post-process to get relative paths
|
||||
my $success = run(['find', $path, '-print0'], '|', ['xargs', '-0', 'ls', '-ld', '--'], \$res, \$error);
|
||||
error($c, "`find $path -print0 | xargs -0 ls -ld --' error: $error") unless $success;
|
||||
|
||||
# Strip the base path to show relative paths
|
||||
my $escaped_path = quotemeta($path);
|
||||
$res =~ s/^(.*\s)$escaped_path(\/|$)/$1.$2/mg;
|
||||
|
||||
#my $baseuri = $c->uri_for('/build', $c->stash->{build}->id, 'download', $product->productnr);
|
||||
#$baseuri .= "/".$product->name if $product->name;
|
||||
@@ -356,34 +372,59 @@ sub contents : Chained('buildChain') PathPart Args(1) {
|
||||
}
|
||||
|
||||
elsif ($path =~ /\.rpm$/) {
|
||||
$res = `rpm --query --info --package '$path'`;
|
||||
error($c, "RPM error: $?") if $? != 0;
|
||||
my ($stdout1, $stderr1);
|
||||
run3(['rpm', '--query', '--info', '--package', $path], \undef, \$stdout1, \$stderr1);
|
||||
error($c, "RPM error: $stderr1") if $? != 0;
|
||||
$res = $stdout1;
|
||||
|
||||
$res .= "===\n";
|
||||
$res .= `rpm --query --list --verbose --package '$path'`;
|
||||
error($c, "RPM error: $?") if $? != 0;
|
||||
|
||||
my ($stdout2, $stderr2);
|
||||
run3(['rpm', '--query', '--list', '--verbose', '--package', $path], \undef, \$stdout2, \$stderr2);
|
||||
error($c, "RPM error: $stderr2") if $? != 0;
|
||||
$res .= $stdout2;
|
||||
}
|
||||
|
||||
elsif ($path =~ /\.deb$/) {
|
||||
$res = `dpkg-deb --info '$path'`;
|
||||
error($c, "`dpkg-deb' error: $?") if $? != 0;
|
||||
my ($stdout1, $stderr1);
|
||||
run3(['dpkg-deb', '--info', $path], \undef, \$stdout1, \$stderr1);
|
||||
error($c, "`dpkg-deb' error: $stderr1") if $? != 0;
|
||||
$res = $stdout1;
|
||||
|
||||
$res .= "===\n";
|
||||
$res .= `dpkg-deb --contents '$path'`;
|
||||
error($c, "`dpkg-deb' error: $?") if $? != 0;
|
||||
|
||||
my ($stdout2, $stderr2);
|
||||
run3(['dpkg-deb', '--contents', $path], \undef, \$stdout2, \$stderr2);
|
||||
error($c, "`dpkg-deb' error: $stderr2") if $? != 0;
|
||||
$res .= $stdout2;
|
||||
}
|
||||
|
||||
elsif ($path =~ /\.(tar(\.gz|\.bz2|\.xz|\.lzma)?|tgz)$/ ) {
|
||||
$res = `tar tvfa '$path'`;
|
||||
error($c, "`tar' error: $?") if $? != 0;
|
||||
my ($stdout, $stderr);
|
||||
run3(['tar', 'tvfa', $path], \undef, \$stdout, \$stderr);
|
||||
error($c, "`tar' error: $stderr") if $? != 0;
|
||||
$res = $stdout;
|
||||
}
|
||||
|
||||
elsif ($path =~ /\.(zip|jar)$/ ) {
|
||||
$res = `unzip -v '$path'`;
|
||||
error($c, "`unzip' error: $?") if $? != 0;
|
||||
my ($stdout, $stderr);
|
||||
run3(['unzip', '-v', $path], \undef, \$stdout, \$stderr);
|
||||
error($c, "`unzip' error: $stderr") if $? != 0;
|
||||
$res = $stdout;
|
||||
}
|
||||
|
||||
elsif ($path =~ /\.iso$/ ) {
|
||||
$res = `isoinfo -d -i '$path' && isoinfo -l -R -i '$path'`;
|
||||
error($c, "`isoinfo' error: $?") if $? != 0;
|
||||
# Run first isoinfo command
|
||||
my ($stdout1, $stderr1);
|
||||
run3(['isoinfo', '-d', '-i', $path], \undef, \$stdout1, \$stderr1);
|
||||
error($c, "`isoinfo' error: $stderr1") if $? != 0;
|
||||
$res = $stdout1;
|
||||
|
||||
# Run second isoinfo command
|
||||
my ($stdout2, $stderr2);
|
||||
run3(['isoinfo', '-l', '-R', '-i', $path], \undef, \$stdout2, \$stderr2);
|
||||
error($c, "`isoinfo' error: $stderr2") if $? != 0;
|
||||
$res .= $stdout2;
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -415,7 +456,7 @@ sub getDependencyGraph {
|
||||
};
|
||||
$$done{$path} = $node;
|
||||
my @refs;
|
||||
foreach my $ref (queryReferences($path)) {
|
||||
foreach my $ref ($MACHINE_LOCAL_STORE->queryReferences($path)) {
|
||||
next if $ref eq $path;
|
||||
next unless $runtime || $ref =~ /\.drv$/;
|
||||
getDependencyGraph($self, $c, $runtime, $done, $ref);
|
||||
@@ -423,7 +464,7 @@ sub getDependencyGraph {
|
||||
}
|
||||
# Show in reverse topological order to flatten the graph.
|
||||
# Should probably do a proper BFS.
|
||||
my @sorted = reverse topoSortPaths(@refs);
|
||||
my @sorted = reverse $MACHINE_LOCAL_STORE->topoSortPaths(@refs);
|
||||
$node->{refs} = [map { $$done{$_} } @sorted];
|
||||
}
|
||||
|
||||
@@ -436,7 +477,7 @@ sub build_deps : Chained('buildChain') PathPart('build-deps') {
|
||||
my $build = $c->stash->{build};
|
||||
my $drvPath = $build->drvpath;
|
||||
|
||||
error($c, "Derivation no longer available.") unless isValidPath $drvPath;
|
||||
error($c, "Derivation no longer available.") unless $MACHINE_LOCAL_STORE->isValidPath($drvPath);
|
||||
|
||||
$c->stash->{buildTimeGraph} = getDependencyGraph($self, $c, 0, {}, $drvPath);
|
||||
|
||||
@@ -451,7 +492,7 @@ sub runtime_deps : Chained('buildChain') PathPart('runtime-deps') {
|
||||
|
||||
requireLocalStore($c);
|
||||
|
||||
error($c, "Build outputs no longer available.") unless all { isValidPath($_) } @outPaths;
|
||||
error($c, "Build outputs no longer available.") unless all { $MACHINE_LOCAL_STORE->isValidPath($_) } @outPaths;
|
||||
|
||||
my $done = {};
|
||||
$c->stash->{runtimeGraph} = [ map { getDependencyGraph($self, $c, 1, $done, $_) } @outPaths ];
|
||||
@@ -471,7 +512,7 @@ sub nix : Chained('buildChain') PathPart('nix') CaptureArgs(0) {
|
||||
if (isLocalStore) {
|
||||
foreach my $out ($build->buildoutputs) {
|
||||
notFound($c, "Path " . $out->path . " is no longer available.")
|
||||
unless isValidPath($out->path);
|
||||
unless $MACHINE_LOCAL_STORE->isValidPath($out->path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -364,6 +364,21 @@ sub evals_GET {
|
||||
);
|
||||
}
|
||||
|
||||
sub errors :Chained('jobsetChain') :PathPart('errors') :Args(0) :ActionClass('REST') { }
|
||||
|
||||
sub errors_GET {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
$c->stash->{template} = 'eval-error.tt';
|
||||
|
||||
my $jobsetName = $c->stash->{params}->{name};
|
||||
$c->stash->{jobset} = $c->stash->{project}->jobsets->find(
|
||||
{ name => $jobsetName },
|
||||
{ '+columns' => { 'errormsg' => 'errormsg' } }
|
||||
);
|
||||
|
||||
$self->status_ok($c, entity => $c->stash->{jobset});
|
||||
}
|
||||
|
||||
# Redirect to the latest finished evaluation of this jobset.
|
||||
sub latest_eval : Chained('jobsetChain') PathPart('latest-eval') {
|
||||
|
||||
@@ -76,7 +76,9 @@ sub view_GET {
|
||||
$c->stash->{removed} = $diff->{removed};
|
||||
$c->stash->{unfinished} = $diff->{unfinished};
|
||||
$c->stash->{aborted} = $diff->{aborted};
|
||||
$c->stash->{failed} = $diff->{failed};
|
||||
$c->stash->{totalAborted} = $diff->{totalAborted};
|
||||
$c->stash->{totalFailed} = $diff->{totalFailed};
|
||||
$c->stash->{totalQueued} = $diff->{totalQueued};
|
||||
|
||||
$c->stash->{full} = ($c->req->params->{full} || "0") eq "1";
|
||||
|
||||
@@ -86,6 +88,17 @@ sub view_GET {
|
||||
);
|
||||
}
|
||||
|
||||
sub errors :Chained('evalChain') :PathPart('errors') :Args(0) :ActionClass('REST') { }
|
||||
|
||||
sub errors_GET {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
$c->stash->{template} = 'eval-error.tt';
|
||||
|
||||
$c->stash->{eval} = $c->model('DB::JobsetEvals')->find($c->stash->{eval}->id, { prefetch => 'evaluationerror' });
|
||||
|
||||
$self->status_ok($c, entity => $c->stash->{eval});
|
||||
}
|
||||
|
||||
sub create_jobset : Chained('evalChain') PathPart('create-jobset') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
@@ -9,15 +9,21 @@ use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::View::TT;
|
||||
use Nix::Store;
|
||||
use Nix::Config;
|
||||
use Number::Bytes::Human qw(format_bytes);
|
||||
use Encode;
|
||||
use File::Basename;
|
||||
use JSON::MaybeXS;
|
||||
use HTML::Entities;
|
||||
use IPC::Run3;
|
||||
use List::Util qw[min max];
|
||||
use List::SomeUtils qw{any};
|
||||
use Net::Prometheus;
|
||||
use Types::Standard qw/StrMatch/;
|
||||
use WWW::Form::UrlEncoded::PP qw();
|
||||
|
||||
use constant NARINFO_REGEX => qr{^([a-z0-9]{32})\.narinfo$};
|
||||
# e.g.: https://hydra.example.com/realisations/sha256:a62128132508a3a32eef651d6467695944763602f226ac630543e947d9feb140!out.doi
|
||||
use constant REALISATIONS_REGEX => qr{^(sha256:[a-z0-9]{64}![a-z]+)\.doi$};
|
||||
|
||||
# Put this controller at top-level.
|
||||
__PACKAGE__->config->{namespace} = '';
|
||||
@@ -32,6 +38,7 @@ sub noLoginNeeded {
|
||||
|
||||
return $whitelisted ||
|
||||
$c->request->path eq "api/push-github" ||
|
||||
$c->request->path eq "api/push-gitea" ||
|
||||
$c->request->path eq "google-login" ||
|
||||
$c->request->path eq "github-redirect" ||
|
||||
$c->request->path eq "github-login" ||
|
||||
@@ -47,11 +54,13 @@ sub begin :Private {
|
||||
$c->stash->{curUri} = $c->request->uri;
|
||||
$c->stash->{version} = $ENV{"HYDRA_RELEASE"} || "<devel>";
|
||||
$c->stash->{nixVersion} = $ENV{"NIX_RELEASE"} || "<devel>";
|
||||
$c->stash->{nixEvalJobsVersion} = $ENV{"NIX_EVAL_JOBS_RELEASE"} || "<devel>";
|
||||
$c->stash->{curTime} = time;
|
||||
$c->stash->{logo} = defined $c->config->{hydra_logo} ? "/logo" : "";
|
||||
$c->stash->{tracker} = defined $c->config->{tracker} ? $c->config->{tracker} : "";
|
||||
$c->stash->{flashMsg} = $c->flash->{flashMsg};
|
||||
$c->stash->{successMsg} = $c->flash->{successMsg};
|
||||
$c->stash->{localStore} = isLocalStore;
|
||||
|
||||
$c->stash->{isPrivateHydra} = $c->config->{private} // "0" ne "0";
|
||||
|
||||
@@ -77,7 +86,7 @@ sub begin :Private {
|
||||
$_->supportedInputTypes($c->stash->{inputTypes}) foreach @{$c->hydra_plugins};
|
||||
|
||||
# XSRF protection: require POST requests to have the same origin.
|
||||
if ($c->req->method eq "POST" && $c->req->path ne "api/push-github") {
|
||||
if ($c->req->method eq "POST" && $c->req->path ne "api/push-github" && $c->req->path ne "api/push-gitea") {
|
||||
my $referer = $c->req->header('Referer');
|
||||
$referer //= $c->req->header('Origin');
|
||||
my $base = $c->req->base;
|
||||
@@ -157,7 +166,7 @@ sub status_GET {
|
||||
{ "buildsteps.busy" => { '!=', 0 } },
|
||||
{ order_by => ["globalpriority DESC", "id"],
|
||||
join => "buildsteps",
|
||||
columns => [@buildListColumns]
|
||||
columns => [@buildListColumns, 'buildsteps.drvpath', 'buildsteps.type']
|
||||
})]
|
||||
);
|
||||
}
|
||||
@@ -169,8 +178,14 @@ sub queue_runner_status_GET {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
#my $status = from_json($c->model('DB::SystemStatus')->find('queue-runner')->status);
|
||||
my $status = decode_json(`hydra-queue-runner --status`);
|
||||
if ($?) { $status->{status} = "unknown"; }
|
||||
my ($stdout, $stderr);
|
||||
run3(['hydra-queue-runner', '--status'], \undef, \$stdout, \$stderr);
|
||||
my $status;
|
||||
if ($? != 0) {
|
||||
$status = { status => "unknown" };
|
||||
} else {
|
||||
$status = decode_json($stdout);
|
||||
}
|
||||
my $json = JSON->new->pretty()->canonical();
|
||||
|
||||
$c->stash->{template} = 'queue-runner-status.tt';
|
||||
@@ -183,8 +198,10 @@ sub machines :Local Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
my $machines = getMachines;
|
||||
|
||||
# Add entry for localhost.
|
||||
$machines->{''} //= {};
|
||||
# Add entry for localhost. The implicit addition is not needed with queue runner v2
|
||||
if (not $c->config->{'queue_runner_endpoint'}) {
|
||||
$machines->{''} //= {};
|
||||
}
|
||||
delete $machines->{'localhost'};
|
||||
|
||||
my $status = $c->model('DB::SystemStatus')->find("queue-runner");
|
||||
@@ -192,9 +209,11 @@ sub machines :Local Args(0) {
|
||||
my $ms = decode_json($status->status)->{"machines"};
|
||||
foreach my $name (keys %{$ms}) {
|
||||
$name = "" if $name eq "localhost";
|
||||
$machines->{$name} //= {disabled => 1};
|
||||
$machines->{$name}->{nrStepsDone} = $ms->{$name}->{nrStepsDone};
|
||||
$machines->{$name}->{avgStepBuildTime} = $ms->{$name}->{avgStepBuildTime} // 0;
|
||||
my $outName = $name;
|
||||
$outName = "" if $name eq "ssh://localhost";
|
||||
$machines->{$outName} //= {disabled => 1};
|
||||
$machines->{$outName}->{nrStepsDone} = $ms->{$name}->{nrStepsDone};
|
||||
$machines->{$outName}->{avgStepBuildTime} = $ms->{$name}->{avgStepBuildTime} // 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +226,19 @@ sub machines :Local Args(0) {
|
||||
"where busy != 0 order by machine, stepnr",
|
||||
{ Slice => {} });
|
||||
$c->stash->{template} = 'machine-status.tt';
|
||||
$c->stash->{human_bytes} = sub {
|
||||
my ($bytes) = @_;
|
||||
return format_bytes($bytes, si => 1);
|
||||
};
|
||||
$c->stash->{pretty_load} = sub {
|
||||
my ($load) = @_;
|
||||
return sprintf('%.2f', $load);
|
||||
};
|
||||
$c->stash->{pretty_percent} = sub {
|
||||
my ($percent) = @_;
|
||||
my $ret = sprintf('%.2f', $percent);
|
||||
return (' ' x (6 - length($ret))) . encode_entities($ret);
|
||||
};
|
||||
$self->status_ok($c, entity => $c->stash->{machines});
|
||||
}
|
||||
|
||||
@@ -326,7 +358,7 @@ sub nar :Local :Args(1) {
|
||||
else {
|
||||
$path = $Nix::Config::storeDir . "/$path";
|
||||
|
||||
gone($c, "Path " . $path . " is no longer available.") unless isValidPath($path);
|
||||
gone($c, "Path " . $path . " is no longer available.") unless $MACHINE_LOCAL_STORE->isValidPath($path);
|
||||
|
||||
$c->stash->{current_view} = 'NixNAR';
|
||||
$c->stash->{storePath} = $path;
|
||||
@@ -355,6 +387,33 @@ sub nix_cache_info :Path('nix-cache-info') :Args(0) {
|
||||
}
|
||||
|
||||
|
||||
sub realisations :Path('realisations') :Args(StrMatch[REALISATIONS_REGEX]) {
|
||||
my ($self, $c, $realisation) = @_;
|
||||
|
||||
if (!isLocalStore) {
|
||||
notFound($c, "There is no binary cache here.");
|
||||
}
|
||||
|
||||
else {
|
||||
my ($rawDrvOutput) = $realisation =~ REALISATIONS_REGEX;
|
||||
my $rawRealisation = $MACHINE_LOCAL_STORE->queryRawRealisation($rawDrvOutput);
|
||||
|
||||
if (!$rawRealisation) {
|
||||
$c->response->status(404);
|
||||
$c->response->content_type('text/plain');
|
||||
$c->stash->{plain}->{data} = "does not exist\n";
|
||||
$c->forward('Hydra::View::Plain');
|
||||
setCacheHeaders($c, 60 * 60);
|
||||
return;
|
||||
}
|
||||
|
||||
$c->response->content_type('text/plain');
|
||||
$c->stash->{plain}->{data} = $rawRealisation;
|
||||
$c->forward('Hydra::View::Plain');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub narinfo :Path :Args(StrMatch[NARINFO_REGEX]) {
|
||||
my ($self, $c, $narinfo) = @_;
|
||||
|
||||
@@ -366,7 +425,7 @@ sub narinfo :Path :Args(StrMatch[NARINFO_REGEX]) {
|
||||
my ($hash) = $narinfo =~ NARINFO_REGEX;
|
||||
|
||||
die("Hash length was not 32") if length($hash) != 32;
|
||||
my $path = queryPathFromHashPart($hash);
|
||||
my $path = $MACHINE_LOCAL_STORE->queryPathFromHashPart($hash);
|
||||
|
||||
if (!$path) {
|
||||
$c->response->status(404);
|
||||
@@ -524,7 +583,7 @@ sub log :Local :Args(1) {
|
||||
my $logPrefix = $c->config->{log_prefix};
|
||||
|
||||
if (defined $logPrefix) {
|
||||
$c->res->redirect($logPrefix . "log/" . basename($drvPath));
|
||||
$c->res->redirect($logPrefix . "log/" . WWW::Form::UrlEncoded::PP::url_encode(basename($drvPath)));
|
||||
} else {
|
||||
notFound($c, "The build log of $drvPath is not available.");
|
||||
}
|
||||
|
||||
@@ -463,7 +463,7 @@ sub my_jobs_tab :Chained('dashboard_base') :PathPart('my-jobs-tab') :Args(0) {
|
||||
, "jobset.enabled" => 1
|
||||
},
|
||||
{ order_by => ["project", "jobset", "job"]
|
||||
, join => ["project", "jobset"]
|
||||
, join => {"jobset" => "project"}
|
||||
})];
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,26 @@ sub buildDiff {
|
||||
removed => [],
|
||||
unfinished => [],
|
||||
aborted => [],
|
||||
failed => [],
|
||||
|
||||
# These summary counters cut across the categories to determine whether
|
||||
# actions such as "Restart all failed" or "Bump queue" are available.
|
||||
totalAborted => 0,
|
||||
totalFailed => 0,
|
||||
totalQueued => 0,
|
||||
};
|
||||
|
||||
my $n = 0;
|
||||
foreach my $build (@{$builds}) {
|
||||
my $aborted = $build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4);
|
||||
my $aborted = $build->finished != 0 && (
|
||||
# aborted
|
||||
$build->buildstatus == 3
|
||||
# cancelled
|
||||
|| $build->buildstatus == 4
|
||||
# timeout
|
||||
|| $build->buildstatus == 7
|
||||
# log limit exceeded
|
||||
|| $build->buildstatus == 10
|
||||
);
|
||||
my $d;
|
||||
my $found = 0;
|
||||
while ($n < scalar(@{$builds2})) {
|
||||
@@ -71,12 +85,19 @@ sub buildDiff {
|
||||
} else {
|
||||
push @{$ret->{new}}, $build if !$found;
|
||||
}
|
||||
if (defined $build->buildstatus && $build->buildstatus != 0) {
|
||||
push @{$ret->{failed}}, $build;
|
||||
|
||||
if ($build->finished != 0 && $build->buildstatus != 0) {
|
||||
if ($aborted) {
|
||||
++$ret->{totalAborted};
|
||||
} else {
|
||||
++$ret->{totalFailed};
|
||||
}
|
||||
} elsif ($build->finished == 0) {
|
||||
++$ret->{totalQueued};
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
1;
|
||||
1;
|
||||
|
||||
@@ -15,6 +15,7 @@ our @EXPORT = qw(
|
||||
forceLogin requireUser requireProjectOwner requireRestartPrivileges requireAdmin requirePost isAdmin isProjectOwner
|
||||
requireBumpPrivileges
|
||||
requireCancelBuildPrivileges
|
||||
requireEvalJobsetPrivileges
|
||||
trim
|
||||
getLatestFinishedEval getFirstEval
|
||||
paramToList
|
||||
@@ -186,6 +187,27 @@ sub isProjectOwner {
|
||||
defined $c->model('DB::ProjectMembers')->find({ project => $project, userName => $c->user->username }));
|
||||
}
|
||||
|
||||
sub hasEvalJobsetRole {
|
||||
my ($c) = @_;
|
||||
return $c->user_exists && $c->check_user_roles("eval-jobset");
|
||||
}
|
||||
|
||||
sub mayEvalJobset {
|
||||
my ($c, $project) = @_;
|
||||
return
|
||||
$c->user_exists &&
|
||||
(isAdmin($c) ||
|
||||
hasEvalJobsetRole($c) ||
|
||||
isProjectOwner($c, $project));
|
||||
}
|
||||
|
||||
sub requireEvalJobsetPrivileges {
|
||||
my ($c, $project) = @_;
|
||||
requireUser($c);
|
||||
accessDenied($c, "Only the project members, administrators, and accounts with eval-jobset privileges can perform this operation.")
|
||||
unless mayEvalJobset($c, $project);
|
||||
}
|
||||
|
||||
sub hasCancelBuildRole {
|
||||
my ($c) = @_;
|
||||
return $c->user_exists && $c->check_user_roles('cancel-build');
|
||||
@@ -272,7 +294,7 @@ sub requireAdmin {
|
||||
|
||||
sub requirePost {
|
||||
my ($c) = @_;
|
||||
error($c, "Request must be POSTed.") if $c->request->method ne "POST";
|
||||
error($c, "Request must be POSTed.", 405) if $c->request->method ne "POST";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,10 +12,14 @@ use Nix::Store;
|
||||
use Encode;
|
||||
use Sys::Hostname::Long;
|
||||
use IPC::Run;
|
||||
use IPC::Run3;
|
||||
use LWP::UserAgent;
|
||||
use JSON::MaybeXS;
|
||||
use UUID4::Tiny qw(is_uuid4_string);
|
||||
|
||||
our @ISA = qw(Exporter);
|
||||
our @EXPORT = qw(
|
||||
addToStore
|
||||
cancelBuilds
|
||||
constructRunCommandLogPath
|
||||
findLog
|
||||
@@ -36,12 +40,16 @@ our @EXPORT = qw(
|
||||
jobsetOverview
|
||||
jobsetOverview_
|
||||
pathIsInsidePrefix
|
||||
readIntoSocket
|
||||
readNixFile
|
||||
registerRoot
|
||||
restartBuilds
|
||||
run
|
||||
runCommand
|
||||
$MACHINE_LOCAL_STORE
|
||||
);
|
||||
|
||||
our $MACHINE_LOCAL_STORE = Nix::Store->new();
|
||||
|
||||
|
||||
sub getHydraHome {
|
||||
my $dir = $ENV{"HYDRA_HOME"} or die "The HYDRA_HOME directory does not exist!\n";
|
||||
@@ -171,6 +179,9 @@ sub getDrvLogPath {
|
||||
for ($fn . $bucketed, $fn . $bucketed . ".bz2") {
|
||||
return $_ if -f $_;
|
||||
}
|
||||
for ($fn . $bucketed, $fn . $bucketed . ".zst") {
|
||||
return $_ if -f $_;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
@@ -187,6 +198,10 @@ sub findLog {
|
||||
|
||||
return undef if scalar @outPaths == 0;
|
||||
|
||||
# Filter out any NULLs. Content-addressed derivations
|
||||
# that haven't built yet or failed to build may have a NULL outPath.
|
||||
@outPaths = grep {defined} @outPaths;
|
||||
|
||||
my @steps = $c->model('DB::BuildSteps')->search(
|
||||
{ path => { -in => [@outPaths] } },
|
||||
{ select => ["drvpath"]
|
||||
@@ -286,8 +301,7 @@ sub getEvals {
|
||||
|
||||
my @evals = $evals_result_set->search(
|
||||
{ hasnewbuilds => 1 },
|
||||
{ order_by => "$me.id DESC", rows => $rows, offset => $offset
|
||||
, prefetch => { evaluationerror => [ ] } });
|
||||
{ order_by => "$me.id DESC", rows => $rows, offset => $offset });
|
||||
my @res = ();
|
||||
my $cache = {};
|
||||
|
||||
@@ -330,37 +344,68 @@ sub getEvals {
|
||||
|
||||
sub getMachines {
|
||||
my %machines = ();
|
||||
my $config = getHydraConfig();
|
||||
|
||||
my @machinesFiles = split /:/, ($ENV{"NIX_REMOTE_SYSTEMS"} || "/etc/nix/machines");
|
||||
if ($config->{'queue_runner_endpoint'}) {
|
||||
my $ua = LWP::UserAgent->new();
|
||||
my $resp = $ua->get($config->{'queue_runner_endpoint'} . "/status/machines");
|
||||
if (not $resp->is_success) {
|
||||
print STDERR "Unable to ask queue runner for machines\n";
|
||||
return \%machines;
|
||||
}
|
||||
|
||||
for my $machinesFile (@machinesFiles) {
|
||||
next unless -e $machinesFile;
|
||||
open(my $conf, "<", $machinesFile) or die;
|
||||
while (my $line = <$conf>) {
|
||||
chomp($line);
|
||||
$line =~ s/\#.*$//g;
|
||||
next if $line =~ /^\s*$/;
|
||||
my @tokens = split /\s+/, $line;
|
||||
my $data = decode_json($resp->decoded_content) or return \%machines;
|
||||
my $machinesData = $data->{machines};
|
||||
|
||||
if (!defined($tokens[5]) || $tokens[5] eq "-") {
|
||||
$tokens[5] = "";
|
||||
}
|
||||
my @supportedFeatures = split(/,/, $tokens[5] || "");
|
||||
|
||||
if (!defined($tokens[6]) || $tokens[6] eq "-") {
|
||||
$tokens[6] = "";
|
||||
}
|
||||
my @mandatoryFeatures = split(/,/, $tokens[6] || "");
|
||||
$machines{$tokens[0]} =
|
||||
{ systemTypes => [ split(/,/, $tokens[1]) ]
|
||||
, sshKeys => $tokens[2]
|
||||
, maxJobs => int($tokens[3])
|
||||
, speedFactor => 1.0 * (defined $tokens[4] ? int($tokens[4]) : 1)
|
||||
, supportedFeatures => [ @supportedFeatures, @mandatoryFeatures ]
|
||||
, mandatoryFeatures => [ @mandatoryFeatures ]
|
||||
foreach my $machineName (keys %$machinesData) {
|
||||
my $machine = %$machinesData{$machineName};
|
||||
$machines{$machineName} =
|
||||
{ systemTypes => $machine->{systems}
|
||||
, maxJobs => $machine->{maxJobs}
|
||||
, speedFactor => $machine->{speedFactor}
|
||||
, supportedFeatures => [ @{$machine->{supportedFeatures}}, @{$machine->{mandatoryFeatures}} ]
|
||||
, mandatoryFeatures => [ @{$machine->{mandatoryFeatures}} ]
|
||||
# New fields for the machine status
|
||||
, primarySystemType => $machine->{systems}[0]
|
||||
, hasCapacity => $machine->{hasCapacity}
|
||||
, hasDynamicCapacity => $machine->{hasDynamicCapacity}
|
||||
, hasStaticCapacity => $machine->{hasStaticCapacity}
|
||||
, score => $machine->{score}
|
||||
, stats => $machine->{stats}
|
||||
, memTotal => $machine->{totalMem}
|
||||
};
|
||||
}
|
||||
close $conf;
|
||||
} else {
|
||||
my @machinesFiles = split /:/, ($ENV{"NIX_REMOTE_SYSTEMS"} || "/etc/nix/machines");
|
||||
|
||||
for my $machinesFile (@machinesFiles) {
|
||||
next unless -e $machinesFile;
|
||||
open(my $conf, "<", $machinesFile) or die;
|
||||
while (my $line = <$conf>) {
|
||||
chomp($line);
|
||||
$line =~ s/\#.*$//g;
|
||||
next if $line =~ /^\s*$/;
|
||||
my @tokens = split /\s+/, $line;
|
||||
|
||||
if (!defined($tokens[5]) || $tokens[5] eq "-") {
|
||||
$tokens[5] = "";
|
||||
}
|
||||
my @supportedFeatures = split(/,/, $tokens[5] || "");
|
||||
|
||||
if (!defined($tokens[6]) || $tokens[6] eq "-") {
|
||||
$tokens[6] = "";
|
||||
}
|
||||
my @mandatoryFeatures = split(/,/, $tokens[6] || "");
|
||||
$machines{$tokens[0]} =
|
||||
{ systemTypes => [ split(/,/, $tokens[1]) ]
|
||||
, maxJobs => int($tokens[3])
|
||||
, speedFactor => 1.0 * (defined $tokens[4] ? int($tokens[4]) : 1)
|
||||
, supportedFeatures => [ @supportedFeatures, @mandatoryFeatures ]
|
||||
, mandatoryFeatures => [ @mandatoryFeatures ]
|
||||
};
|
||||
}
|
||||
close $conf;
|
||||
}
|
||||
}
|
||||
|
||||
return \%machines;
|
||||
@@ -407,11 +452,21 @@ sub pathIsInsidePrefix {
|
||||
return $cur;
|
||||
}
|
||||
|
||||
sub readIntoSocket{
|
||||
my (%args) = @_;
|
||||
my $sock;
|
||||
|
||||
eval {
|
||||
open($sock, "-|", @{$args{cmd}}) or die q(failed to open socket from command:\n $x);
|
||||
};
|
||||
|
||||
return $sock;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
sub run {
|
||||
sub runCommand {
|
||||
my (%args) = @_;
|
||||
my $res = { stdout => "", stderr => "" };
|
||||
my $stdin = "";
|
||||
@@ -451,7 +506,7 @@ sub run {
|
||||
|
||||
sub grab {
|
||||
my (%args) = @_;
|
||||
my $res = run(%args, grabStderr => 0);
|
||||
my $res = runCommand(%args, grabStderr => 0);
|
||||
if ($res->{status}) {
|
||||
my $msgloc = "(in an indeterminate location)";
|
||||
if (defined $args{dir}) {
|
||||
@@ -494,7 +549,7 @@ sub restartBuilds {
|
||||
$builds = $builds->search({ finished => 1 });
|
||||
|
||||
foreach my $build ($builds->search({}, { columns => ["drvpath"] })) {
|
||||
next if !isValidPath($build->drvpath);
|
||||
next if !$MACHINE_LOCAL_STORE->isValidPath($build->drvpath);
|
||||
registerRoot $build->drvpath;
|
||||
}
|
||||
|
||||
@@ -561,4 +616,14 @@ sub constructRunCommandLogPath {
|
||||
return "$hydra_path/runcommand-logs/$bucket/$uuid";
|
||||
}
|
||||
|
||||
|
||||
sub addToStore {
|
||||
my ($path) = @_;
|
||||
|
||||
my ($stdout, $stderr);
|
||||
run3(['nix-store', '--add', $path], \undef, \$stdout, \$stderr);
|
||||
die "cannot add path $path to the Nix store: $stderr\n" if $? != 0;
|
||||
return trim($stdout);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
@@ -7,7 +7,6 @@ use Digest::SHA qw(sha256_hex);
|
||||
use File::Path;
|
||||
use Hydra::Helper::Exec;
|
||||
use Hydra::Helper::Nix;
|
||||
use Nix::Store;
|
||||
|
||||
sub supportedInputTypes {
|
||||
my ($self, $inputTypes) = @_;
|
||||
@@ -38,9 +37,9 @@ sub fetchInput {
|
||||
(my $cachedInput) = $self->{db}->resultset('CachedBazaarInputs')->search(
|
||||
{uri => $uri, revision => $revision});
|
||||
|
||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
|
||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
||||
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||
$storePath = $cachedInput->storepath;
|
||||
$sha256 = $cachedInput->sha256hash;
|
||||
} else {
|
||||
@@ -58,7 +57,7 @@ sub fetchInput {
|
||||
($sha256, $storePath) = split ' ', $stdout;
|
||||
|
||||
# FIXME: time window between nix-prefetch-bzr and addTempRoot.
|
||||
addTempRoot($storePath);
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($storePath);
|
||||
|
||||
$self->{db}->txn_do(sub {
|
||||
$self->{db}->resultset('CachedBazaarInputs')->create(
|
||||
|
||||
@@ -7,6 +7,7 @@ use HTTP::Request;
|
||||
use LWP::UserAgent;
|
||||
use JSON::MaybeXS;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::Helper::Nix;
|
||||
use File::Temp;
|
||||
use POSIX qw(strftime);
|
||||
|
||||
@@ -43,14 +44,11 @@ sub fetchInput {
|
||||
my $ua = LWP::UserAgent->new();
|
||||
_iterate("https://api.bitbucket.com/2.0/repositories/$owner/$repo/pullrequests?state=OPEN", $auth, \%pulls, $ua);
|
||||
my $tempdir = File::Temp->newdir("bitbucket-pulls" . "XXXXX", TMPDIR => 1);
|
||||
my $filename = "$tempdir/bitbucket-pulls.json";
|
||||
my $filename = "$tempdir/bitbucket-pulls-sorted.json";
|
||||
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
||||
print $fh encode_json \%pulls;
|
||||
print $fh JSON::MaybeXS->new(canonical => 1, pretty => 1)->encode(\%pulls);
|
||||
close $fh;
|
||||
system("jq -S . < $filename > $tempdir/bitbucket-pulls-sorted.json");
|
||||
my $storePath = trim(`nix-store --add "$tempdir/bitbucket-pulls-sorted.json"`
|
||||
or die "cannot copy path $filename to the Nix store.\n");
|
||||
chomp $storePath;
|
||||
my $storePath = addToStore($filename);
|
||||
my $timestamp = time;
|
||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||
}
|
||||
|
||||
@@ -9,11 +9,24 @@ use Hydra::Helper::CatalystUtils;
|
||||
sub stepFinished {
|
||||
my ($self, $step, $logPath) = @_;
|
||||
|
||||
my $doCompress = $self->{config}->{'compress_build_logs'} // "1";
|
||||
my $doCompress = $self->{config}->{'compress_build_logs'} // '1';
|
||||
my $silent = $self->{config}->{'compress_build_logs_silent'} // '0';
|
||||
my $compression = $self->{config}->{'compress_build_logs_compression'} // 'bzip2';
|
||||
|
||||
if ($doCompress eq "1" && -e $logPath) {
|
||||
print STDERR "compressing ‘$logPath’...\n";
|
||||
system("bzip2", "--force", $logPath);
|
||||
if (not -e $logPath or $doCompress ne "1") {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($silent ne '1') {
|
||||
print STDERR "compressing '$logPath' with $compression...\n";
|
||||
}
|
||||
|
||||
if ($compression eq 'bzip2') {
|
||||
system('bzip2', '--force', $logPath);
|
||||
} elsif ($compression eq 'zstd') {
|
||||
system('zstd', '--rm', '--quiet', '-T0', $logPath);
|
||||
} else {
|
||||
print STDERR "unknown compression type '$compression'\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use Digest::SHA qw(sha256_hex);
|
||||
use File::Path;
|
||||
use Hydra::Helper::Exec;
|
||||
use Hydra::Helper::Nix;
|
||||
use Nix::Store;
|
||||
use IPC::Run3;
|
||||
|
||||
sub supportedInputTypes {
|
||||
my ($self, $inputTypes) = @_;
|
||||
@@ -32,7 +32,7 @@ sub fetchInput {
|
||||
my $stdout = ""; my $stderr = ""; my $res;
|
||||
if (! -d $clonePath) {
|
||||
# Clone the repository.
|
||||
$res = run(timeout => 600,
|
||||
$res = runCommand(timeout => 600,
|
||||
cmd => ["darcs", "get", "--lazy", $uri, $clonePath],
|
||||
dir => $ENV{"TMPDIR"});
|
||||
die "Error getting darcs repo at `$uri':\n$stderr" if $res->{status};
|
||||
@@ -58,7 +58,7 @@ sub fetchInput {
|
||||
{uri => $uri, revision => $revision},
|
||||
{rows => 1});
|
||||
|
||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
||||
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||
$storePath = $cachedInput->storepath;
|
||||
$sha256 = $cachedInput->sha256hash;
|
||||
$revision = $cachedInput->revision;
|
||||
@@ -71,12 +71,15 @@ sub fetchInput {
|
||||
(system "darcs", "get", "--lazy", $clonePath, "$tmpDir/export", "--quiet",
|
||||
"--to-match", "hash $revision") == 0
|
||||
or die "darcs export failed";
|
||||
$revCount = `darcs changes --count --repodir $tmpDir/export`; chomp $revCount;
|
||||
die "darcs changes --count failed" if $? != 0;
|
||||
my ($stdout, $stderr);
|
||||
run3(['darcs', 'changes', '--count', '--repodir', "$tmpDir/export"], \undef, \$stdout, \$stderr);
|
||||
die "darcs changes --count failed: $stderr\n" if $? != 0;
|
||||
$revCount = $stdout;
|
||||
chomp $revCount;
|
||||
|
||||
system "rm", "-rf", "$tmpDir/export/_darcs";
|
||||
$storePath = addToStore("$tmpDir/export", 1, "sha256");
|
||||
$sha256 = queryPathHash($storePath);
|
||||
$storePath = $MACHINE_LOCAL_STORE->addToStore("$tmpDir/export", 1, "sha256");
|
||||
$sha256 = $MACHINE_LOCAL_STORE->queryPathHash($storePath);
|
||||
$sha256 =~ s/sha256://;
|
||||
|
||||
$self->{db}->txn_do(sub {
|
||||
|
||||
@@ -137,8 +137,8 @@ sub fetchInput {
|
||||
my $res;
|
||||
if (! -d $clonePath) {
|
||||
# Clone everything and fetch the branch.
|
||||
$res = run(cmd => ["git", "init", $clonePath]);
|
||||
$res = run(cmd => ["git", "remote", "add", "origin", "--", $uri], dir => $clonePath) unless $res->{status};
|
||||
$res = runCommand(cmd => ["git", "init", $clonePath]);
|
||||
$res = runCommand(cmd => ["git", "remote", "add", "origin", "--", $uri], dir => $clonePath) unless $res->{status};
|
||||
die "error creating git repo in `$clonePath':\n$res->{stderr}" if $res->{status};
|
||||
}
|
||||
|
||||
@@ -146,9 +146,9 @@ sub fetchInput {
|
||||
# the remote branch for whatever the repository state is. This command mirrors
|
||||
# only one branch of the remote repository.
|
||||
my $localBranch = _isHash($branch) ? "_hydra_tmp" : $branch;
|
||||
$res = run(cmd => ["git", "fetch", "-fu", "origin", "+$branch:$localBranch"], dir => $clonePath,
|
||||
$res = runCommand(cmd => ["git", "fetch", "-fu", "origin", "+$branch:$localBranch"], dir => $clonePath,
|
||||
timeout => $cfg->{timeout});
|
||||
$res = run(cmd => ["git", "fetch", "-fu", "origin"], dir => $clonePath, timeout => $cfg->{timeout}) if $res->{status};
|
||||
$res = runCommand(cmd => ["git", "fetch", "-fu", "origin"], dir => $clonePath, timeout => $cfg->{timeout}) if $res->{status};
|
||||
die "error fetching latest change from git repo at `$uri':\n$res->{stderr}" if $res->{status};
|
||||
|
||||
# If deepClone is defined, then we look at the content of the repository
|
||||
@@ -156,16 +156,16 @@ sub fetchInput {
|
||||
if (defined $deepClone) {
|
||||
|
||||
# Is the target branch a topgit branch?
|
||||
$res = run(cmd => ["git", "ls-tree", "-r", "$branch", ".topgit"], dir => $clonePath);
|
||||
$res = runCommand(cmd => ["git", "ls-tree", "-r", "$branch", ".topgit"], dir => $clonePath);
|
||||
|
||||
if ($res->{stdout} ne "") {
|
||||
# Checkout the branch to look at its content.
|
||||
$res = run(cmd => ["git", "checkout", "--force", "$branch"], dir => $clonePath);
|
||||
$res = runCommand(cmd => ["git", "checkout", "--force", "$branch"], dir => $clonePath);
|
||||
die "error checking out Git branch '$branch' at `$uri':\n$res->{stderr}" if $res->{status};
|
||||
|
||||
# This is a TopGit branch. Fetch all the topic branches so
|
||||
# that builders can run "tg patch" and similar.
|
||||
$res = run(cmd => ["tg", "remote", "--populate", "origin"], dir => $clonePath, timeout => $cfg->{timeout});
|
||||
$res = runCommand(cmd => ["tg", "remote", "--populate", "origin"], dir => $clonePath, timeout => $cfg->{timeout});
|
||||
print STDERR "warning: `tg remote --populate origin' failed:\n$res->{stderr}" if $res->{status};
|
||||
}
|
||||
}
|
||||
@@ -186,9 +186,9 @@ sub fetchInput {
|
||||
{uri => $uri, branch => $branch, revision => $revision, isdeepclone => defined($deepClone) ? 1 : 0},
|
||||
{rows => 1});
|
||||
|
||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
|
||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
||||
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||
$storePath = $cachedInput->storepath;
|
||||
$sha256 = $cachedInput->sha256hash;
|
||||
$revision = $cachedInput->revision;
|
||||
@@ -217,7 +217,7 @@ sub fetchInput {
|
||||
($sha256, $storePath) = split ' ', grab(cmd => ["nix-prefetch-git", $clonePath, $revision], chomp => 1);
|
||||
|
||||
# FIXME: time window between nix-prefetch-git and addTempRoot.
|
||||
addTempRoot($storePath);
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($storePath);
|
||||
|
||||
$self->{db}->txn_do(sub {
|
||||
$self->{db}->resultset('CachedGitInputs')->update_or_create(
|
||||
|
||||
@@ -88,10 +88,6 @@ sub buildQueued {
|
||||
common(@_, [], 0);
|
||||
}
|
||||
|
||||
sub buildStarted {
|
||||
common(@_, [], 1);
|
||||
}
|
||||
|
||||
sub buildFinished {
|
||||
common(@_, 2);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use HTTP::Request;
|
||||
use LWP::UserAgent;
|
||||
use JSON::MaybeXS;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::Helper::Nix;
|
||||
use File::Temp;
|
||||
use POSIX qw(strftime);
|
||||
|
||||
@@ -58,9 +59,7 @@ sub fetchInput {
|
||||
print $fh JSON->new->utf8->canonical->encode(\%pulls);
|
||||
close $fh;
|
||||
|
||||
my $storePath = trim(`nix-store --add "$filename"`
|
||||
or die "cannot copy path $filename to the Nix store.\n");
|
||||
chomp $storePath;
|
||||
my $storePath = addToStore($filename);
|
||||
my $timestamp = time;
|
||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use HTTP::Request;
|
||||
use LWP::UserAgent;
|
||||
use JSON::MaybeXS;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::Helper::Nix;
|
||||
use File::Temp;
|
||||
use POSIX qw(strftime);
|
||||
|
||||
@@ -17,9 +18,8 @@ tags) from GitHub following a certain naming scheme
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This plugin reads the list of branches or tags using GitHub's REST API. The name
|
||||
of the reference must follow a particular prefix. This list is stored in the
|
||||
nix-store and used as an input to declarative jobsets.
|
||||
This plugin reads the list of branches or tags using GitHub's REST API. This
|
||||
list is stored in the nix-store and used as an input to declarative jobsets.
|
||||
|
||||
=head1 CONFIGURATION
|
||||
|
||||
@@ -33,7 +33,7 @@ The declarative project C<spec.json> file must contains an input such as
|
||||
|
||||
"pulls": {
|
||||
"type": "github_refs",
|
||||
"value": "[owner] [repo] heads|tags - [prefix]",
|
||||
"value": "[owner] [repo] [type] - [prefix]",
|
||||
"emailresponsible": false
|
||||
}
|
||||
|
||||
@@ -41,12 +41,11 @@ In the above snippet, C<[owner]> is the repository owner and C<[repo]> is the
|
||||
repository name. Also note a literal C<->, which is placed there for the future
|
||||
use.
|
||||
|
||||
C<heads|tags> denotes that one of these two is allowed, that is, the third
|
||||
position should hold either the C<heads> or the C<tags> keyword. In case of the former, the plugin
|
||||
will fetch all branches, while in case of the latter, it will fetch the tags.
|
||||
C<[type]> is the type of ref to list. Typical values are "heads", "tags", and
|
||||
"pull". "." will include all types.
|
||||
|
||||
C<prefix> denotes the prefix the reference name must start with, in order to be
|
||||
included.
|
||||
included. "." will include all references.
|
||||
|
||||
For example, C<"value": "nixos hydra heads - release/"> refers to
|
||||
L<https://github.com/nixos/hydra> repository, and will fetch all branches that
|
||||
@@ -101,8 +100,6 @@ sub fetchInput {
|
||||
return undef if $input_type ne "github_refs";
|
||||
|
||||
my ($owner, $repo, $type, $fut, $prefix) = split ' ', $value;
|
||||
die "type field is neither 'heads' nor 'tags', but '$type'"
|
||||
unless $type eq 'heads' or $type eq 'tags';
|
||||
|
||||
my $auth = $self->{config}->{github_authorization}->{$owner};
|
||||
my $githubEndpoint = $self->{config}->{github_endpoint} // "https://api.github.com";
|
||||
@@ -110,14 +107,11 @@ sub fetchInput {
|
||||
my $ua = LWP::UserAgent->new();
|
||||
_iterate("$githubEndpoint/repos/$owner/$repo/git/matching-refs/$type/$prefix?per_page=100", $auth, \%refs, $ua);
|
||||
my $tempdir = File::Temp->newdir("github-refs" . "XXXXX", TMPDIR => 1);
|
||||
my $filename = "$tempdir/github-refs.json";
|
||||
my $filename = "$tempdir/github-refs-sorted.json";
|
||||
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
||||
print $fh encode_json \%refs;
|
||||
print $fh JSON::MaybeXS->new(canonical => 1, pretty => 1)->encode(\%refs);
|
||||
close $fh;
|
||||
system("jq -S . < $filename > $tempdir/github-refs-sorted.json");
|
||||
my $storePath = trim(qx{nix-store --add "$tempdir/github-refs-sorted.json"}
|
||||
or die "cannot copy path $filename to the Nix store.\n");
|
||||
chomp $storePath;
|
||||
my $storePath = addToStore($filename);
|
||||
my $timestamp = time;
|
||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use HTTP::Request;
|
||||
use LWP::UserAgent;
|
||||
use JSON::MaybeXS;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::Helper::Nix;
|
||||
use File::Temp;
|
||||
use POSIX qw(strftime);
|
||||
|
||||
@@ -81,14 +82,11 @@ sub fetchInput {
|
||||
_iterate($url, $baseUrl, \%pulls, $ua, $target_repo_url);
|
||||
|
||||
my $tempdir = File::Temp->newdir("gitlab-pulls" . "XXXXX", TMPDIR => 1);
|
||||
my $filename = "$tempdir/gitlab-pulls.json";
|
||||
my $filename = "$tempdir/gitlab-pulls-sorted.json";
|
||||
open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
|
||||
print $fh encode_json \%pulls;
|
||||
print $fh JSON::MaybeXS->new(canonical => 1, pretty => 1, utf8 => 1)->encode(\%pulls);
|
||||
close $fh;
|
||||
system("jq -S . < $filename > $tempdir/gitlab-pulls-sorted.json");
|
||||
my $storePath = trim(`nix-store --add "$tempdir/gitlab-pulls-sorted.json"`
|
||||
or die "cannot copy path $filename to the Nix store.\n");
|
||||
chomp $storePath;
|
||||
my $storePath = addToStore($filename);
|
||||
my $timestamp = time;
|
||||
return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use Digest::SHA qw(sha256_hex);
|
||||
use File::Path;
|
||||
use Hydra::Helper::Nix;
|
||||
use Hydra::Helper::Exec;
|
||||
use Nix::Store;
|
||||
use Fcntl qw(:flock);
|
||||
|
||||
sub supportedInputTypes {
|
||||
@@ -68,9 +67,9 @@ sub fetchInput {
|
||||
(my $cachedInput) = $self->{db}->resultset('CachedHgInputs')->search(
|
||||
{uri => $uri, branch => $branch, revision => $revision});
|
||||
|
||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
|
||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
||||
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||
$storePath = $cachedInput->storepath;
|
||||
$sha256 = $cachedInput->sha256hash;
|
||||
} else {
|
||||
@@ -85,7 +84,7 @@ sub fetchInput {
|
||||
($sha256, $storePath) = split ' ', $stdout;
|
||||
|
||||
# FIXME: time window between nix-prefetch-hg and addTempRoot.
|
||||
addTempRoot($storePath);
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($storePath);
|
||||
|
||||
$self->{db}->txn_do(sub {
|
||||
$self->{db}->resultset('CachedHgInputs')->update_or_create(
|
||||
|
||||
@@ -5,7 +5,7 @@ use warnings;
|
||||
use parent 'Hydra::Plugin';
|
||||
use POSIX qw(strftime);
|
||||
use Hydra::Helper::Nix;
|
||||
use Nix::Store;
|
||||
use IPC::Run3;
|
||||
|
||||
sub supportedInputTypes {
|
||||
my ($self, $inputTypes) = @_;
|
||||
@@ -30,7 +30,7 @@ sub fetchInput {
|
||||
{srcpath => $uri, lastseen => {">", $timestamp - $timeout}},
|
||||
{rows => 1, order_by => "lastseen DESC"});
|
||||
|
||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
||||
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||
$storePath = $cachedInput->storepath;
|
||||
$sha256 = $cachedInput->sha256hash;
|
||||
$timestamp = $cachedInput->timestamp;
|
||||
@@ -38,15 +38,20 @@ sub fetchInput {
|
||||
|
||||
print STDERR "copying input ", $name, " from $uri\n";
|
||||
if ( $uri =~ /^\// ) {
|
||||
$storePath = `nix-store --add "$uri"`
|
||||
or die "cannot copy path $uri to the Nix store.\n";
|
||||
$storePath = addToStore($uri);
|
||||
} else {
|
||||
$storePath = `PRINT_PATH=1 nix-prefetch-url "$uri" | tail -n 1`
|
||||
or die "cannot fetch $uri to the Nix store.\n";
|
||||
# Run nix-prefetch-url with PRINT_PATH=1
|
||||
my ($stdout, $stderr);
|
||||
local $ENV{PRINT_PATH} = 1;
|
||||
run3(['nix-prefetch-url', $uri], \undef, \$stdout, \$stderr);
|
||||
die "cannot fetch $uri to the Nix store: $stderr\n" if $? != 0;
|
||||
# Get the last line (which is the store path)
|
||||
my @output_lines = split /\n/, $stdout;
|
||||
$storePath = $output_lines[-1] if @output_lines;
|
||||
}
|
||||
chomp $storePath;
|
||||
|
||||
$sha256 = (queryPathInfo($storePath, 0))[1] or die;
|
||||
$sha256 = ($MACHINE_LOCAL_STORE->queryPathInfo($storePath, 0))[1] or die;
|
||||
|
||||
($cachedInput) = $self->{db}->resultset('CachedPathInputs')->search(
|
||||
{srcpath => $uri, sha256hash => $sha256});
|
||||
|
||||
@@ -7,6 +7,8 @@ use File::Temp;
|
||||
use File::Basename;
|
||||
use Fcntl;
|
||||
use IO::File;
|
||||
use IPC::Run qw(run);
|
||||
use IPC::Run3;
|
||||
use Net::Amazon::S3;
|
||||
use Net::Amazon::S3::Client;
|
||||
use Digest::SHA;
|
||||
@@ -14,6 +16,7 @@ use Nix::Config;
|
||||
use Nix::Store;
|
||||
use Hydra::Model::DB;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::Helper::Nix;
|
||||
|
||||
sub isEnabled {
|
||||
my ($self) = @_;
|
||||
@@ -26,11 +29,11 @@ my %compressors = ();
|
||||
$compressors{"none"} = "";
|
||||
|
||||
if (defined($Nix::Config::bzip2)) {
|
||||
$compressors{"bzip2"} = "| $Nix::Config::bzip2",
|
||||
$compressors{"bzip2"} = "$Nix::Config::bzip2",
|
||||
}
|
||||
|
||||
if (defined($Nix::Config::xz)) {
|
||||
$compressors{"xz"} = "| $Nix::Config::xz",
|
||||
$compressors{"xz"} = "$Nix::Config::xz",
|
||||
}
|
||||
|
||||
my $lockfile = Hydra::Model::DB::getHydraPath . "/.hydra-s3backup.lock";
|
||||
@@ -92,7 +95,7 @@ sub buildFinished {
|
||||
my $hash = substr basename($path), 0, 32;
|
||||
my ($deriver, $narHash, $time, $narSize, $refs) = queryPathInfo($path, 0);
|
||||
my $system;
|
||||
if (defined $deriver and isValidPath($deriver)) {
|
||||
if (defined $deriver and $MACHINE_LOCAL_STORE->isValidPath($deriver)) {
|
||||
$system = derivationFromPath($deriver)->{platform};
|
||||
}
|
||||
foreach my $reference (@{$refs}) {
|
||||
@@ -110,7 +113,16 @@ sub buildFinished {
|
||||
}
|
||||
next unless @incomplete_buckets;
|
||||
my $compressor = $compressors{$compression_type};
|
||||
system("$Nix::Config::binDir/nix-store --dump $path $compressor > $tempdir/nar") == 0 or die;
|
||||
if ($compressor eq "") {
|
||||
# No compression - use IPC::Run3 to redirect stdout to file
|
||||
run3(["$Nix::Config::binDir/nix-store", "--dump", $path],
|
||||
\undef, "$tempdir/nar", \undef) or die "nix-store --dump failed: $!";
|
||||
} else {
|
||||
# With compression - use IPC::Run to pipe nix-store output to compressor
|
||||
my $dump_cmd = ["$Nix::Config::binDir/nix-store", "--dump", $path];
|
||||
my $compress_cmd = [$compressor];
|
||||
run($dump_cmd, '|', $compress_cmd, '>', "$tempdir/nar") or die "Pipeline failed: $?";
|
||||
}
|
||||
my $digest = Digest::SHA->new(256);
|
||||
$digest->addfile("$tempdir/nar");
|
||||
my $file_hash = $digest->hexdigest;
|
||||
|
||||
@@ -7,7 +7,6 @@ use Digest::SHA qw(sha256_hex);
|
||||
use Hydra::Helper::Exec;
|
||||
use Hydra::Helper::Nix;
|
||||
use IPC::Run;
|
||||
use Nix::Store;
|
||||
|
||||
sub supportedInputTypes {
|
||||
my ($self, $inputTypes) = @_;
|
||||
@@ -45,9 +44,9 @@ sub fetchInput {
|
||||
(my $cachedInput) = $self->{db}->resultset('CachedSubversionInputs')->search(
|
||||
{uri => $uri, revision => $revision});
|
||||
|
||||
addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
$MACHINE_LOCAL_STORE->addTempRoot($cachedInput->storepath) if defined $cachedInput;
|
||||
|
||||
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
|
||||
if (defined $cachedInput && $MACHINE_LOCAL_STORE->isValidPath($cachedInput->storepath)) {
|
||||
$storePath = $cachedInput->storepath;
|
||||
$sha256 = $cachedInput->sha256hash;
|
||||
} else {
|
||||
@@ -62,16 +61,16 @@ sub fetchInput {
|
||||
die "error checking out Subversion repo at `$uri':\n$stderr" if $res;
|
||||
|
||||
if ($type eq "svn-checkout") {
|
||||
$storePath = addToStore($wcPath, 1, "sha256");
|
||||
$storePath = $MACHINE_LOCAL_STORE->addToStore($wcPath, 1, "sha256");
|
||||
} else {
|
||||
# Hm, if the Nix Perl bindings supported filters in
|
||||
# addToStore(), then we wouldn't need to make a copy here.
|
||||
my $tmpDir = File::Temp->newdir("hydra-svn-export.XXXXXX", CLEANUP => 1, TMPDIR => 1) or die;
|
||||
(system "svn", "export", $wcPath, "$tmpDir/source", "--quiet") == 0 or die "svn export failed";
|
||||
$storePath = addToStore("$tmpDir/source", 1, "sha256");
|
||||
$storePath = $MACHINE_LOCAL_STORE->addToStore("$tmpDir/source", 1, "sha256");
|
||||
}
|
||||
|
||||
$sha256 = queryPathHash($storePath); $sha256 =~ s/sha256://;
|
||||
$sha256 = $MACHINE_LOCAL_STORE->queryPathHash($storePath); $sha256 =~ s/sha256://;
|
||||
|
||||
$self->{db}->txn_do(sub {
|
||||
$self->{db}->resultset('CachedSubversionInputs')->update_or_create(
|
||||
|
||||
@@ -49,7 +49,7 @@ __PACKAGE__->table("buildoutputs");
|
||||
=head2 path
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 0
|
||||
is_nullable: 1
|
||||
|
||||
=cut
|
||||
|
||||
@@ -59,7 +59,7 @@ __PACKAGE__->add_columns(
|
||||
"name",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"path",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
@@ -94,8 +94,8 @@ __PACKAGE__->belongs_to(
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-26 12:02:36
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gU+kZ6A0ISKpaXGRGve8mg
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-06-30 12:02:32
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Jsabm3YTcI7YvCuNdKP5Ng
|
||||
|
||||
my %hint = (
|
||||
columns => [
|
||||
|
||||
@@ -55,7 +55,7 @@ __PACKAGE__->table("buildstepoutputs");
|
||||
=head2 path
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 0
|
||||
is_nullable: 1
|
||||
|
||||
=cut
|
||||
|
||||
@@ -67,7 +67,7 @@ __PACKAGE__->add_columns(
|
||||
"name",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"path",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
@@ -119,8 +119,8 @@ __PACKAGE__->belongs_to(
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-26 12:02:36
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gxp8rOjpRVen4YbIjomHTw
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2022-06-30 12:02:32
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Bad70CRTt7zb2GGuRoQ++Q
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
|
||||
@@ -105,4 +105,6 @@ __PACKAGE__->add_column(
|
||||
"+id" => { retrieve_on_insert => 1 }
|
||||
);
|
||||
|
||||
__PACKAGE__->mk_group_accessors('column' => 'has_error');
|
||||
|
||||
1;
|
||||
|
||||
@@ -386,6 +386,8 @@ __PACKAGE__->add_column(
|
||||
"+id" => { retrieve_on_insert => 1 }
|
||||
);
|
||||
|
||||
__PACKAGE__->mk_group_accessors('column' => 'has_error');
|
||||
|
||||
sub supportsDynamicRunCommand {
|
||||
my ($self) = @_;
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ sub json_hint {
|
||||
|
||||
sub _authenticator() {
|
||||
my $authenticator = Crypt::Passphrase->new(
|
||||
encoder => 'Argon2',
|
||||
encoder => { module => 'Argon2', output_size => 16 },
|
||||
validators => [
|
||||
(sub {
|
||||
my ($password, $hash) = @_;
|
||||
|
||||
30
src/lib/Hydra/Schema/ResultSet/EvaluationErrors.pm
Normal file
30
src/lib/Hydra/Schema/ResultSet/EvaluationErrors.pm
Normal file
@@ -0,0 +1,30 @@
|
||||
package Hydra::Schema::ResultSet::EvaluationErrors;
|
||||
|
||||
use strict;
|
||||
use utf8;
|
||||
use warnings;
|
||||
|
||||
use parent 'DBIx::Class::ResultSet';
|
||||
|
||||
use Storable qw(dclone);
|
||||
|
||||
__PACKAGE__->load_components('Helper::ResultSet::RemoveColumns');
|
||||
|
||||
# Exclude expensive error message values unless explicitly requested, and
|
||||
# replace them with a summary field describing their presence/absence.
|
||||
sub search_rs {
|
||||
my ( $class, $query, $attrs ) = @_;
|
||||
|
||||
if ($attrs) {
|
||||
$attrs = dclone($attrs);
|
||||
}
|
||||
|
||||
unless (exists $attrs->{'select'} || exists $attrs->{'columns'}) {
|
||||
$attrs->{'+columns'}->{'has_error'} = "errormsg != ''";
|
||||
}
|
||||
unless (exists $attrs->{'+columns'}->{'errormsg'}) {
|
||||
push @{ $attrs->{'remove_columns'} }, 'errormsg';
|
||||
}
|
||||
|
||||
return $class->next::method($query, $attrs);
|
||||
}
|
||||
30
src/lib/Hydra/Schema/ResultSet/Jobsets.pm
Normal file
30
src/lib/Hydra/Schema/ResultSet/Jobsets.pm
Normal file
@@ -0,0 +1,30 @@
|
||||
package Hydra::Schema::ResultSet::Jobsets;
|
||||
|
||||
use strict;
|
||||
use utf8;
|
||||
use warnings;
|
||||
|
||||
use parent 'DBIx::Class::ResultSet';
|
||||
|
||||
use Storable qw(dclone);
|
||||
|
||||
__PACKAGE__->load_components('Helper::ResultSet::RemoveColumns');
|
||||
|
||||
# Exclude expensive error message values unless explicitly requested, and
|
||||
# replace them with a summary field describing their presence/absence.
|
||||
sub search_rs {
|
||||
my ( $class, $query, $attrs ) = @_;
|
||||
|
||||
if ($attrs) {
|
||||
$attrs = dclone($attrs);
|
||||
}
|
||||
|
||||
unless (exists $attrs->{'select'} || exists $attrs->{'columns'}) {
|
||||
$attrs->{'+columns'}->{'has_error'} = "errormsg != ''";
|
||||
}
|
||||
unless (exists $attrs->{'+columns'}->{'errormsg'}) {
|
||||
push @{ $attrs->{'remove_columns'} }, 'errormsg';
|
||||
}
|
||||
|
||||
return $class->next::method($query, $attrs);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use MIME::Base64;
|
||||
use Nix::Manifest;
|
||||
use Nix::Store;
|
||||
use Nix::Utils;
|
||||
use Hydra::Helper::Nix;
|
||||
use base qw/Catalyst::View/;
|
||||
|
||||
sub process {
|
||||
@@ -17,7 +18,7 @@ sub process {
|
||||
|
||||
$c->response->content_type('text/x-nix-narinfo'); # !!! check MIME type
|
||||
|
||||
my ($deriver, $narHash, $time, $narSize, $refs) = queryPathInfo($storePath, 1);
|
||||
my ($deriver, $narHash, $time, $narSize, $refs) = $MACHINE_LOCAL_STORE->queryPathInfo($storePath, 1);
|
||||
|
||||
my $info;
|
||||
$info .= "StorePath: $storePath\n";
|
||||
@@ -28,8 +29,8 @@ sub process {
|
||||
$info .= "References: " . join(" ", map { basename $_ } @{$refs}) . "\n";
|
||||
if (defined $deriver) {
|
||||
$info .= "Deriver: " . basename $deriver . "\n";
|
||||
if (isValidPath($deriver)) {
|
||||
my $drv = derivationFromPath($deriver);
|
||||
if ($MACHINE_LOCAL_STORE->isValidPath($deriver)) {
|
||||
my $drv = $MACHINE_LOCAL_STORE->derivationFromPath($deriver);
|
||||
$info .= "System: $drv->{platform}\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ sub process {
|
||||
|
||||
my $tail = int($c->stash->{tail} // "0");
|
||||
|
||||
if ($logPath =~ /\.bz2$/) {
|
||||
if ($logPath =~ /\.zst$/) {
|
||||
my $doTail = $tail ? "| tail -n '$tail'" : "";
|
||||
open($fh, "-|", "zstd -dc < '$logPath' $doTail") or die;
|
||||
} elsif ($logPath =~ /\.bz2$/) {
|
||||
my $doTail = $tail ? "| tail -n '$tail'" : "";
|
||||
open($fh, "-|", "bzip2 -dc < '$logPath' $doTail") or die;
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@ use base 'Catalyst::View::TT';
|
||||
use Template::Plugin::HTML;
|
||||
use Hydra::Helper::Nix;
|
||||
use Time::Seconds;
|
||||
use Digest::SHA qw(sha1_hex);
|
||||
|
||||
__PACKAGE__->config(
|
||||
TEMPLATE_EXTENSION => '.tt',
|
||||
@@ -25,8 +26,14 @@ __PACKAGE__->config(
|
||||
makeNameTextForJobset
|
||||
relativeDuration
|
||||
stripSSHUser
|
||||
metricDivId
|
||||
/]);
|
||||
|
||||
sub metricDivId {
|
||||
my ($self, $c, $text) = @_;
|
||||
return "metric-" . sha1_hex($text);
|
||||
}
|
||||
|
||||
sub buildLogExists {
|
||||
my ($self, $c, $build) = @_;
|
||||
return 1 if defined $c->config->{log_prefix};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
PERL_MODULES = \
|
||||
$(wildcard *.pm) \
|
||||
$(wildcard Hydra/*.pm) \
|
||||
$(wildcard Hydra/Helper/*.pm) \
|
||||
$(wildcard Hydra/Model/*.pm) \
|
||||
$(wildcard Hydra/View/*.pm) \
|
||||
$(wildcard Hydra/Schema/*.pm) \
|
||||
$(wildcard Hydra/Schema/Result/*.pm) \
|
||||
$(wildcard Hydra/Schema/ResultSet/*.pm) \
|
||||
$(wildcard Hydra/Controller/*.pm) \
|
||||
$(wildcard Hydra/Base/*.pm) \
|
||||
$(wildcard Hydra/Base/Controller/*.pm) \
|
||||
$(wildcard Hydra/Script/*.pm) \
|
||||
$(wildcard Hydra/Component/*.pm) \
|
||||
$(wildcard Hydra/Event/*.pm) \
|
||||
$(wildcard Hydra/Plugin/*.pm)
|
||||
|
||||
EXTRA_DIST = \
|
||||
$(PERL_MODULES)
|
||||
|
||||
hydradir = $(libexecdir)/hydra/lib
|
||||
nobase_hydra_DATA = $(PERL_MODULES)
|
||||
@@ -0,0 +1,103 @@
|
||||
package Perl::Critic::Policy::Hydra::ProhibitShellInvokingSystemCalls;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use constant;
|
||||
|
||||
use Perl::Critic::Utils qw{ :severities :classification :ppi };
|
||||
use base 'Perl::Critic::Policy';
|
||||
|
||||
our $VERSION = '1.000';
|
||||
|
||||
use constant DESC => q{Shell-invoking system calls are prohibited};
|
||||
use constant EXPL => q{Use list form system() or IPC::Run3 for better security. String form invokes shell and is vulnerable to injection};
|
||||
|
||||
sub supported_parameters { return () }
|
||||
sub default_severity { return $SEVERITY_HIGHEST }
|
||||
sub default_themes { return qw( hydra security ) }
|
||||
sub applies_to { return 'PPI::Token::Word' }
|
||||
|
||||
sub violates {
|
||||
my ( $self, $elem, undef ) = @_;
|
||||
|
||||
# Only check system() and exec() calls
|
||||
return () unless $elem->content() =~ /^(system|exec)$/;
|
||||
return () unless is_function_call($elem);
|
||||
|
||||
# Skip method calls (->system or ->exec)
|
||||
my $prev = $elem->sprevious_sibling();
|
||||
return () if $prev && $prev->isa('PPI::Token::Operator') && $prev->content() eq '->';
|
||||
|
||||
# Get first argument after function name, skipping whitespace
|
||||
my $args = $elem->snext_sibling();
|
||||
return () unless $args;
|
||||
$args = $args->snext_sibling() while $args && $args->isa('PPI::Token::Whitespace');
|
||||
|
||||
# For parenthesized calls, look inside
|
||||
my $search_elem = $args;
|
||||
if ($args && $args->isa('PPI::Structure::List')) {
|
||||
$search_elem = $args->schild(0);
|
||||
return () unless $search_elem;
|
||||
}
|
||||
|
||||
# Check if it's list form (has comma)
|
||||
my $current = $search_elem;
|
||||
if ($current && $current->isa('PPI::Statement')) {
|
||||
# Look through statement children
|
||||
for my $child ($current->schildren()) {
|
||||
return () if $child->isa('PPI::Token::Operator') && $child->content() eq ',';
|
||||
}
|
||||
} else {
|
||||
# Look through siblings for non-parenthesized calls
|
||||
while ($current) {
|
||||
return () if $current->isa('PPI::Token::Operator') && $current->content() eq ',';
|
||||
last if $current->isa('PPI::Token::Structure') && $current->content() eq ';';
|
||||
$current = $current->snext_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
# Check if first arg is array variable
|
||||
my $first = $search_elem->isa('PPI::Statement') ?
|
||||
$search_elem->schild(0) : $search_elem;
|
||||
return () if $first && $first->isa('PPI::Token::Symbol') && $first->content() =~ /^[@]/;
|
||||
|
||||
# Check if it's a safe single-word command
|
||||
if ($first && $first->isa('PPI::Token::Quote')) {
|
||||
my $content = $first->string();
|
||||
return () if $content =~ /^[a-zA-Z0-9_\-\.\/]+$/;
|
||||
}
|
||||
|
||||
return $self->violation( DESC, EXPL, $elem );
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=pod
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Perl::Critic::Policy::Hydra::ProhibitShellInvokingSystemCalls - Prohibit shell-invoking system() and exec() calls
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This policy prohibits the use of C<system()> and C<exec()> functions when called with a single string argument,
|
||||
which invokes the shell and is vulnerable to injection attacks.
|
||||
|
||||
The list form (e.g., C<system('ls', '-la')>) is allowed as it executes directly without shell interpretation.
|
||||
For better error handling and output capture, consider using C<IPC::Run3>.
|
||||
|
||||
=head1 CONFIGURATION
|
||||
|
||||
This Policy is not configurable except for the standard options.
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Hydra Development Team
|
||||
|
||||
=head1 COPYRIGHT
|
||||
|
||||
Copyright (c) 2025 Hydra Development Team. All rights reserved.
|
||||
|
||||
=cut
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
#include <pqxx/pqxx>
|
||||
|
||||
#include "util.hh"
|
||||
#include <nix/util/environment-variables.hh>
|
||||
#include <nix/util/util.hh>
|
||||
|
||||
|
||||
struct Connection : pqxx::connection
|
||||
@@ -26,19 +27,20 @@ struct Connection : pqxx::connection
|
||||
};
|
||||
|
||||
|
||||
class receiver : public pqxx::notification_receiver
|
||||
class receiver
|
||||
{
|
||||
std::optional<std::string> status;
|
||||
pqxx::connection & conn;
|
||||
|
||||
public:
|
||||
|
||||
receiver(pqxx::connection_base & c, const std::string & channel)
|
||||
: pqxx::notification_receiver(c, channel) { }
|
||||
|
||||
void operator() (const std::string & payload, int pid) override
|
||||
: conn(static_cast<pqxx::connection &>(c))
|
||||
{
|
||||
status = payload;
|
||||
};
|
||||
conn.listen(channel, [this](pqxx::notification n) {
|
||||
status = std::string(n.payload);
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<std::string> get() {
|
||||
auto s = status;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "util.hh"
|
||||
#include <nix/util/file-system.hh>
|
||||
#include <nix/util/util.hh>
|
||||
|
||||
struct HydraConfig
|
||||
{
|
||||
|
||||
5
src/libhydra/meson.build
Normal file
5
src/libhydra/meson.build
Normal file
@@ -0,0 +1,5 @@
|
||||
libhydra_inc = include_directories('.')
|
||||
|
||||
libhydra_dep = declare_dependency(
|
||||
include_directories: [libhydra_inc],
|
||||
)
|
||||
77
src/meson.build
Normal file
77
src/meson.build
Normal file
@@ -0,0 +1,77 @@
|
||||
# Native code
|
||||
subdir('libhydra')
|
||||
subdir('hydra-evaluator')
|
||||
subdir('hydra-queue-runner')
|
||||
|
||||
hydra_libexecdir = get_option('libexecdir') / 'hydra'
|
||||
|
||||
# Data and interpreted
|
||||
foreach dir : ['lib', 'root']
|
||||
install_subdir(dir,
|
||||
install_dir: hydra_libexecdir,
|
||||
)
|
||||
endforeach
|
||||
subdir('sql')
|
||||
subdir('ttf')
|
||||
|
||||
# Static files for website
|
||||
|
||||
hydra_libexecdir_static = hydra_libexecdir / 'root' / 'static'
|
||||
|
||||
## Bootstrap
|
||||
|
||||
bootstrap_name = 'bootstrap-4.3.1-dist'
|
||||
bootstrap = custom_target(
|
||||
'extract-bootstrap',
|
||||
input: 'root' / (bootstrap_name + '.zip'),
|
||||
output: bootstrap_name,
|
||||
command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
|
||||
)
|
||||
custom_target(
|
||||
'name-bootstrap',
|
||||
input: bootstrap,
|
||||
output: 'bootstrap',
|
||||
command: ['cp', '-r', '@INPUT@' , '@OUTPUT@'],
|
||||
install: true,
|
||||
install_dir: hydra_libexecdir_static,
|
||||
)
|
||||
|
||||
## Flot
|
||||
|
||||
custom_target(
|
||||
'extract-flot',
|
||||
input: 'root' / 'flot-0.8.3.zip',
|
||||
output: 'flot',
|
||||
command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
|
||||
install: true,
|
||||
install_dir: hydra_libexecdir_static / 'js',
|
||||
)
|
||||
|
||||
## Fontawesome
|
||||
|
||||
fontawesome_name = 'fontawesome-free-5.10.2-web'
|
||||
fontawesome = custom_target(
|
||||
'extract-fontawesome',
|
||||
input: 'root' / (fontawesome_name + '.zip'),
|
||||
output: fontawesome_name,
|
||||
command: ['unzip', '-u', '-d', '@OUTDIR@', '@INPUT@'],
|
||||
)
|
||||
custom_target(
|
||||
'name-fontawesome',
|
||||
input: fontawesome,
|
||||
output: 'fontawesome',
|
||||
command: ['cp', '-r', '@INPUT@' , '@OUTPUT@'],
|
||||
install: true,
|
||||
install_dir: hydra_libexecdir_static,
|
||||
)
|
||||
|
||||
# Scripts
|
||||
|
||||
install_subdir('script',
|
||||
install_dir: get_option('bindir'),
|
||||
exclude_files: [
|
||||
'hydra-dev-server',
|
||||
],
|
||||
install_mode: 'rwxr-xr-x',
|
||||
strip_directory: true,
|
||||
)
|
||||
@@ -1,39 +0,0 @@
|
||||
TEMPLATES = $(wildcard *.tt)
|
||||
STATIC = \
|
||||
$(wildcard static/images/*) \
|
||||
$(wildcard static/css/*) \
|
||||
static/js/bootbox.min.js \
|
||||
static/js/popper.min.js \
|
||||
static/js/common.js \
|
||||
static/js/jquery/jquery-3.4.1.min.js \
|
||||
static/js/jquery/jquery-ui-1.10.4.min.js
|
||||
|
||||
FLOT = flot-0.8.3.zip
|
||||
BOOTSTRAP = bootstrap-4.3.1-dist.zip
|
||||
FONTAWESOME = fontawesome-free-5.10.2-web.zip
|
||||
|
||||
ZIPS = $(FLOT) $(BOOTSTRAP) $(FONTAWESOME)
|
||||
|
||||
EXTRA_DIST = $(TEMPLATES) $(STATIC) $(ZIPS)
|
||||
|
||||
hydradir = $(libexecdir)/hydra/root
|
||||
nobase_hydra_DATA = $(EXTRA_DIST)
|
||||
|
||||
all:
|
||||
mkdir -p $(srcdir)/static/js
|
||||
unzip -u -d $(srcdir)/static $(BOOTSTRAP)
|
||||
rm -rf $(srcdir)/static/bootstrap
|
||||
mv $(srcdir)/static/$(basename $(BOOTSTRAP)) $(srcdir)/static/bootstrap
|
||||
unzip -u -d $(srcdir)/static/js $(FLOT)
|
||||
unzip -u -d $(srcdir)/static $(FONTAWESOME)
|
||||
rm -rf $(srcdir)/static/fontawesome
|
||||
mv $(srcdir)/static/$(basename $(FONTAWESOME)) $(srcdir)/static/fontawesome
|
||||
|
||||
install-data-local: $(ZIPS)
|
||||
mkdir -p $(hydradir)/static/js
|
||||
cp -prvd $(srcdir)/static/js/* $(hydradir)/static/js
|
||||
mkdir -p $(hydradir)/static/bootstrap
|
||||
cp -prvd $(srcdir)/static/bootstrap/* $(hydradir)/static/bootstrap
|
||||
mkdir -p $(hydradir)/static/fontawesome/{css,webfonts}
|
||||
cp -prvd $(srcdir)/static/fontawesome/css/* $(hydradir)/static/fontawesome/css
|
||||
cp -prvd $(srcdir)/static/fontawesome/webfonts/* $(hydradir)/static/fontawesome/webfonts
|
||||
@@ -11,7 +11,7 @@ titleHTML="Latest builds" _
|
||||
"") %]
|
||||
[% PROCESS common.tt %]
|
||||
|
||||
<p>Showing builds [% (page - 1) * resultsPerPage + 1 %] - [% (page - 1) * resultsPerPage + builds.size %] out of [% total %] in order of descending finish time.</p>
|
||||
<p>Showing builds [% (page - 1) * resultsPerPage + 1 %] - [% (page - 1) * resultsPerPage + builds.size %] out of [% HTML.escape(total) %] in order of descending finish time.</p>
|
||||
|
||||
[% INCLUDE renderBuildList hideProjectName=project hideJobsetName=jobset hideJobName=job %]
|
||||
[% INCLUDE renderPager %]
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form>
|
||||
<form id="signin-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="username" class="col-form-label">User name</label>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="do-signin" type="button" class="btn btn-primary">Sign in</button>
|
||||
<button type="submit" class="btn btn-primary">Sign in</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -57,10 +57,11 @@
|
||||
|
||||
function finishSignOut() { }
|
||||
|
||||
$("#do-signin").click(function() {
|
||||
$("#signin-form").submit(function(e) {
|
||||
e.preventDefault();
|
||||
requestJSON({
|
||||
url: "[% c.uri_for('/login') %]",
|
||||
data: $(this).parents("form").serialize(),
|
||||
data: $(this).serialize(),
|
||||
type: 'POST',
|
||||
success: function(data) {
|
||||
window.location.reload();
|
||||
@@ -82,7 +83,7 @@
|
||||
function onGoogleSignIn(googleUser) {
|
||||
requestJSON({
|
||||
url: "[% c.uri_for('/google-login') %]",
|
||||
data: "id_token=" + googleUser.getAuthResponse().id_token,
|
||||
data: "id_token=" + googleUser.credential,
|
||||
type: 'POST',
|
||||
success: function(data) {
|
||||
window.location.reload();
|
||||
@@ -91,9 +92,6 @@
|
||||
return false;
|
||||
};
|
||||
|
||||
$("#google-signin").click(function() {
|
||||
$(".g-signin2:first-child > div").click();
|
||||
});
|
||||
</script>
|
||||
[% END %]
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ END;
|
||||
seen.${step.drvpath} = 1;
|
||||
log = c.uri_for('/build' build.id 'nixlog' step.stepnr); %]
|
||||
<tr>
|
||||
<td>[% step.stepnr %]</td>
|
||||
<td>[% HTML.escape(step.stepnr) %]</td>
|
||||
<td>
|
||||
[% IF step.type == 0 %]
|
||||
Build of <tt>[% INCLUDE renderOutputs outputs=step.buildstepoutputs %]</tt>
|
||||
@@ -61,21 +61,7 @@ END;
|
||||
<td>[% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)); INCLUDE renderMachineName machine=step.machine; ELSE; "<em>n/a</em>"; END %]</td>
|
||||
<td class="step-status">
|
||||
[% IF step.busy != 0 %]
|
||||
[% IF step.busy == 1 %]
|
||||
<strong>Preparing</strong>
|
||||
[% ELSIF step.busy == 10 %]
|
||||
<strong>Connecting</strong>
|
||||
[% ELSIF step.busy == 20 %]
|
||||
<strong>Sending inputs</strong>
|
||||
[% ELSIF step.busy == 30 %]
|
||||
<strong>Building</strong>
|
||||
[% ELSIF step.busy == 40 %]
|
||||
<strong>Receiving outputs</strong>
|
||||
[% ELSIF step.busy == 50 %]
|
||||
<strong>Post-processing</strong>
|
||||
[% ELSE %]
|
||||
<strong>Unknown state</strong>
|
||||
[% END %]
|
||||
[% INCLUDE renderBusyStatus %]
|
||||
[% ELSIF step.status == 0 %]
|
||||
[% IF step.isnondeterministic %]
|
||||
<span class="warn">Succeeded with non-determistic result</span>
|
||||
@@ -100,7 +86,7 @@ END;
|
||||
[% ELSIF step.status == 11 %]
|
||||
<span class="error">Output limit exceeded</span>
|
||||
[% ELSIF step.status == 12 %]
|
||||
<span class="error">Non-determinism detected</span> [% IF step.timesbuilt %] after [% step.timesbuilt %] times[% END %]
|
||||
<span class="error">Non-determinism detected</span> [% IF step.timesbuilt %] after [% HTML.escape(step.timesbuilt) %] times[% END %]
|
||||
[% ELSIF step.errormsg %]
|
||||
<span class="error">Failed</span>: <em>[% HTML.escape(step.errormsg) %]</em>
|
||||
[% ELSE %]
|
||||
@@ -126,16 +112,16 @@ END;
|
||||
[% IF c.user_exists %]
|
||||
[% IF available %]
|
||||
[% IF build.keep %]
|
||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'keep' 0) %]">Unkeep</a>
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'keep' 0)) %]>Unkeep</a>
|
||||
[% ELSE %]
|
||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'keep' 1) %]">Keep</a>
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'keep' 1)) %]>Keep</a>
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% IF build.finished %]
|
||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'restart') %]">Restart</a>
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'restart')) %]>Restart</a>
|
||||
[% ELSE %]
|
||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'cancel') %]">Cancel</a>
|
||||
<a class="dropdown-item" href="[% c.uri_for('/build' build.id 'bump') %]">Bump up</a>
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'cancel')) %]>Cancel</a>
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for('/build' build.id 'bump')) %]>Bump up</a>
|
||||
[% END %]
|
||||
[% END %]
|
||||
</div>
|
||||
@@ -146,7 +132,7 @@ END;
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-details" data-toggle="tab">Details</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-buildinputs" data-toggle="tab">Inputs</a></li>
|
||||
[% IF steps.size() > 0 %]<li class="nav-item"><a class="nav-link" href="#tabs-buildsteps" data-toggle="tab">Build Steps</a></li>[% END %]
|
||||
[% IF build.dependents %]<li class="nav-item"><a class="nav-link" href="#tabs-usedby" data-toggle="tab">Used By</a></li>[% END%]
|
||||
[% IF build.dependents %]<li class="nav-item"><a class="nav-link" href="#tabs-usedby" data-toggle="tab">Used By</a></li>[% END %]
|
||||
[% IF drvAvailable %]<li class="nav-item"><a class="nav-link" href="#tabs-build-deps" data-toggle="tab">Build Dependencies</a></li>[% END %]
|
||||
[% IF localStore && available %]<li class="nav-item"><a class="nav-link" href="#tabs-runtime-deps" data-toggle="tab">Runtime Dependencies</a></li>[% END %]
|
||||
[% IF runcommandlogProblem || runcommandlogs.size() > 0 %]<li class="nav-item"><a class="nav-link" href="#tabs-runcommandlogs" data-toggle="tab">RunCommand Logs[% IF runcommandlogProblem %] <span class="badge badge-warning">Disabled</span>[% END %]</a></li>[% END %]
|
||||
@@ -165,7 +151,7 @@ END;
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Build ID:</th>
|
||||
<td>[% build.id %]</td>
|
||||
<td>[% HTML.escape(build.id) %]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
@@ -182,9 +168,9 @@ END;
|
||||
END;
|
||||
%];
|
||||
[%+ IF nrFinished == nrConstituents && nrFailedConstituents == 0 %]
|
||||
all [% nrConstituents %] constituent builds succeeded
|
||||
all [% HTML.escape(nrConstituents) %] constituent builds succeeded
|
||||
[% ELSE %]
|
||||
[% nrFailedConstituents %] out of [% nrConstituents %] constituent builds failed
|
||||
[% HTML.escape(nrFailedConstituents) %] out of [% HTML.escape(nrConstituents) %] constituent builds failed
|
||||
[% IF nrFinished < nrConstituents %]
|
||||
([% nrConstituents - nrFinished %] still pending)
|
||||
[% END %]
|
||||
@@ -194,25 +180,25 @@ END;
|
||||
</tr>
|
||||
<tr>
|
||||
<th>System:</th>
|
||||
<td><tt>[% build.system %]</tt></td>
|
||||
<td><tt>[% build.system | html %]</tt></td>
|
||||
</tr>
|
||||
[% IF build.releasename %]
|
||||
<tr>
|
||||
<th>Release name:</th>
|
||||
<td><tt>[% HTML.escape(build.releasename) %]</tt></td>
|
||||
<td><tt>[% build.releasename | html %]</tt></td>
|
||||
</tr>
|
||||
[% ELSE %]
|
||||
<tr>
|
||||
<th>Nix name:</th>
|
||||
<td><tt>[% build.nixname %]</tt></td>
|
||||
<td><tt>[% build.nixname | html %]</tt></td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% IF eval %]
|
||||
<tr>
|
||||
<th>Part of:</th>
|
||||
<td>
|
||||
<a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id]) %]">evaluation [% eval.id %]</a>
|
||||
[% IF nrEvals > 1 +%] (and <a href="[% c.uri_for('/build' build.id 'evals') %]">[% nrEvals - 1 %] others</a>)[% END %]
|
||||
<a [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id])) %]>evaluation [% HTML.escape(eval.id) %]</a>
|
||||
[% IF nrEvals > 1 +%] (and <a [% HTML.attributes(href => c.uri_for('/build' build.id 'evals')) %]>[% nrEvals - 1 %] others</a>)[% END %]
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
@@ -240,9 +226,9 @@ END;
|
||||
<th>Logfile:</th>
|
||||
<td>
|
||||
[% actualLog = cachedBuildStep ? c.uri_for('/build' cachedBuild.id 'nixlog' cachedBuildStep.stepnr) : c.uri_for('/build' build.id 'log') %]
|
||||
<a class="btn btn-secondary btn-sm" href="[%actualLog%]">pretty</a>
|
||||
<a class="btn btn-secondary btn-sm" href="[%actualLog%]/raw">raw</a>
|
||||
<a class="btn btn-secondary btn-sm" href="[%actualLog%]/tail">tail</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => actualLog) %]>pretty</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => actualLog _ "/raw") %]>raw</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => actualLog _ "/tail") %]>tail</a>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
@@ -350,12 +336,12 @@ END;
|
||||
[% IF eval.nixexprinput %]
|
||||
<tr>
|
||||
<th>Nix expression:</th>
|
||||
<td>file <tt>[% HTML.escape(eval.nixexprpath) %]</tt> in input <tt>[% HTML.escape(eval.nixexprinput) %]</tt></td>
|
||||
<td>file <tt>[% eval.nixexprpath | html %]</tt> in input <tt>[% eval.nixexprinput | html %]</tt></td>
|
||||
</tr>
|
||||
[% END %]
|
||||
<tr>
|
||||
<th>Nix name:</th>
|
||||
<td><tt>[% build.nixname %]</tt></td>
|
||||
<td><tt>[% build.nixname | html %]</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Short description:</th>
|
||||
@@ -375,11 +361,11 @@ END;
|
||||
</tr>
|
||||
<tr>
|
||||
<th>System:</th>
|
||||
<td><tt>[% build.system %]</tt></td>
|
||||
<td><tt>[% build.system | html %]</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Derivation store path:</th>
|
||||
<td><tt>[% build.drvpath %]</tt></td>
|
||||
<td><tt>[% build.drvpath | html %]</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Output store paths:</th>
|
||||
@@ -390,14 +376,14 @@ END;
|
||||
<tr>
|
||||
<th>Closure size:</th>
|
||||
<td>[% mibs(build.closuresize / (1024 * 1024)) %] MiB
|
||||
(<a href="[%chartsURL%]">history</a>)</td>
|
||||
(<a [% HTML.attributes(href => chartsURL) %]>history</a>)</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% IF build.finished && build.closuresize %]
|
||||
<tr>
|
||||
<th>Output size:</th>
|
||||
<td>[% mibs(build.size / (1024 * 1024)) %] MiB
|
||||
(<a href="[%chartsURL%]">history</a>)</td>
|
||||
(<a [% HTML.attributes(href => chartsURL) %]>history</a>)</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% IF build.finished && build.buildproducts %]
|
||||
@@ -426,9 +412,9 @@ END;
|
||||
<tbody>
|
||||
[% FOREACH metric IN build.buildmetrics %]
|
||||
<tr>
|
||||
<td><tt><a class="row-link" [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'metric' metric.name)) %]">[%HTML.escape(metric.name)%]</a></tt></td>
|
||||
<td style="text-align: right">[%metric.value%]</td>
|
||||
<td>[%metric.unit%]</td>
|
||||
<td><tt><a class="row-link" [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'metric' metric.name)) %]">[% metric.name | html %]</a></tt></td>
|
||||
<td style="text-align: right">[% HTML.escape(metric.value) %]</td>
|
||||
<td>[% HTML.escape(metric.unit) %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
@@ -470,8 +456,8 @@ END;
|
||||
[% FOREACH input IN build.dependents %]
|
||||
<tr>
|
||||
<td>[% INCLUDE renderFullBuildLink build=input.build %]</td>
|
||||
<td><tt>[% input.name %]</tt></td>
|
||||
<td><tt>[% input.build.system %]</tt></td>
|
||||
<td><tt>[% input.name | html %]</tt></td>
|
||||
<td><tt>[% input.build.system | html %]</tt></td>
|
||||
<td>[% INCLUDE renderDateTime timestamp = input.build.timestamp %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
@@ -498,7 +484,7 @@ END;
|
||||
[% ELSIF runcommandlogProblem == "disabled-jobset" %]
|
||||
This jobset does not enable Dynamic RunCommand support.
|
||||
[% ELSE %]
|
||||
Dynamic RunCommand is not enabled: [% runcommandlogProblem %].
|
||||
Dynamic RunCommand is not enabled: [% HTML.escape(runcommandlogProblem) %].
|
||||
[% END %]
|
||||
</div>
|
||||
[% END %]
|
||||
@@ -517,18 +503,18 @@ END;
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column mr-auto align-self-center">
|
||||
<div><tt>[% runcommandlog.command | html%]</tt></div>
|
||||
<div><tt>[% runcommandlog.command | html %]</tt></div>
|
||||
<div>
|
||||
[% IF not runcommandlog.is_running() %]
|
||||
[% IF runcommandlog.did_fail_with_signal() %]
|
||||
Exit signal: [% runcommandlog.signal %]
|
||||
Exit signal: [% runcommandlog.signal | html %]
|
||||
[% IF runcommandlog.core_dumped %]
|
||||
(Core Dumped)
|
||||
[% END %]
|
||||
[% ELSIF runcommandlog.did_fail_with_exec_error() %]
|
||||
Exec error: [% runcommandlog.error_number %]
|
||||
Exec error: [% runcommandlog.error_number | html %]
|
||||
[% ELSIF not runcommandlog.did_succeed() %]
|
||||
Exit code: [% runcommandlog.exit_code %]
|
||||
Exit code: [% runcommandlog.exit_code | html %]
|
||||
[% END %]
|
||||
[% END %]
|
||||
</div>
|
||||
@@ -546,9 +532,9 @@ END;
|
||||
[% IF runcommandlog.uuid != undef %]
|
||||
[% runLog = c.uri_for('/build', build.id, 'runcommandlog', runcommandlog.uuid) %]
|
||||
<div>
|
||||
<a class="btn btn-secondary btn-sm" href="[% runLog %]">pretty</a>
|
||||
<a class="btn btn-secondary btn-sm" href="[% runLog %]/raw">raw</a>
|
||||
<a class="btn btn-secondary btn-sm" href="[% runLog %]/tail">tail</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => runLog) %]>pretty</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => runLog) %]/raw">raw</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => runLog) %]/tail">tail</a>
|
||||
</div>
|
||||
[% END %]
|
||||
</div>
|
||||
@@ -577,17 +563,17 @@ END;
|
||||
|
||||
[% IF eval.flake %]
|
||||
|
||||
<p>If you have <a href='https://nixos.org/nix/download.html'>Nix
|
||||
<p>If you have <a href='https://nixos.org/download/'>Nix
|
||||
installed</a>, you can reproduce this build on your own machine by
|
||||
running the following command:</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body p-2"><code>
|
||||
<span class="shell-prompt"># </span>nix build [% HTML.escape(eval.flake) %]#hydraJobs.[% HTML.escape(job) %]
|
||||
<span class="shell-prompt"># </span>nix build '[% HTML.escape(eval.flake) %]#hydraJobs.[% HTML.escape(job) %]'
|
||||
</code></div></div>
|
||||
|
||||
[% ELSE %]
|
||||
|
||||
<p>If you have <a href='https://nixos.org/nix/download.html'>Nix
|
||||
<p>If you have <a href='https://nixos.org/download/'>Nix
|
||||
installed</a>, you can reproduce this build on your own machine by
|
||||
downloading <a [% HTML.attributes(href => url) %]>a script</a>
|
||||
that checks out all inputs of the build and then invokes Nix to
|
||||
|
||||
@@ -7,7 +7,7 @@ href="http://nixos.org/">Nix package manager</a>. If you have Nix
|
||||
installed, you can subscribe to this channel by once executing</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body"><pre>
|
||||
<span class="shell-prompt">$ </span>nix-channel --add [% curUri +%]
|
||||
<span class="shell-prompt">$ </span>nix-channel --add [% HTML.escape(curUri) +%]
|
||||
<span class="shell-prompt">$ </span>nix-channel --update
|
||||
</pre></div></div>
|
||||
|
||||
@@ -49,9 +49,9 @@ installed, you can subscribe to this channel by once executing</p>
|
||||
[% b = pkg.build %]
|
||||
|
||||
<tr>
|
||||
<td><a href="[% c.uri_for('/build' b.id) %]">[% b.id %]</a></td>
|
||||
<td><tt>[% b.get_column('releasename') || b.nixname %]</tt></td>
|
||||
<td><tt>[% b.system %]</tt></td>
|
||||
<td><a [% HTML.attributes(href => c.uri_for('/build' b.id)) %]>[% HTML.escape(b.id) %]</a></td>
|
||||
<td><tt>[% b.get_column('releasename') || b.nixname | html %]</tt></td>
|
||||
<td><tt>[% b.system | html %]</tt></td>
|
||||
<td>
|
||||
[% IF b.homepage %]
|
||||
<a [% HTML.attributes(href => b.homepage) %]>[% HTML.escape(b.description) %]</a>
|
||||
|
||||
@@ -55,17 +55,17 @@ BLOCK renderRelativeDate %]
|
||||
[% END;
|
||||
|
||||
BLOCK renderProjectName %]
|
||||
<a [% IF inRow %]class="row-link"[% END %] href="[% c.uri_for('/project' project) %]"><tt>[% project %]</tt></a>
|
||||
<a [% IF inRow %]class="row-link"[% END %] [% HTML.attributes(href => c.uri_for('/project' project)) %]><tt>[% project | html %]</tt></a>
|
||||
[% END;
|
||||
|
||||
|
||||
BLOCK renderJobsetName %]
|
||||
<a [% IF inRow %]class="row-link"[% END %] href="[% c.uri_for('/jobset' project jobset) %]"><tt>[% jobset %]</tt></a>
|
||||
<a [% IF inRow %]class="row-link"[% END %] [% HTML.attributes(href => c.uri_for('/jobset' project jobset)) %]><tt>[% jobset | html %]</tt></a>
|
||||
[% END;
|
||||
|
||||
|
||||
BLOCK renderJobName %]
|
||||
<a [% IF inRow %]class="row-link"[% END %] href="[% c.uri_for('/job' project jobset job) %]">[% job %]</a>
|
||||
<a [% IF inRow %]class="row-link"[% END %] [% HTML.attributes(href => c.uri_for('/job' project jobset job)) %]>[% job | html %]</a>
|
||||
[% END;
|
||||
|
||||
|
||||
@@ -91,6 +91,17 @@ BLOCK renderDuration;
|
||||
duration % 60 %]s[%
|
||||
END;
|
||||
|
||||
BLOCK renderDrvInfo;
|
||||
drvname = step.drvpath
|
||||
.substr(11) # strip `/nix/store/`
|
||||
.split('-').slice(1).join("-") # strip hash part
|
||||
.substr(0, -4); # strip `.drv`
|
||||
IF drvname != releasename;
|
||||
IF step.type == 0; action = "Build"; ELSE; action = "Substitution"; END;
|
||||
IF drvname; %]<em> ([% HTML.escape(action) %] of [% HTML.escape(drvname) %])</em>[% END;
|
||||
END;
|
||||
END;
|
||||
|
||||
|
||||
BLOCK renderBuildListHeader %]
|
||||
<table class="table table-striped table-condensed clickable-rows">
|
||||
@@ -129,20 +140,25 @@ BLOCK renderBuildListBody;
|
||||
[% IF showSchedulingInfo %]
|
||||
<td>[% IF busy %]<span class="badge badge-success">Started</span>[% ELSE %]<span class="badge badge-secondary">Queued</span>[% END %]</td>
|
||||
[% END %]
|
||||
<td><a class="row-link" href="[% link %]">[% build.id %]</a></td>
|
||||
<td><a class="row-link" [% HTML.attributes(href => link) %]>[% HTML.escape(build.id) %]</a></td>
|
||||
[% IF !hideJobName %]
|
||||
<td><a href="[%link%]">[% IF !hideJobsetName %][%build.jobset.get_column("project")%]:[%build.jobset.get_column("name")%]:[% END %][%build.get_column("job")%]</td>
|
||||
<td>
|
||||
<a [% HTML.attributes(href => link) %]>[% IF !hideJobsetName %][% HTML.escape(build.jobset.get_column("project")) %]:[% HTML.escape(build.jobset.get_column("name")) %]:[% END %][% HTML.escape(build.get_column("job")) %]</a>
|
||||
[% IF showStepName %]
|
||||
[% INCLUDE renderDrvInfo step=build.buildsteps releasename=build.nixname %]
|
||||
[% END %]
|
||||
</td>
|
||||
[% END %]
|
||||
<td class="nowrap">[% t = showSchedulingInfo ? build.timestamp : build.stoptime; IF t; INCLUDE renderRelativeDate timestamp=(showSchedulingInfo ? build.timestamp : build.stoptime); ELSE; "-"; END %]</td>
|
||||
<td>[% !showSchedulingInfo and build.get_column('releasename') ? build.get_column('releasename') : build.nixname %]</td>
|
||||
<td class="nowrap"><tt>[% build.system %]</tt></td>
|
||||
<td>[% !showSchedulingInfo and build.get_column('releasename') ? HTML.escape(build.get_column('releasename')) : HTML.escape(build.nixname) %]</td>
|
||||
<td class="nowrap"><tt>[% build.system | html %]</tt></td>
|
||||
[% IF showDescription %]
|
||||
<td>[% build.description %]</td>
|
||||
<td>[% HTML.escape(build.description) %]</td>
|
||||
[% END %]
|
||||
</tr>
|
||||
[% END;
|
||||
IF linkToAll %]
|
||||
<tr><td class="centered" colspan="5"><a href="[% linkToAll %]"><em>More...</em></a></td></tr>
|
||||
<tr><td class="centered" colspan="5"><a [% HTML.attributes(href => linkToAll) %]><em>More...</em></a></td></tr>
|
||||
[% END;
|
||||
END;
|
||||
|
||||
@@ -160,11 +176,11 @@ BLOCK renderBuildList;
|
||||
END;
|
||||
|
||||
|
||||
BLOCK renderLink %]<a href="[% uri %]">[% title %]</a>[% END;
|
||||
BLOCK renderLink %]<a [% HTML.attributes(href => uri) %]>[% HTML.escape(title) %]</a>[% END;
|
||||
|
||||
|
||||
BLOCK maybeLink;
|
||||
IF uri %]<a [% HTML.attributes(href => uri, class => class); IF confirmmsg +%] onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>[% content %]</a>[% ELSE; content; END;
|
||||
IF uri %]<a [% HTML.attributes(href => uri, class => class); IF confirmmsg +%] onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>[% HTML.escape(content) %]</a>[% ELSE; HTML.escape(content); END;
|
||||
END;
|
||||
|
||||
|
||||
@@ -176,7 +192,7 @@ BLOCK renderSelection;
|
||||
<label class="radio inline">
|
||||
<input type="radio" [% HTML.attributes(id => param, name => param, value => name) %]
|
||||
[% IF name == curValue; "checked='1'"; END %]>
|
||||
[% options.$name %]
|
||||
[% HTML.escape(options.$name) %]
|
||||
</input>
|
||||
</label>
|
||||
[% END %]
|
||||
@@ -184,7 +200,7 @@ BLOCK renderSelection;
|
||||
[% ELSE %]
|
||||
<select class="custom-select" [% HTML.attributes(id => param, name => param) %]>
|
||||
[% FOREACH name IN options.keys.sort %]
|
||||
<option [% IF name == curValue; "selected='selected'"; END; " "; HTML.attributes(value => name) %]>[% options.$name %]</option>
|
||||
<option [% IF name == curValue; "selected='selected'"; END; " "; HTML.attributes(value => name) %]>[% HTML.escape(options.$name) %]</option>
|
||||
[% END %]
|
||||
</select>
|
||||
[% END;
|
||||
@@ -200,12 +216,12 @@ BLOCK editString; %]
|
||||
|
||||
|
||||
BLOCK renderFullBuildLink;
|
||||
INCLUDE renderFullJobNameOfBuild build=build %] <a href="[% c.uri_for('/build' build.id) %]">build [% build.id %]</a>[%
|
||||
INCLUDE renderFullJobNameOfBuild build=build %] <a [% HTML.attributes(href => c.uri_for('/build' build.id)) %]>build [% HTML.escape(build.id) %]</a>[%
|
||||
END;
|
||||
|
||||
|
||||
BLOCK renderBuildIdLink; %]
|
||||
<a href="[% c.uri_for('/build' id) %]">build [% id %]</a>
|
||||
<a [% HTML.attributes(href => c.uri_for('/build' id)) %]>build [% HTML.escape(id) %]</a>
|
||||
[% END;
|
||||
|
||||
|
||||
@@ -245,6 +261,27 @@ BLOCK renderBuildStatusIcon;
|
||||
END;
|
||||
|
||||
|
||||
BLOCK renderBusyStatus;
|
||||
IF step.busy == 1 %]
|
||||
<strong>Preparing</strong>
|
||||
[% ELSIF step.busy == 10 %]
|
||||
<strong>Connecting</strong>
|
||||
[% ELSIF step.busy == 20 %]
|
||||
<strong>Sending inputs</strong>
|
||||
[% ELSIF step.busy == 30 %]
|
||||
<strong>Building</strong>
|
||||
[% ELSIF step.busy == 35 %]
|
||||
<strong>Waiting to receive outputs</strong>
|
||||
[% ELSIF step.busy == 40 %]
|
||||
<strong>Receiving outputs</strong>
|
||||
[% ELSIF step.busy == 50 %]
|
||||
<strong>Post-processing</strong>
|
||||
[% ELSE %]
|
||||
<strong>Unknown state</strong>
|
||||
[% END;
|
||||
END;
|
||||
|
||||
|
||||
BLOCK renderStatus;
|
||||
IF build.finished;
|
||||
buildstatus = build.buildstatus;
|
||||
@@ -283,7 +320,7 @@ END;
|
||||
|
||||
BLOCK renderShortInputValue;
|
||||
IF input.type == "build" || input.type == "sysbuild" %]
|
||||
<a href="[% c.uri_for('/build' input.dependency.id) %]">[% input.dependency.id %]</a>
|
||||
<a [% HTML.attributes(href => c.uri_for('/build' input.dependency.id)) %]>[% HTML.escape(input.dependency.id) %]</a>
|
||||
[% ELSIF input.type == "string" %]
|
||||
<tt>"[% HTML.escape(input.value) %]"</tt>
|
||||
[% ELSIF input.type == "nix" || input.type == "boolean" %]
|
||||
@@ -301,7 +338,7 @@ BLOCK renderDiffUri;
|
||||
url = bi1.uri;
|
||||
path = url.replace(base, '');
|
||||
IF url.match(base) %]
|
||||
<a target="_blank" href="[% m.uri.replace('_path_', path).replace('_1_', bi1.revision).replace('_2_', bi2.revision) %]">[% contents %]</a>
|
||||
<a target="_blank" [% HTML.attributes(href => m.uri.replace('_path_', path).replace('_1_', bi1.revision).replace('_2_', bi2.revision)) %]>[% HTML.escape(contents) %]</a>
|
||||
[% nouri = 0;
|
||||
END;
|
||||
END;
|
||||
@@ -310,13 +347,13 @@ BLOCK renderDiffUri;
|
||||
url = res.0;
|
||||
branch = res.1;
|
||||
IF bi1.type == "hg" || bi1.type == "git" %]
|
||||
<a target="_blank" href="[% HTML.escape(c.uri_for('/api/scmdiff', {
|
||||
<a target="_blank" [% HTML.attributes(href => c.uri_for('/api/scmdiff', {
|
||||
uri = url,
|
||||
rev1 = bi1.revision,
|
||||
rev2 = bi2.revision,
|
||||
type = bi1.type,
|
||||
branch = branch
|
||||
})) %]">[% contents %]</a>
|
||||
})) %]>[% HTML.escape(contents) %]</a>
|
||||
[% ELSE;
|
||||
contents;
|
||||
END;
|
||||
@@ -332,8 +369,8 @@ BLOCK renderInputs; %]
|
||||
<tbody>
|
||||
[% FOREACH input IN inputs %]
|
||||
<tr>
|
||||
<td><tt>[% input.name %]</tt></td>
|
||||
<td>[% type = input.type; inputTypes.$type %]</td>
|
||||
<td><tt>[% input.name | html %]</tt></td>
|
||||
<td>[% type = input.type; HTML.escape(inputTypes.$type) %]</td>
|
||||
<td>
|
||||
[% IF input.type == "build" || input.type == "sysbuild" %]
|
||||
[% INCLUDE renderFullBuildLink build=input.dependency %]
|
||||
@@ -346,7 +383,7 @@ BLOCK renderInputs; %]
|
||||
[% END %]
|
||||
</td>
|
||||
<td>[% IF input.revision %][% HTML.escape(input.revision) %][% END %]</td>
|
||||
<td><tt>[% input.path %]</tt></td>
|
||||
<td><tt>[% input.path | html %]</tt></td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
@@ -370,33 +407,33 @@ BLOCK renderInputDiff; %]
|
||||
IF bi1.name == bi2.name;
|
||||
IF bi1.type == bi2.type;
|
||||
IF bi1.value != bi2.value || bi1.uri != bi2.uri %]
|
||||
<tr><td><b>[% bi1.name %]</b></td><td><tt>[% INCLUDE renderShortInputValue input=bi1 %]</tt> to <tt>[% INCLUDE renderShortInputValue input=bi2 %]</tt></td></tr>
|
||||
<tr><td><b>[% HTML.escape(bi1.name) %]</b></td><td><tt>[% INCLUDE renderShortInputValue input=bi1 %]</tt> to <tt>[% INCLUDE renderShortInputValue input=bi2 %]</tt></td></tr>
|
||||
[% ELSIF bi1.uri == bi2.uri && bi1.revision != bi2.revision %]
|
||||
[% IF bi1.type == "git" %]
|
||||
<tr><td>
|
||||
<b>[% bi1.name %]</b></td><td><tt>[% INCLUDE renderDiffUri contents=(bi1.revision.substr(0, 6) _ ' to ' _ bi2.revision.substr(0, 6)) %]</tt>
|
||||
<b>[% HTML.escape(bi1.name) %]</b></td><td><tt>[% INCLUDE renderDiffUri contents=(bi1.revision.substr(0, 12) _ ' to ' _ bi2.revision.substr(0, 12)) %]</tt>
|
||||
</td></tr>
|
||||
[% ELSE %]
|
||||
<tr><td>
|
||||
<b>[% bi1.name %]</b></td><td><tt>[% INCLUDE renderDiffUri contents=(bi1.revision _ ' to ' _ bi2.revision) %]</tt>
|
||||
<b>[% HTML.escape(bi1.name) %]</b></td><td><tt>[% INCLUDE renderDiffUri contents=(bi1.revision _ ' to ' _ bi2.revision) %]</tt>
|
||||
</td></tr>
|
||||
[% END %]
|
||||
[% ELSIF bi1.dependency.id != bi2.dependency.id || bi1.path != bi2.path %]
|
||||
<tr><td>
|
||||
<b>[% bi1.name %]</b></td><td><tt>[% INCLUDE renderShortInputValue input=bi1 %]</tt> to <tt>[% INCLUDE renderShortInputValue input=bi2 %]</tt>
|
||||
<b>[% HTML.escape(bi1.name) %]</b></td><td><tt>[% INCLUDE renderShortInputValue input=bi1 %]</tt> to <tt>[% INCLUDE renderShortInputValue input=bi2 %]</tt>
|
||||
<br/>
|
||||
<br/>
|
||||
[% INCLUDE renderInputDiff inputs1=bi1.dependency.inputs inputs2=bi2.dependency.inputs nestedDiff=1 nestLevel=nestLevel+1 %]
|
||||
</td></tr>
|
||||
[% END %]
|
||||
[% ELSE %]
|
||||
<tr><td><b>[% bi1.name %]</b></td><td>Changed input type from '[% type = bi1.type; inputTypes.$type %]' to '[% type = bi2.type; inputTypes.$type %]'</td></tr>
|
||||
<tr><td><b>[% HTML.escape(bi1.name) %]</b></td><td>Changed input type from '[% type = bi1.type; HTML.escape(inputTypes.$type) %]' to '[% type = bi2.type; HTML.escape(inputTypes.$type) %]'</td></tr>
|
||||
[% END;
|
||||
deletedInput = 0;
|
||||
END;
|
||||
END;
|
||||
IF deletedInput == 1 %]
|
||||
<tr><td><b>[% bi1.name %]</b></td><td>Input not present in this build.</td></tr>
|
||||
<tr><td><b>[% HTML.escape(bi1.name) %]</b></td><td>Input not present in this build.</td></tr>
|
||||
[% END;
|
||||
END;
|
||||
END %]
|
||||
@@ -406,10 +443,10 @@ BLOCK renderInputDiff; %]
|
||||
|
||||
BLOCK renderPager %]
|
||||
<ul class="pagination">
|
||||
<li class="page-item[% IF page == 1 %] disabled[% END %]"><a class="page-link" href="[% "$baseUri?page=1" %]">« First</a></li>
|
||||
<li class="page-item[% IF page == 1 %] disabled[% END %]"><a class="page-link" href="[% "$baseUri?page="; (page - 1) %]">‹ Previous</a></li>
|
||||
<li class="page-item[% IF page * resultsPerPage >= total %] disabled[% END %]"><a class="page-link" href="[% "$baseUri?page="; (page + 1) %]">Next ›</a></li>
|
||||
<li class="page-item[% IF page * resultsPerPage >= total %] disabled[% END %]"><a class="page-link" href="[% "$baseUri?page="; (total - 1) div resultsPerPage + 1 %]">Last »</a></li>
|
||||
<li class="page-item[% IF page == 1 %] disabled[% END %]"><a class="page-link" [% HTML.attributes(href => "$baseUri?page=1") %]>« First</a></li>
|
||||
<li class="page-item[% IF page == 1 %] disabled[% END %]"><a class="page-link" [% HTML.attributes(href => "$baseUri?page=" _ (page - 1)) %]>‹ Previous</a></li>
|
||||
<li class="page-item[% IF page * resultsPerPage >= total %] disabled[% END %]"><a class="page-link" [% HTML.attributes(href => "$baseUri?page=" _ (page + 1)) %]>Next ›</a></li>
|
||||
<li class="page-item[% IF page * resultsPerPage >= total %] disabled[% END %]"><a class="page-link" [% HTML.attributes(href => "$baseUri?page=" _ ((total - 1) div resultsPerPage + 1)) %]>Last »</a></li>
|
||||
</ul>
|
||||
[% END;
|
||||
|
||||
@@ -418,13 +455,13 @@ BLOCK renderShortEvalInput;
|
||||
IF input.type == "svn" || input.type == "svn-checkout" || input.type == "bzr" || input.type == "bzr-checkout" %]
|
||||
r[% input.revision %]
|
||||
[% ELSIF input.type == "git" %]
|
||||
<tt>[% input.revision.substr(0, 7) %]</tt>
|
||||
<tt>[% input.revision.substr(0, 7) | html %]</tt>
|
||||
[% ELSIF input.type == "hg" %]
|
||||
<tt>[% input.revision.substr(0, 12) %]</tt>
|
||||
<tt>[% input.revision.substr(0, 12) | html %]</tt>
|
||||
[% ELSIF input.type == "build" || input.type == "sysbuild" %]
|
||||
<a href="[% c.uri_for('/build' input.get_column('dependency')) %]">[% input.get_column('dependency') %]</a>
|
||||
<a [% HTML.attributes(href => c.uri_for('/build' input.get_column('dependency'))) %]>[% HTML.escape(input.get_column('dependency')) %]</a>
|
||||
[% ELSE %]
|
||||
<tt>[% input.revision %]</tt>
|
||||
<tt>[% input.revision | html %]</tt>
|
||||
[% END;
|
||||
END;
|
||||
|
||||
@@ -461,7 +498,7 @@ BLOCK renderEvals %]
|
||||
eval = e.eval;
|
||||
link = c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id]) %]
|
||||
<tr>
|
||||
<td><a class="row-link" href="[% link %]">[% eval.id %]</a></td>
|
||||
<td><a class="row-link" [% HTML.attributes(href => link) %]>[% HTML.escape(eval.id) %]</a></td>
|
||||
[% IF !jobset && !build %]
|
||||
<td>[% INCLUDE renderFullJobsetName project=eval.jobset.project.name jobset=eval.jobset.name %]</td>
|
||||
[% END %]
|
||||
@@ -470,40 +507,40 @@ BLOCK renderEvals %]
|
||||
[% IF e.changedInputs.size > 0;
|
||||
sep='';
|
||||
FOREACH input IN e.changedInputs;
|
||||
sep; %] [% input.name %] → [% INCLUDE renderShortEvalInput input=input;
|
||||
sep; %] [% HTML.escape(input.name) %] → [% INCLUDE renderShortEvalInput input=input;
|
||||
sep=', ';
|
||||
END;
|
||||
ELSE %]
|
||||
-
|
||||
[% END %]
|
||||
[% IF eval.evaluationerror.errormsg %]
|
||||
[% IF eval.evaluationerror.has_error %]
|
||||
<span class="badge badge-warning">Eval Errors</span>
|
||||
[% END %]
|
||||
</td>
|
||||
<td align='right' class="nowrap">
|
||||
<span class="badge badge-success">[% e.nrSucceeded %]</span>
|
||||
<span class="badge badge-success">[% HTML.escape(e.nrSucceeded) %]</span>
|
||||
</td>
|
||||
<td align="right" class="nowrap">
|
||||
[% IF e.nrFailed > 0 %]
|
||||
<span class="badge badge-danger">[% e.nrFailed %]</span>
|
||||
<span class="badge badge-danger">[% HTML.escape(e.nrFailed) %]</span>
|
||||
[% END %]
|
||||
</td>
|
||||
<td align="right" class="nowrap">
|
||||
[% IF e.nrScheduled > 0 %]
|
||||
<span class="badge badge-secondary">[% e.nrScheduled %]</span>
|
||||
<span class="badge badge-secondary">[% HTML.escape(e.nrScheduled) %]</span>
|
||||
[% END %]
|
||||
</td>
|
||||
<td align='right' class="nowrap">
|
||||
[% IF e.diff > 0 %]
|
||||
<span class='badge badge-success'><strong>+[% e.diff %]</strong></span>
|
||||
<span class='badge badge-success'><strong>+[% HTML.escape(e.diff) %]</strong></span>
|
||||
[% ELSIF e.diff < 0 && e.nrScheduled == 0 %]
|
||||
<span class='badge badge-danger'><strong>[% e.diff %]</strong></span>
|
||||
<span class='badge badge-danger'><strong>[% HTML.escape(e.diff) %]</strong></span>
|
||||
[% END %]
|
||||
</td>
|
||||
</tr>
|
||||
[% END;
|
||||
IF linkToAll %]
|
||||
<tr><td class="centered" colspan="7"><a href="[% linkToAll %]"><em>More...</em></a></td></tr>
|
||||
<tr><td class="centered" colspan="7"><a [% HTML.attributes(href => linkToAll) %]><em>More...</em></a></td></tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -511,19 +548,19 @@ BLOCK renderEvals %]
|
||||
|
||||
|
||||
BLOCK renderLogLinks %]
|
||||
(<a [% IF inRow %]class="row-link"[% END %] href="[% url %]">log</a>, <a href="[% "$url/raw" %]">raw</a>, <a href="[% "$url/tail" %]">tail</a>)
|
||||
(<a [% IF inRow %]class="row-link"[% END %] [% HTML.attributes(href => url) %]>log</a>, <a [% HTML.attributes(href => "$url/raw") %]>raw</a>, <a [% HTML.attributes(href => "$url/tail") %]>tail</a>)
|
||||
[% END;
|
||||
|
||||
|
||||
BLOCK makeLazyTab %]
|
||||
<div id="[% tabName %]" class="tab-pane">
|
||||
<div [% HTML.attributes(id => tabName) %] class="tab-pane">
|
||||
<center><span class="spinner-border spinner-border-sm"/></center>
|
||||
</div>
|
||||
<script>
|
||||
[% IF callback.defined %]
|
||||
$(function() { makeLazyTab("[% tabName %]", "[% uri %]", [% callback %] ); });
|
||||
$(function() { makeLazyTab("[% HTML.escape(tabName) %]", "[% uri %]", [% callback %] ); });
|
||||
[% ELSE %]
|
||||
$(function() { makeLazyTab("[% tabName %]", "[% uri %]", null ); });
|
||||
$(function() { makeLazyTab("[% HTML.escape(tabName) %]", "[% uri %]", null ); });
|
||||
[% END %]
|
||||
</script>
|
||||
[% END;
|
||||
@@ -550,7 +587,7 @@ BLOCK navItem %]
|
||||
<li class="nav-item">
|
||||
<a class="nav-link[% IF "${root}${curUri}" == uri %] active[% END %]"
|
||||
[% HTML.attributes(href => uri) %]>
|
||||
[% title %]
|
||||
[% HTML.escape(title) %]
|
||||
</a>
|
||||
</li>
|
||||
[% END;
|
||||
@@ -602,7 +639,7 @@ BLOCK renderJobsetOverview %]
|
||||
<td>[% HTML.escape(j.description) %]</td>
|
||||
<td>[% IF j.lastcheckedtime;
|
||||
INCLUDE renderDateTime timestamp = j.lastcheckedtime;
|
||||
IF j.errormsg || j.fetcherrormsg; %] <span class = 'badge badge-warning'>Error</span>[% END;
|
||||
IF j.has_error || j.fetcherrormsg; %] <span class = 'badge badge-warning'>Error</span>[% END;
|
||||
ELSE; "-";
|
||||
END %]</td>
|
||||
[% IF j.get_column('nrtotal') > 0 %]
|
||||
@@ -620,17 +657,17 @@ BLOCK renderJobsetOverview %]
|
||||
<td><span class="[% class %]">[% successrate FILTER format('%d') %]%</span></td>
|
||||
<td>
|
||||
[% IF j.get_column('nrsucceeded') > 0 %]
|
||||
<span class="badge badge-success">[% j.get_column('nrsucceeded') %]</span>
|
||||
<span class="badge badge-success">[% HTML.escape(j.get_column('nrsucceeded')) %]</span>
|
||||
[% END %]
|
||||
</td>
|
||||
<td>
|
||||
[% IF j.get_column('nrfailed') > 0 %]
|
||||
<span class="badge badge-danger">[% j.get_column('nrfailed') %]</span>
|
||||
<span class="badge badge-danger">[% HTML.escape(j.get_column('nrfailed')) %]</span>
|
||||
[% END %]
|
||||
</td>
|
||||
<td>
|
||||
[% IF j.get_column('nrscheduled') > 0 %]
|
||||
<span class="badge badge-secondary">[% j.get_column('nrscheduled') %]</span>
|
||||
<span class="badge badge-secondary">[% HTML.escape(j.get_column('nrscheduled')) %]</span>
|
||||
[% END %]
|
||||
</td>
|
||||
</tr>
|
||||
@@ -648,14 +685,22 @@ BLOCK includeFlot %]
|
||||
[% END;
|
||||
|
||||
|
||||
BLOCK renderYesNo %]
|
||||
[% IF value %]
|
||||
<span class="text-success">Yes</span>
|
||||
[% ELSE %]
|
||||
<span class="text-danger">No</span>
|
||||
[% END %]
|
||||
[% END;
|
||||
|
||||
BLOCK createChart %]
|
||||
|
||||
<div id="[%id%]-chart" style="width: 1000px; height: 400px;"></div>
|
||||
<div id="[%id%]-overview" style="margin-top: 20px; margin-left: 50px; margin-right: 50px; width: 900px; height: 100px"></div>
|
||||
<div id="[% id %]-chart" style="width: 1000px; height: 400px;"></div>
|
||||
<div id="[% id %]-overview" style="margin-top: 20px; margin-left: 50px; margin-right: 50px; width: 900px; height: 100px"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
showChart("[%id%]", "[%dataUrl%]", "[%yaxis%]");
|
||||
showChart("[% HTML.escape(id) %]", "[% dataUrl %]", "[% yaxis %]");
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
[% ELSE %]
|
||||
|
||||
<p>Below are the most recent builds of the [% builds.size %] jobs of which you
|
||||
<p>Below are the most recent builds of the [% HTML.escape(builds.size) %] jobs of which you
|
||||
(<tt>[% HTML.escape(user.emailaddress) %]</tt>) are a maintainer.</p>
|
||||
|
||||
[% INCLUDE renderBuildList %]
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<tr>
|
||||
<td><span class="[% IF !jobExists(j.job.jobset j.job.job) %]disabled-job[% END %]">[% INCLUDE renderFullJobName project=j.job.get_column('project') jobset=j.job.get_column('jobset') job=j.job.job %]</span></td>
|
||||
[% FOREACH b IN j.builds %]
|
||||
<td><a href="[% c.uri_for('/build' b.id) %]">[% INCLUDE renderBuildStatusIcon size=16 build=b %]</a></td>
|
||||
<td><a [% HTML.attributes(href => c.uri_for('/build' b.id)) %]>[% INCLUDE renderBuildStatusIcon size=16 build=b %]</a></td>
|
||||
[% END %]
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
[% BLOCK renderNode %]
|
||||
<li>
|
||||
[% IF done.${node.path} %]
|
||||
<tt>[% node.name %]</tt> (<a href="#[% done.${node.path} %]"><em>repeated</em></a>)
|
||||
<tt>[% node.name | html %]</tt> (<a [% HTML.attributes(href => "#" _ done.${node.path}) %]><em>repeated</em></a>)
|
||||
[% ELSE %]
|
||||
[% done.${node.path} = global.nodeId; global.nodeId = global.nodeId + 1; %]
|
||||
[% IF node.refs.size > 0 %]
|
||||
<a href="javascript:" class="tree-toggle"></a>
|
||||
[% END %]
|
||||
<span id="[% done.${node.path} %]"><span class="dep-tree-line">
|
||||
<span [% HTML.attributes(id => done.${node.path}) %]><span class="dep-tree-line">
|
||||
[% IF node.buildStep %]
|
||||
<a href="[% c.uri_for('/build' node.buildStep.get_column('build')) %]"><tt>[% node.name %]</tt></a> [%
|
||||
<a [% HTML.attributes(href => c.uri_for('/build' node.buildStep.get_column('build'))) %]><tt>[% node.name %]</tt></a> [%
|
||||
IF buildStepLogExists(node.buildStep);
|
||||
INCLUDE renderLogLinks url=c.uri_for('/build' node.buildStep.get_column('build') 'nixlog' node.buildStep.stepnr);
|
||||
END %]
|
||||
[% ELSE %]
|
||||
<tt>[% node.name %]</tt> (<em>no info</em>)
|
||||
<tt>[% node.name | html %]</tt> (<em>no info</em>)
|
||||
[% END %]
|
||||
</span></span>
|
||||
[% IF isRoot %]
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
[% USE format %]
|
||||
|
||||
[% BLOCK renderJobsetInput %]
|
||||
<tr class="input [% extraClass %]" [% IF id %]id="[% id %]"[% END %]>
|
||||
<tr class="input [% extraClass %]" [% IF id %][% HTML.attributes(id => id) %][% END %]>
|
||||
<td>
|
||||
<button type="button" class="btn btn-warning" onclick='$(this).parents(".input").remove()'><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="[% baseName %]-name" name="[% baseName %]-name" [% HTML.attributes(value => input.name) %]/>
|
||||
<input type="text" [% HTML.attributes(id => baseName _ "-name", name => baseName _ "-name", value => input.name) %] />
|
||||
</td>
|
||||
<td>
|
||||
[% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes edit=1 %]
|
||||
</td>
|
||||
<td id="[% baseName %]">
|
||||
<td [% HTML.attributes(id => baseName) %]>
|
||||
[% IF createFromEval %]
|
||||
[% value = (input.uri or input.value); IF input.revision; value = value _ " " _ input.revision; END;
|
||||
warn = input.altnr != 0;
|
||||
@@ -36,7 +36,7 @@
|
||||
<input style="width: 95%" type="text" [% HTML.attributes(value => value, id => "$baseName-value", name => "$baseName-value") %]/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="[% baseName %]-emailresponsible" name="[% baseName %]-emailresponsible" [% IF input.emailresponsible; 'checked="checked"'; END %]/>
|
||||
<input type="checkbox" [% HTML.attributes(id => "$baseName-emailresponsible", name => "$baseName-emailresponsible") %] [% IF input.emailresponsible; 'checked="checked"'; END %]/>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
@@ -149,7 +149,7 @@
|
||||
<label class="col-sm-3" for="editjobsetschedulingshares">
|
||||
Scheduling shares
|
||||
[% IF totalShares %]
|
||||
<small class="form-text text-muted">([% f = format("%.2f"); f(jobset.schedulingshares / totalShares * 100) %]% out of [% totalShares %] shares)</small>
|
||||
<small class="form-text text-muted">([% f = format("%.2f"); f(jobset.schedulingshares / totalShares * 100) %]% out of [% HTML.escape(totalShares) %] shares)</small>
|
||||
[% END %]
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
@@ -195,7 +195,7 @@
|
||||
|
||||
[% INCLUDE renderJobsetInputs %]
|
||||
|
||||
<button id="submit-jobset" type="submit" class="btn btn-primary"><i class="fas fa-check"></i> [%IF !edit %]Create jobset[% ELSE %]Apply changes[% END %]</button>
|
||||
<button id="submit-jobset" type="submit" class="btn btn-primary"><i class="fas fa-check"></i> [% IF !edit %]Create jobset[% ELSE %]Apply changes[% END %]</button>
|
||||
|
||||
<table style="display: none">
|
||||
[% INCLUDE renderJobsetInput input="" extraClass="template" id="input-template" baseName="input-template" %]
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
<button id="submit-project" type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-check"></i>
|
||||
[%IF create %]Create project[% ELSE %]Apply changes[% END %]
|
||||
[% IF create %]Create project[% ELSE %]Apply changes[% END %]
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
26
src/root/eval-error.tt
Normal file
26
src/root/eval-error.tt
Normal file
@@ -0,0 +1,26 @@
|
||||
[% PROCESS common.tt %]
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
[% INCLUDE style.tt %]
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="tab-content tab-pane">
|
||||
<div id="tabs-errors" class="">
|
||||
[% IF eval %]
|
||||
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(eval.evaluationerror.errortime || eval.timestamp) %].</p>
|
||||
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(eval.evaluationerror.errormsg) %]</pre></div></div>
|
||||
[% ELSIF jobset %]
|
||||
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(jobset.errortime || jobset.lastcheckedtime) %].</p>
|
||||
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(jobset.fetcherrormsg || jobset.errormsg) %]</pre></div></div>
|
||||
[% END %]
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,7 +10,7 @@
|
||||
[% PROCESS common.tt %]
|
||||
|
||||
<p>Showing evaluations [% (page - 1) * resultsPerPage + 1 %] - [%
|
||||
(page - 1) * resultsPerPage + evals.size %] out of [% total %].</p>
|
||||
(page - 1) * resultsPerPage + evals.size %] out of [% HTML.escape(total) %].</p>
|
||||
|
||||
[% INCLUDE renderEvals %]
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
|
||||
[% FOREACH metric IN metrics %]
|
||||
|
||||
<h3>Metric: <a [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'metric' metric.name)) %]><tt>[%HTML.escape(metric.name)%]</tt></a></h3>
|
||||
<h3>Metric: <a [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'metric' metric.name)) %]><tt>[% HTML.escape(metric.name) %]</tt></a></h3>
|
||||
|
||||
[% id = "metric-" _ metric.name;
|
||||
id = id.replace('\.', '_');
|
||||
[% id = metricDivId(metric.name);
|
||||
INCLUDE createChart dataUrl=c.uri_for('/job' project.name jobset.name job 'metric' metric.name); %]
|
||||
|
||||
[% END %]
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
[% INCLUDE includeFlot %]
|
||||
|
||||
[% IF !jobExists(jobset, job) %]
|
||||
<div class="alert alert-warning">This job is not a member of the <a
|
||||
href="[%c.uri_for('/jobset' project.name jobset.name
|
||||
'evals')%]">latest evaluation</a> of its jobset. This means it was
|
||||
<div class="alert alert-warning">This job is not a member of the
|
||||
<a [% HTML.attributes(href => c.uri_for('/jobset' project.name jobset.name
|
||||
'evals')) %]>latest evaluation</a> of its jobset. This means it was
|
||||
removed or had an evaluation error.</div>
|
||||
[% END %]
|
||||
|
||||
@@ -46,7 +46,7 @@ removed or had an evaluation error.</div>
|
||||
its success or failure is determined entirely by the result of
|
||||
building its <em>constituent jobs</em>. The table below shows
|
||||
the status of each constituent job for the [%
|
||||
aggregates.keys.size %] most recent builds of the
|
||||
HTML.escape(aggregates.keys.size) %] most recent builds of the
|
||||
aggregate.</div>
|
||||
|
||||
[% aggs = aggregates.keys.nsort.reverse %]
|
||||
@@ -58,7 +58,7 @@ removed or had an evaluation error.</div>
|
||||
<th class="rotate-45">
|
||||
[% agg_ = aggregates.$agg %]
|
||||
<div><span class="[% agg_.build.finished == 0 ? "text-info" : (agg_.build.buildstatus == 0 ? "text-success" : "text-warning") %] override-link">
|
||||
<a href="[% c.uri_for('/build' agg) %]">[% agg %]</a>
|
||||
<a [% HTML.attributes(href => c.uri_for('/build' agg)) %]>[% agg %]</a>
|
||||
</span></div></th>
|
||||
[% END %]
|
||||
</tr>
|
||||
@@ -70,7 +70,7 @@ removed or had an evaluation error.</div>
|
||||
[% FOREACH agg IN aggs %]
|
||||
<td>
|
||||
[% r = aggregates.$agg.constituents.$j; IF r.id %]
|
||||
<a href="[% c.uri_for('/build' r.id) %]">
|
||||
<a [% HTML.attributes(href => c.uri_for('/build' r.id)) %]>
|
||||
[% INCLUDE renderBuildStatusIcon size=16 build=r %]
|
||||
</a>
|
||||
[% END %]
|
||||
@@ -89,8 +89,8 @@ removed or had an evaluation error.</div>
|
||||
|
||||
<div id="tabs-links" class="tab-pane">
|
||||
<ul>
|
||||
<li><a href="[% c.uri_for('/job' project.name jobset.name job 'latest') %]">Latest successful build</a></li>
|
||||
<li><a href="[% c.uri_for('/job' project.name jobset.name job 'latest-finished') %]">Latest successful build from a finished evaluation</a></li>
|
||||
<li><a [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'latest')) %]>Latest successful build</a></li>
|
||||
<li><a [% HTML.attributes(href => c.uri_for('/job' project.name jobset.name job 'latest-finished')) %]>Latest successful build from a finished evaluation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
[% FOREACH eval IN evalIds %]
|
||||
<th class="rotate-45">
|
||||
<div><span>
|
||||
<a href="[% c.uri_for('/eval' eval) %]">[% INCLUDE renderRelativeDate timestamp=evals.$eval.timestamp %]</a>
|
||||
<a [% HTML.attributes(href => c.uri_for('/eval' eval)) %]>[% INCLUDE renderRelativeDate timestamp=evals.$eval.timestamp %]</a>
|
||||
</span></div></th>
|
||||
[% END %]
|
||||
</tr>
|
||||
@@ -22,9 +22,9 @@
|
||||
<tbody>
|
||||
[% FOREACH chan IN channels-%]
|
||||
<tr>
|
||||
<th><span><a href="[% c.uri_for('/channel/custom' project.name jobset.name chan) %]">[% chan %]</a></span></th>
|
||||
<th><span><a [% HTML.attributes(href => c.uri_for('/channel/custom' project.name jobset.name chan)) %]>[% HTML.escape(chan) %]</a></span></th>
|
||||
[% FOREACH eval IN evalIds %]
|
||||
<td>[% r = evals.$eval.builds.$chan; IF r.id %]<a href="[% c.uri_for('/build' r.id) %]">[% INCLUDE renderBuildStatusIcon size=16 build=r %]</a>[% END %]</td>
|
||||
<td>[% r = evals.$eval.builds.$chan; IF r.id %]<a [% HTML.attributes(href => c.uri_for('/build' r.id)) %]>[% INCLUDE renderBuildStatusIcon size=16 build=r %]</a>[% END %]</td>
|
||||
[% END %]
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||
@@ -13,25 +13,23 @@
|
||||
<a class="dropdown-item" href="?compare=-[% 31 * 24 * 60 * 60 %]&full=[% full ? 1 : 0 %]">This jobset <strong>one month</strong> earlier</a>
|
||||
[% IF project.jobsets_rs.count > 1 %]
|
||||
<div class="dropdown-divider"></div>
|
||||
[% FOREACH j IN project.jobsets.sort('name'); IF j.name != jobset.name %]
|
||||
<a class="dropdown-item" href="?compare=[% j.name %]&full=[% full ? 1 : 0 %]">Jobset <tt>[% project.name %]:[% j.name %]</tt></a>
|
||||
[% FOREACH j IN project.jobsets.sort('name'); IF j.name != jobset.name && j.enabled == 1 %]
|
||||
<a class="dropdown-item" href="?compare=[% j.name | uri %]&full=[% full ? 1 : 0 %]">Jobset <tt>[% project.name | html %]:[% j.name | html %]</tt></a>
|
||||
[% END; END %]
|
||||
[% END %]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>This evaluation was performed [% IF eval.flake %]from the flake
|
||||
<tt>[%HTML.escape(eval.flake)%]</tt>[%END%] on [% INCLUDE renderDateTime
|
||||
<tt>[% HTML.escape(eval.flake) %]</tt>[% END %] on [% INCLUDE renderDateTime
|
||||
timestamp=eval.timestamp %]. Fetching the dependencies took [%
|
||||
eval.checkouttime %]s and evaluation took [% eval.evaltime %]s.</p>
|
||||
eval.checkouttime %]s and evaluation took [% HTML.escape(eval.evaltime) %]s.</p>
|
||||
|
||||
[% IF otherEval %]
|
||||
<p>Comparisons are relative to [% INCLUDE renderFullJobsetName
|
||||
project=otherEval.jobset.project.name jobset=otherEval.jobset.name %] evaluation <a href="[%
|
||||
c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
[otherEval.id]) %]">[% otherEval.id %]</a>.</p>
|
||||
[% ELSE %]
|
||||
<div class="alert alert-danger">Couldn't find an evaluation to compare to.</div>
|
||||
project=otherEval.jobset.project.name jobset=otherEval.jobset.name %] evaluation <a [%
|
||||
HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
[otherEval.id])) %]>[% HTML.escape(otherEval.id) %]</a>.</p>
|
||||
[% END %]
|
||||
|
||||
<form>
|
||||
@@ -47,50 +45,50 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#">Actions</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('create_jobset'), [eval.id]) %]">Create a jobset from this evaluation</a>
|
||||
[% IF unfinished.size > 0 %]
|
||||
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('cancel'), [eval.id]) %]">Cancel all scheduled builds</a>
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('create_jobset'), [eval.id])) %]>Create a jobset from this evaluation</a>
|
||||
[% IF totalQueued > 0 %]
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('cancel'), [eval.id])) %]>Cancel all scheduled builds</a>
|
||||
[% END %]
|
||||
[% IF aborted.size > 0 || stillFail.size > 0 || nowFail.size > 0 || failed.size > 0 %]
|
||||
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('restart_failed'), [eval.id]) %]">Restart all failed builds</a>
|
||||
[% IF totalFailed > 0 %]
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('restart_failed'), [eval.id])) %]>Restart all failed builds</a>
|
||||
[% END %]
|
||||
[% IF aborted.size > 0 %]
|
||||
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('restart_aborted'), [eval.id]) %]">Restart all aborted builds</a>
|
||||
[% IF totalAborted > 0 %]
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('restart_aborted'), [eval.id])) %]>Restart all aborted builds</a>
|
||||
[% END %]
|
||||
[% IF unfinished.size > 0 %]
|
||||
<a class="dropdown-item" href="[% c.uri_for(c.controller('JobsetEval').action_for('bump'), [eval.id]) %]">Bump builds to front of queue</a>
|
||||
[% IF totalQueued > 0 %]
|
||||
<a class="dropdown-item" [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('bump'), [eval.id])) %]>Bump builds to front of queue</a>
|
||||
[% END %]
|
||||
</div>
|
||||
</li>
|
||||
[% END %]
|
||||
|
||||
[% IF aborted.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-aborted" data-toggle="tab"><span class="text-warning">Aborted Jobs ([% aborted.size %])</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-aborted" data-toggle="tab"><span class="text-warning">Aborted / Timed out Jobs ([% HTML.escape(aborted.size) %])</span></a></li>
|
||||
[% END %]
|
||||
[% IF nowFail.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-now-fail" data-toggle="tab"><span class="text-warning">Newly Failing Jobs ([% nowFail.size %])</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-now-fail" data-toggle="tab"><span class="text-warning">Newly Failing Jobs ([% HTML.escape(nowFail.size) %])</span></a></li>
|
||||
[% END %]
|
||||
[% IF nowSucceed.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-now-succeed" data-toggle="tab"><span class="text-success">Newly Succeeding Jobs ([% nowSucceed.size %])</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-now-succeed" data-toggle="tab"><span class="text-success">Newly Succeeding Jobs ([% HTML.escape(nowSucceed.size) %])</span></a></li>
|
||||
[% END %]
|
||||
[% IF new.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-new" data-toggle="tab">New Jobs ([% new.size %])</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-new" data-toggle="tab">New Jobs ([% HTML.escape(new.size) %])</a></li>
|
||||
[% END %]
|
||||
[% IF removed.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-removed" data-toggle="tab">Removed Jobs ([% removed.size %])</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-removed" data-toggle="tab">Removed Jobs ([% HTML.escape(removed.size) %])</a></li>
|
||||
[% END %]
|
||||
[% IF stillFail.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-still-fail" data-toggle="tab">Still Failing Jobs ([% stillFail.size %])</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-still-fail" data-toggle="tab">Still Failing Jobs ([% HTML.escape(stillFail.size) %])</a></li>
|
||||
[% END %]
|
||||
[% IF stillSucceed.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-still-succeed" data-toggle="tab">Still Succeeding Jobs ([% stillSucceed.size %])</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-still-succeed" data-toggle="tab">Still Succeeding Jobs ([% HTML.escape(stillSucceed.size) %])</a></li>
|
||||
[% END %]
|
||||
[% IF unfinished.size > 0 %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-unfinished" data-toggle="tab">Queued Jobs ([% unfinished.size %])</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-unfinished" data-toggle="tab">Queued Jobs ([% HTML.escape(unfinished.size) %])</a></li>
|
||||
[% END %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-inputs" data-toggle="tab">Inputs</a></li>
|
||||
|
||||
[% IF eval.evaluationerror.errormsg %]
|
||||
[% IF eval.evaluationerror.has_error %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation Errors</span></a></li>
|
||||
[% END %]
|
||||
</ul>
|
||||
@@ -101,20 +99,13 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
[% INCLUDE renderBuildListBody builds=builds.slice(0, (size > max ? max : size) - 1)
|
||||
hideProjectName=1 hideJobsetName=1 busy=0 %]
|
||||
[% IF size > max; params = c.req.params; params.full = 1 %]
|
||||
<tr><td class="centered" colspan="6"><a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) %][% tabname %]"><em>([% size - max %] more builds omitted)</em></a></td></tr>
|
||||
<tr><td class="centered" colspan="6"><a [% HTML.attributes(href => c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) _ tabname) %]><em>([% size - max %] more builds omitted)</em></a></td></tr>
|
||||
[% END %]
|
||||
[% INCLUDE renderBuildListFooter %]
|
||||
[% END %]
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
[% IF eval.evaluationerror.errormsg %]
|
||||
<div id="tabs-errors" class="tab-pane">
|
||||
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(eval.evaluationerror.errortime || eval.timestamp) %].</p>
|
||||
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(eval.evaluationerror.errormsg) %]</pre></div></div>
|
||||
</div>
|
||||
[% END %]
|
||||
|
||||
<div id="tabs-aborted" class="tab-pane">
|
||||
[% INCLUDE renderSome builds=aborted tabname="#tabs-aborted" %]
|
||||
</div>
|
||||
@@ -141,11 +132,11 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
[% FOREACH j IN removed.slice(0,(size > max ? max : size) - 1) %]
|
||||
<tr>
|
||||
<td>[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j.job %]</td>
|
||||
<td><tt>[% j.system %]</tt></td>
|
||||
<td><tt>[% j.system | html %]</tt></td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% IF size > max; params = c.req.params; params.full = 1 %]
|
||||
<tr><td class="centered" colspan="2"><a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) %]#tabs-removed"><em>([% size - max %] more jobs omitted)</em></a></td></tr>
|
||||
<tr><td class="centered" colspan="2"><a [% HTML.attributes(c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) _ "#tabs-removed") %]><em>([% size - max %] more jobs omitted)</em></a></td></tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -172,10 +163,9 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
[% END %]
|
||||
</div>
|
||||
|
||||
[% IF eval.evaluationerror.errormsg %]
|
||||
[% IF eval.evaluationerror.has_error %]
|
||||
<div id="tabs-errors" class="tab-pane">
|
||||
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(eval.evaluationerror.errortime || eval.timestamp) %].</p>
|
||||
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(eval.evaluationerror.errormsg) %]</pre></div></div>
|
||||
<iframe src="[% c.uri_for(c.controller('JobsetEval').action_for('errors'), [eval.id], params) %]" loading="lazy" frameBorder="0" width="100%"></iframe>
|
||||
</div>
|
||||
[% END %]
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
[% ELSE %]
|
||||
|
||||
[% IF nrJobs > jobs.size %]
|
||||
<div class="alert alert-info">Showing the first [% jobs.size %] jobs. <a href="javascript:setFilter('filter=%')">Show all [% nrJobs %] jobs...</a></div>
|
||||
<div class="alert alert-info">Showing the first [% HTML.escape(jobs.size) %] jobs. <a href="javascript:setFilter('filter=%')">Show all [% HTML.escape(nrJobs) %] jobs...</a></div>
|
||||
[% END %]
|
||||
|
||||
[% evalIds = evals.keys.nsort.reverse %]
|
||||
@@ -52,7 +52,7 @@
|
||||
[% FOREACH eval IN evalIds %]
|
||||
<th class="rotate-45">
|
||||
<div><span>
|
||||
<a href="[% c.uri_for('/eval' eval) %]">[% INCLUDE renderRelativeDate timestamp=evals.$eval.timestamp %]</a>
|
||||
<a [% HTML.attributes(href => c.uri_for('/eval' eval)) %]>[% INCLUDE renderRelativeDate timestamp=evals.$eval.timestamp %]</a>
|
||||
</span></div></th>
|
||||
[% END %]
|
||||
</tr>
|
||||
@@ -62,7 +62,7 @@
|
||||
<tr>
|
||||
<th><span [% IF inactiveJobs.$j %]class="muted override-link"[% END %]>[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]</span></th>
|
||||
[% FOREACH eval IN evalIds %]
|
||||
<td>[% r = evals.$eval.builds.$j; IF r.id %]<a href="[% c.uri_for('/build' r.id) %]">[% INCLUDE renderBuildStatusIcon size=16 build=r %]</a>[% END %]</td>
|
||||
<td>[% r = evals.$eval.builds.$j; IF r.id %]<a [% HTML.attributes(href => c.uri_for('/build' r.id)) %]>[% INCLUDE renderBuildStatusIcon size=16 build=r %]</a>[% END %]</td>
|
||||
[% END %]
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
|
||||
[% BLOCK renderJobsetInput %]
|
||||
<tr class="input [% extraClass %]" [% IF id %]id="[% id %]"[% END %]>
|
||||
<tr class="input [% extraClass %]" [% IF id %][% HTML.attributes(id => id) %][% END %]>
|
||||
<td>
|
||||
<tt>[% HTML.escape(input.name) %]</tt>
|
||||
</td>
|
||||
<td>
|
||||
[% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %]
|
||||
</td>
|
||||
<td class="inputalts" id="[% baseName %]">
|
||||
<td class="inputalts" [% HTML.attributes(id => baseName) %]>
|
||||
[% FOREACH alt IN input.search_related('jobsetinputalts', {}, { order_by => 'altnr' }) %]
|
||||
<tt class="inputalt">
|
||||
[% IF input.type == "string" %]
|
||||
@@ -61,7 +61,7 @@
|
||||
[% END %]
|
||||
|
||||
<li class="nav-item"><a class="nav-link active" href="#tabs-evaluations" data-toggle="tab">Evaluations</a></li>
|
||||
[% IF jobset.errormsg || jobset.fetcherrormsg %]
|
||||
[% IF jobset.has_error || jobset.fetcherrormsg %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation Errors</span></a></li>
|
||||
[% END %]
|
||||
<li class="nav-item"><a class="nav-link" href="#tabs-jobs" data-toggle="tab">Jobs</a></li>
|
||||
@@ -79,7 +79,7 @@
|
||||
<th>Last checked:</th>
|
||||
<td>
|
||||
[% IF jobset.lastcheckedtime %]
|
||||
[% INCLUDE renderDateTime timestamp = jobset.lastcheckedtime %], [% IF jobset.errormsg || jobset.fetcherrormsg %]<em class="text-warning">with errors!</em>[% ELSE %]<em>no errors</em>[% END %]
|
||||
[% INCLUDE renderDateTime timestamp = jobset.lastcheckedtime %], [% IF jobset.has_error || jobset.fetcherrormsg %]<em class="text-warning">with errors!</em>[% ELSE %]<em>no errors</em>[% END %]
|
||||
[% ELSE %]
|
||||
<em>never</em>
|
||||
[% END %]
|
||||
@@ -117,10 +117,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
[% IF jobset.errormsg || jobset.fetcherrormsg %]
|
||||
[% IF jobset.has_error || jobset.fetcherrormsg %]
|
||||
<div id="tabs-errors" class="tab-pane">
|
||||
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(jobset.errortime || jobset.lastcheckedtime) %].</p>
|
||||
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(jobset.fetcherrormsg || jobset.errormsg) %]</pre></div></div>
|
||||
<iframe src="[% c.uri_for('/jobset' project.name jobset.name "errors") %]" loading="lazy" frameBorder="0" width="100%"></iframe>
|
||||
</div>
|
||||
[% END %]
|
||||
|
||||
@@ -154,11 +153,11 @@
|
||||
[% END %]
|
||||
<tr>
|
||||
<th>Check interval:</th>
|
||||
<td>[% jobset.checkinterval || "<em>disabled</em>" %]</td>
|
||||
<td>[% HTML.escape(jobset.checkinterval) || "<em>disabled</em>" %]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Scheduling shares:</th>
|
||||
<td>[% jobset.schedulingshares %] [% IF totalShares %] ([% f = format("%.2f"); f(jobset.schedulingshares / totalShares * 100) %]% out of [% totalShares %] shares)[% END %]</td>
|
||||
<td>[% HTML.escape(jobset.schedulingshares) %] [% IF totalShares %] ([% f = format("%.2f"); f(jobset.schedulingshares / totalShares * 100) %]% out of [% HTML.escape(totalShares) %] shares)[% END %]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Enable Dynamic RunCommand Hooks:</th>
|
||||
@@ -176,7 +175,7 @@
|
||||
[% END %]
|
||||
<tr>
|
||||
<th>Number of evaluations to keep:</th>
|
||||
<td>[% jobset.keepnr %]</td>
|
||||
<td>[% HTML.escape(jobset.keepnr) %]</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -189,7 +188,7 @@
|
||||
|
||||
<div id="tabs-links" class="tab-pane">
|
||||
<ul>
|
||||
<li><a href="[% c.uri_for(c.controller('Jobset').action_for('latest_eval'), c.req.captures) %]">Latest finished evaluation</a></li>
|
||||
<li><a [% HTML.attributes(href => c.uri_for(c.controller('Jobset').action_for('latest_eval'), c.req.captures)) %]>Latest finished evaluation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -205,6 +204,7 @@
|
||||
if (!c) return;
|
||||
requestJSON({
|
||||
url: "[% HTML.escape(c.uri_for('/api/push', { jobsets = project.name _ ':' _ jobset.name, force = "1" })) %]",
|
||||
type: 'POST',
|
||||
success: function(data) {
|
||||
bootbox.alert("The jobset has been scheduled for evaluation.");
|
||||
}
|
||||
|
||||
@@ -10,31 +10,7 @@
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-3.4.1.min.js") %]"></script>
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-ui-1.10.4.min.js") %]"></script>
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/moment/moment-2.24.0.min.js") %]"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link href="[% c.uri_for("/static/fontawesome/css/all.css") %]" rel="stylesheet" />
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/popper.min.js") %]"></script>
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/bootstrap/js/bootstrap.min.js") %]"></script>
|
||||
<link href="[% c.uri_for("/static/bootstrap/css/bootstrap.min.css") %]" rel="stylesheet" />
|
||||
|
||||
<!-- hydra.css may need to be moved to before boostrap to make the @media rule work. -->
|
||||
<link rel="stylesheet" href="[% c.uri_for("/static/css/hydra.css") %]" type="text/css" />
|
||||
<link rel="stylesheet" href="[% c.uri_for("/static/css/rotated-th.css") %]" type="text/css" />
|
||||
|
||||
<style>
|
||||
.popover { max-width: 40%; }
|
||||
</style>
|
||||
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/bootbox.min.js") %]"></script>
|
||||
|
||||
<link rel="stylesheet" href="[% c.uri_for("/static/css/tree.css") %]" type="text/css" />
|
||||
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/common.js") %]"></script>
|
||||
[% INCLUDE style.tt %]
|
||||
|
||||
[% IF c.config.enable_google_login %]
|
||||
<meta name="google-signin-client_id" content="[% c.config.google_client_id %]">
|
||||
@@ -48,7 +24,7 @@
|
||||
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="[% c.uri_for(c.controller('Root').action_for('index')) %]">
|
||||
<a class="navbar-brand" [% HTML.attributes(href => c.uri_for(c.controller('Root').action_for('index'))) %]>
|
||||
[% IF logo == "" %]
|
||||
Hydra
|
||||
[% ELSE %]
|
||||
@@ -93,7 +69,7 @@
|
||||
<footer class="navbar">
|
||||
<hr />
|
||||
<small>
|
||||
<em><a href="http://nixos.org/hydra" target="_blank">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em>
|
||||
<em><a href="http://nixos.org/hydra" target="_blank" class="squiggle">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %] and [% HTML.escape(nixEvalJobsVersion) %]).</em>
|
||||
[% IF c.user_exists %]
|
||||
You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>
|
||||
[%- IF c.user.type == 'google' %] via Google[% END %].
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
[% ELSE %]
|
||||
is
|
||||
[% END %]
|
||||
the build log of derivation <tt>[% IF step; step.drvpath; ELSE; build.drvpath; END %]</tt>.
|
||||
the build log (<a [% HTML.attributes(href => step ? c.uri_for('/build' build.id 'nixlog' step.stepnr, 'raw')
|
||||
: c.uri_for('/build' build.id 'log', 'raw')) %]>raw</a>) of derivation <tt>[% IF step; step.drvpath; ELSE; build.drvpath; END %]</tt>.
|
||||
[% IF step && step.machine %]
|
||||
It was built on <tt>[% step.machine %]</tt>.
|
||||
It was built on <tt>[% step.machine | html %]</tt>.
|
||||
[% END %]
|
||||
[% IF tail %]
|
||||
The <a href="[% step ? c.uri_for('/build' build.id 'nixlog' step.stepnr)
|
||||
: c.uri_for('/build' build.id 'log') %]">full log</a> is also available.
|
||||
The <a [% HTML.attributes(href => step ? c.uri_for('/build' build.id 'nixlog' step.stepnr)
|
||||
: c.uri_for('/build' build.id 'log')) %]>full log</a> is also available.
|
||||
[% END %]
|
||||
</p>
|
||||
|
||||
@@ -36,7 +37,7 @@
|
||||
[% IF tail %]
|
||||
/* The server may give us a full log (e.g. if the log is in
|
||||
S3). So extract the last lines. */
|
||||
log_data = log_data.split("\n").slice(-[%tail%]).join("\n");
|
||||
log_data = log_data.split("\n").slice(-[% HTML.escape(tail) %]).join("\n");
|
||||
[% END %]
|
||||
|
||||
$("#contents").text(log_data);
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job</th>
|
||||
<th>System</th>
|
||||
<th>Build</th>
|
||||
<th>Step</th>
|
||||
<th>What</th>
|
||||
<th>Status</th>
|
||||
<th>Since</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -17,12 +17,48 @@
|
||||
[% name = m.key ? stripSSHUser(m.key) : "localhost" %]
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="6">
|
||||
<th colspan="7">
|
||||
<tt [% IF m.value.disabled %]style="text-decoration: line-through;"[% END %]>[% INCLUDE renderMachineName machine=m.key %]</tt>
|
||||
[% IF m.value.systemTypes %]
|
||||
[% IF m.value.primarySystemType %]
|
||||
<span class="muted" style="font-weight: normal;">
|
||||
([% comma=0; FOREACH system IN m.value.systemTypes %][% IF comma; %], [% ELSE; comma = 1; END %]<tt>[% system %]</tt>[% END %])
|
||||
(<tt>[% m.value.primarySystemType | html %]</tt>)
|
||||
</span>
|
||||
|
||||
[% WRAPPER makePopover title="Details" classes="btn-secondary btn-sm" %]
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><b>System types: </b>[% comma=0; FOREACH system IN m.value.systemTypes %][% IF comma; %], [% ELSE; comma = 1; END %]<tt>[% system | html%]</tt>[% END %]</li>
|
||||
<li><b>Supported Features: </b>[% comma=0; FOREACH feat IN m.value.supportedFeatures %][% IF comma; %], [% ELSE; comma = 1; END %]<tt>[% feat| html %]</tt>[% END %]</li>
|
||||
<li><b>Mandatory Features: </b>[% comma=0; FOREACH feat IN m.value.mandatoryFeatures %][% IF comma; %], [% ELSE; comma = 1; END %]<tt>[% feat| html %]</tt>[% END %]</li>
|
||||
<li><b>Capacity: </b>[% INCLUDE renderYesNo value=m.value.hasCapacity %] <b>Static: </b>[% INCLUDE renderYesNo value=m.value.hasStaticCapacity %] <b>Dynamic: </b>[% INCLUDE renderYesNo value=m.value.hasDynamicCapacity %]</li>
|
||||
<li><b>Scheduling Score: </b>[% HTML.escape(m.value.score) %]</li>
|
||||
<li><b>Load: </b><tt>[% pretty_load(m.value.stats.load1) | html %]</tt> <tt>[% pretty_load(m.value.stats.load5) | html %]</tt> <tt>[% pretty_load(m.value.stats.load15) | html %]</tt></li>
|
||||
<li><b>Memory: </b><tt>[% human_bytes(m.value.stats.memUsage) | html %]</tt> of <tt>[% human_bytes(m.value.memTotal) | html %]</tt> used (<tt>[% human_bytes(m.value.memTotal - m.value.stats.memUsage) | html %]</tt> free)</li>
|
||||
[% pressure = m.value.stats.pressure %]
|
||||
[% MACRO render_pressure(title, pressure) BLOCK %]
|
||||
[% IF pressure %]
|
||||
<tr><td><b>[% HTML.escape(title) %]:</b></td><td><tt>[% pretty_percent(pressure.avg10) %]%</tt></td><td><td><tt>[% pretty_percent(pressure.avg60) %]%</tt></td><td><td><tt>[% pretty_percent(pressure.avg300) %]%</tt></td><td>
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% IF pressure %]
|
||||
<li><b>Pressure: </b>
|
||||
<table class="pressureTable">
|
||||
[% render_pressure('Some CPU', pressure.cpuSome) %]
|
||||
[% render_pressure('Some IO', pressure.ioSome) %]
|
||||
[% render_pressure('Full IO', pressure.ioFull) %]
|
||||
[% render_pressure('Full IRQ', pressure.irqFull) %]
|
||||
[% render_pressure('Some Memory', pressure.memSome) %]
|
||||
[% render_pressure('Full Memory', pressure.memFull) %]
|
||||
</table>
|
||||
</li>
|
||||
[% END %]
|
||||
</ul>
|
||||
[% END %]
|
||||
[% ELSE %]
|
||||
[% IF m.value.systemTypes %]
|
||||
<span class="muted" style="font-weight: normal;">
|
||||
([% comma=0; FOREACH system IN m.value.systemTypes %][% IF comma; %], [% ELSE; comma = 1; END %]<tt>[% system | html %]</tt>[% END %])
|
||||
</span>
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% IF m.value.nrStepsDone %]
|
||||
<span class="muted" style="font-weight: normal;">
|
||||
@@ -40,10 +76,10 @@
|
||||
[% idle = 0 %]
|
||||
<tr>
|
||||
<td><tt>[% INCLUDE renderFullJobName project=step.project jobset=step.jobset job=step.job %]</tt></td>
|
||||
<td><tt>[% step.system %]</tt></td>
|
||||
<td><a href="[% c.uri_for('/build' step.build) %]">[% step.build %]</a></td>
|
||||
<td>[% IF step.busy >= 30 %]<a class="row-link" href="[% c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail') %]">[% step.stepnr %]</a>[% ELSE; step.stepnr; END %]</td>
|
||||
<td><tt>[% step.drvpath.match('-(.*)').0 %]</tt></td>
|
||||
<td><a [% HTML.attributes(href => c.uri_for('/build' step.build)) %]>[% HTML.escape(step.build) %]</a></td>
|
||||
<td>[% IF step.busy >= 30 %]<a class="row-link" [% HTML.attributes(href => c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail')) %]>[% HTML.escape(step.stepnr) %]</a>[% ELSE; HTML.escape(step.stepnr); END %]</td>
|
||||
<td><tt>[% step.drvpath.match('-(.*)').0 | html %]</tt></td>
|
||||
<td>[% INCLUDE renderBusyStatus %]</td>
|
||||
<td style="width: 10em">[% INCLUDE renderDuration duration = curTime - step.starttime %] </td>
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
[% FOREACH m IN machines %]
|
||||
<tr>
|
||||
<td><input type="checkbox" name="enabled" [% IF m.value.maxJobs > 0 %]CHECKED[% END %] disabled="true" /></td>
|
||||
<td>[% m.key %]</a></td>
|
||||
<td>[% m.value.maxJobs %]</td>
|
||||
<td>[% m.value.speedFactor %]</td>
|
||||
<td>[% HTML.escape(m.key) %]</a></td>
|
||||
<td>[% HTML.escape(m.value.maxJobs) %]</td>
|
||||
<td>[% HTML.escape(m.value.speedFactor) %]</td>
|
||||
<td>
|
||||
[% comma=0; FOREACH system IN m.value.systemTypes %][% IF comma; %], [% ELSE; comma = 1; END; system; END %]
|
||||
[% comma=0; FOREACH system IN m.value.systemTypes %][% IF comma; %], [% ELSE; comma = 1; END; HTML.escape(system); END %]
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[% FOREACH i IN newsItems %]
|
||||
<div class="news-item">
|
||||
[% contents = String.new(i.contents) %]
|
||||
<h4 class="alert-heading">[% INCLUDE renderDateTime timestamp=i.createtime %] by [% i.author.fullname %]</h4>
|
||||
<h4 class="alert-heading">[% INCLUDE renderDateTime timestamp=i.createtime %] by [% HTML.escape(i.author.fullname) %]</h4>
|
||||
[% contents.replace('\n','<br />\n') %]
|
||||
</div>
|
||||
[% END %]
|
||||
@@ -65,7 +65,7 @@
|
||||
[% ELSE %]
|
||||
|
||||
<div class="alert alert-warning">Hydra has no projects yet. Please
|
||||
<a href="[% c.uri_for(c.controller('Project').action_for('create')) %]">create a project</a>.</div>
|
||||
<a [% HTML.attributes(href => c.uri_for(c.controller('Project').action_for('create'))) %]>create a project</a>.</div>
|
||||
|
||||
[% END %]
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
[% BLOCK renderProductLinks %]
|
||||
<tr>
|
||||
<th>URL:</th>
|
||||
<td><a href="[% uri %]"><tt>[% uri %]</tt></a></td>
|
||||
<td><a [% HTML.attributes(href => uri) %]><tt>[% uri | html %]</tt></a></td>
|
||||
</tr>
|
||||
[% IF latestRoot %]
|
||||
<tr>
|
||||
<th>Links to latest:</th>
|
||||
<td>
|
||||
[% uri2 = "${c.uri_for(latestRoot.join('/') 'download-by-type' product.type product.subtype)}" %]
|
||||
<a href="[% uri2 %]"><tt>[% uri2 %]</tt></a>
|
||||
<a [% HTML.attributes(href => uri2) %]><tt>[% uri2 | html %]</tt></a>
|
||||
<br />
|
||||
[% uri2 = "${c.uri_for(latestRoot.join('/') 'download' product.productnr)}" %]
|
||||
<a href="[% uri2 %]"><tt>[% uri2 %]</tt></a>
|
||||
<a [% HTML.attributes(href => uri2) %]><tt>[% uri2 | html %]</tt></a>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
@@ -49,7 +49,7 @@
|
||||
Error
|
||||
</td>
|
||||
<td>
|
||||
<a href="[% contents %]">
|
||||
<a [% HTML.attributes(href => contents) %]>
|
||||
Failed build produced output. Click here to inspect the output.
|
||||
</a>
|
||||
</td>
|
||||
@@ -58,9 +58,9 @@
|
||||
<p>If you have Nix installed on your machine, this failed build output and
|
||||
all its dependencies can be unpacked into your local Nix store by doing:</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>curl [% uri %] | gunzip | nix-store --import</code></div></div>
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>curl [% HTML.escape(uri) %] | gunzip | nix-store --import</code></div></div>
|
||||
|
||||
<p>The build output can then be found in the path <tt>[% product.path %]</tt>.</p>
|
||||
<p>The build output can then be found in the path <tt>[% product.path | html %]</tt>.</p>
|
||||
[% END %]
|
||||
</td>
|
||||
</tr>
|
||||
@@ -74,17 +74,17 @@
|
||||
Nix package
|
||||
</td>
|
||||
<td>
|
||||
<tt>[% HTML.escape(build.nixname) %]</tt>
|
||||
<tt>[% build.nixname | html %]</tt>
|
||||
</td>
|
||||
<td>
|
||||
[% WRAPPER makePopover title="Help" classes="btn-secondary btn-sm"
|
||||
%] <p>You can install this package using the Nix package
|
||||
manager from the command-line:</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>nix-env -i [%HTML.escape(product.path)%][% IF binaryCachePublicUri %] --option binary-caches [% HTML.escape(binaryCachePublicUri) %][% END %]</code></div></div>
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>nix-env -i [% HTML.escape(product.path) %][% IF binaryCachePublicUri %] --option binary-caches [% HTML.escape(binaryCachePublicUri) %][% END %]</code></div></div>
|
||||
[% END %]
|
||||
[% IF localStore %]
|
||||
<a class="btn btn-secondary btn-sm" href="[% contents %]">Contents</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => contents) %]>Contents</a>
|
||||
[% END %]
|
||||
</td>
|
||||
</tr>
|
||||
@@ -100,8 +100,8 @@
|
||||
[% filename = build.nixname _ (product.subtype ? "-" _ product.subtype : "") _ ".closure.gz" %]
|
||||
[% uri = c.uri_for('/build' build.id 'nix' 'closure' filename ) %]
|
||||
|
||||
<a href="[% uri %]">
|
||||
<tt>[% product.path %]</tt>
|
||||
<a [% HTML.attributes(href => uri) %]>
|
||||
<tt>[% product.path | html %]</tt>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@@ -110,16 +110,16 @@
|
||||
all its dependencies can be unpacked into your local Nix
|
||||
store by doing:</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>gunzip < [% filename %] | nix-store --import</code></div></div>
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>gunzip < [% HTML.escape(filename) %] | nix-store --import</code></div></div>
|
||||
|
||||
<p>or to download and unpack in one command:</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>curl [% uri %] | gunzip | nix-store --import</code></div></div>
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>curl [% HTML.escape(uri) %] | gunzip | nix-store --import</code></div></div>
|
||||
|
||||
<p>The package can then be found in the path <tt>[%
|
||||
product.path %]</tt>. You’ll probably also want to do</p>
|
||||
product.path | html %]</tt>. You’ll probably also want to do</p>
|
||||
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>nix-env -i [% product.path %]</code></div></div>
|
||||
<div class="card bg-light"><div class="card-body p-2"><code><span class="shell-prompt">$ </span>nix-env -i [% HTML.escape(product.path) %]</code></div></div>
|
||||
|
||||
<p>to actually install the package in your Nix user environment.</p>
|
||||
|
||||
@@ -174,16 +174,16 @@
|
||||
</td>
|
||||
<td>
|
||||
Channel expression tarball
|
||||
[% IF product.subtype != "-" %]for <tt>[% product.subtype %]</tt>[% END %]
|
||||
[% IF product.subtype != "-" %]for <tt>[% product.subtype | html %]</tt>[% END %]
|
||||
</td>
|
||||
[% ELSE %]
|
||||
<td>File</td>
|
||||
<td>[% product.subtype %]</td>
|
||||
<td>[% HTML.escape(product.subtype) %]</td>
|
||||
[% END %]
|
||||
[% END %]
|
||||
<td>
|
||||
<a href="[% uri %]">
|
||||
<tt>[% product.name %]</tt>
|
||||
<a [% HTML.attributes(href => uri) %]>
|
||||
<tt>[% product.name | html %]</tt>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
@@ -191,12 +191,12 @@
|
||||
<table class="info-table">
|
||||
[% INCLUDE renderProductLinks %]
|
||||
<tr><th>File size:</th><td>[% product.filesize %] bytes ([% mibs(product.filesize / (1024 * 1024)) %] MiB)</td></tr>
|
||||
<tr><th>SHA-256 hash:</th><td><tt>[% product.sha256hash %]</tt></td></tr>
|
||||
<tr><th>Full path:</th><td><tt>[% product.path %]</tt></td></tr>
|
||||
<tr><th>SHA-256 hash:</th><td><tt>[% product.sha256hash | html %]</tt></td></tr>
|
||||
<tr><th>Full path:</th><td><tt>[% product.path | html %]</tt></td></tr>
|
||||
</table>
|
||||
[% END %]
|
||||
[% IF localStore %]
|
||||
<a class="btn btn-secondary btn-sm" href="[% contents %]">Contents</a>
|
||||
<a class="btn btn-secondary btn-sm" [% HTML.attributes(href => contents) %]>Contents</a>
|
||||
[% END %]
|
||||
</td>
|
||||
</tr>
|
||||
@@ -211,15 +211,15 @@
|
||||
[% CASE "coverage" %]
|
||||
<td>Code coverage</td>
|
||||
<td>
|
||||
<a href="[% uri %]">
|
||||
<a [% HTML.attributes(href => uri) %]>
|
||||
Analysis report
|
||||
</a>
|
||||
</td>
|
||||
[% CASE DEFAULT %]
|
||||
<td>Report</td>
|
||||
<td>
|
||||
<a href="[% uri %]">
|
||||
<tt>[% product.subtype %]</tt>
|
||||
<a [% HTML.attributes(href => uri) %]>
|
||||
<tt>[% product.subtype | html %]</tt>
|
||||
</a>
|
||||
</td>
|
||||
[% END %]
|
||||
@@ -240,7 +240,7 @@
|
||||
Documentation
|
||||
</td>
|
||||
<td>
|
||||
<a href="[% uri %]">
|
||||
<a [% HTML.attributes(href => uri) %]>
|
||||
[% SWITCH product.subtype %]
|
||||
[% CASE "readme" %]
|
||||
Read Me!
|
||||
@@ -249,7 +249,7 @@
|
||||
[% CASE "release-notes" %]
|
||||
Release notes
|
||||
[% CASE DEFAULT %]
|
||||
[% product.subtype %]
|
||||
[% HTML.escape(product.subtype) %]
|
||||
[% END %]
|
||||
</a>
|
||||
</td>
|
||||
@@ -266,12 +266,12 @@
|
||||
|
||||
<tr class="product">
|
||||
<td>
|
||||
<tt>[% product.type %]</tt>
|
||||
<tt>[% product.type | html %]</tt>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
[% product %]
|
||||
[% HTML.escape(product) %]
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
[% FOREACH s IN systems %]
|
||||
<tr>
|
||||
<td><tt>[% HTML.escape(s.system) %]</tt></td>
|
||||
<td>[% s.c %]</td>
|
||||
<td>[% HTML.escape(s.c) %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tdata>
|
||||
|
||||
@@ -7,7 +7,7 @@ main() {
|
||||
|
||||
set -e
|
||||
|
||||
tmpDir=${TMPDIR:-/tmp}/build-[% build.id +%]
|
||||
tmpDir=$(realpath "${TMPDIR:-/tmp}")/build-[% build.id +%]
|
||||
declare -a args extraArgs
|
||||
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
is
|
||||
[% END %]
|
||||
the output of a RunCommand execution of the command <tt>[% HTML.escape(runcommandlog.command) %]</tt>
|
||||
on <a href="[% c.uri_for('/build', build.id) %]">Build [% build.id %]</a>.
|
||||
on <a [% HTML.attributes(href => c.uri_for('/build', build.id)) %]>Build [% HTML.escape(build.id) %]</a>.
|
||||
[% IF tail %]
|
||||
The <a href="[% c.uri_for('/build', build.id, 'runcommandlog', runcommandlog.uuid) %]">full log</a> is also available.
|
||||
The <a [% HTML.attributes(href => c.uri_for('/build', build.id, 'runcommandlog', runcommandlog.uuid)) %]>full log</a> is also available.
|
||||
[% END %]
|
||||
</p>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
[% IF tail %]
|
||||
/* The server may give us a full log (e.g. if the log is in
|
||||
S3). So extract the last lines. */
|
||||
log_data = log_data.split("\n").slice(-[%tail%]).join("\n");
|
||||
log_data = log_data.split("\n").slice(-[% HTML.escape(tail) %]).join("\n");
|
||||
[% END %]
|
||||
|
||||
$("#contents").text(log_data);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
[% IF builds.size > 0 %]
|
||||
|
||||
<p>The following builds match your query:[% IF builds.size > limit %] <span class="text-warning">(first [% limit %] results only)</span>[% END %]</p>
|
||||
<p>The following builds match your query:[% IF builds.size > limit %] <span class="text-warning">(first [% HTML.escape(limit) %] results only)</span>[% END %]</p>
|
||||
|
||||
[% INCLUDE renderBuildList %]
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
[% IF jobs.size > 0; matched = 1 %]
|
||||
|
||||
<p>The following jobs match your query:[% IF jobs.size > limit %] <span class="text-warning">(first [% limit %] results only)</span>[% END %]</p>
|
||||
<p>The following jobs match your query:[% IF jobs.size > limit %] <span class="text-warning">(first [% HTML.escape(limit) %] results only)</span>[% END %]</p>
|
||||
|
||||
<table class="table table-striped table-condensed clickable-rows">
|
||||
<thead>
|
||||
|
||||
@@ -171,12 +171,30 @@ body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a.squiggle:hover {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg id='squiggle-link' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:ev='http://www.w3.org/2001/xml-events' viewBox='0 0 10 18'%3E%3Cstyle type='text/css'%3E.squiggle{animation:shift .5s linear infinite;}@keyframes shift {from {transform:translateX(-10px);}to {transform:translateX(0);}}%3C/style%3E%3Cpath fill='none' stroke='%230056b3' stroke-width='0.65' class='squiggle' d='M0,17.5 c 2.5,0,2.5,-1.5,5,-1.5 s 2.5,1.5,5,1.5 c 2.5,0,2.5,-1.5,5,-1.5 s 2.5,1.5,5,1.5' /%3E%3C/svg%3E");
|
||||
background-position: 0 100%;
|
||||
background-size: auto 24px;
|
||||
background-repeat: repeat;
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
table.pressureTable {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
table.pressureTable td {
|
||||
padding: 0 .4em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* Prevent some flickering */
|
||||
html {
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
body, div.popover {
|
||||
body, div.popover, div.popover-body {
|
||||
background-color: #1f1f1f;
|
||||
color: #fafafa !important;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,12 @@ $(document).ready(function() {
|
||||
el.addClass("is-local");
|
||||
}
|
||||
});
|
||||
|
||||
[...document.getElementsByTagName("iframe")].forEach((element) => {
|
||||
element.contentWindow.addEventListener("DOMContentLoaded", (_) => {
|
||||
element.style.height = element.contentWindow.document.body.scrollHeight + 'px';
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
var tabsLoaded = {};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
[% ELSE %]
|
||||
|
||||
[% INCLUDE renderBuildList builds=resource showSchedulingInfo=1 hideResultInfo=1 busy=1 %]
|
||||
[% INCLUDE renderBuildList builds=resource showSchedulingInfo=1 hideResultInfo=1 busy=1 showStepName=1 %]
|
||||
|
||||
[% END %]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[% PROCESS common.tt %]
|
||||
|
||||
<p>Showing steps [% (page - 1) * resultsPerPage + 1 %] - [% (page - 1)
|
||||
* resultsPerPage + steps.size %] of about [% total %] in
|
||||
* resultsPerPage + steps.size %] of about [% HTML.escape(total) %] in
|
||||
order of descending finish time.</p>
|
||||
|
||||
<table class="table table-striped table-condensed clickable-rows">
|
||||
@@ -24,8 +24,8 @@ order of descending finish time.</p>
|
||||
<td>[% INCLUDE renderBuildStatusIcon buildstatus=step.status size=16 %]</td>
|
||||
<td><tt>[% step.drvpath.match('-(.*).drv').0 %]</tt></td>
|
||||
<td><tt>[% INCLUDE renderFullJobNameOfBuild build=step.build %]</tt></td>
|
||||
<td><a href="[% c.uri_for('/build' step.build.id) %]">[% step.build.id %]</a></td>
|
||||
<td><a class="row-link" href="[% c.uri_for('/build' step.build.id 'nixlog' step.stepnr 'tail') %]">[% step.stepnr %]</a></td>
|
||||
<td><a [% HTML.attributes(href => c.uri_for('/build' step.build.id)) %]>[% HTML.escape(step.build.id) %]</a></td>
|
||||
<td><a class="row-link" [% HTML.attributes(href => c.uri_for('/build' step.build.id 'nixlog' step.stepnr 'tail')) %]>[% HTML.escape(step.stepnr) %]</a></td>
|
||||
<td>[% INCLUDE renderRelativeDate timestamp=step.stoptime %]</td>
|
||||
<td style="width: 10em">[% INCLUDE renderDuration duration = step.stoptime - step.starttime %] </td>
|
||||
<td><tt>[% INCLUDE renderMachineName machine=step.machine %]</tt></td>
|
||||
|
||||
24
src/root/style.tt
Normal file
24
src/root/style.tt
Normal file
@@ -0,0 +1,24 @@
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-3.4.1.min.js") %]"></script>
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-ui-1.10.4.min.js") %]"></script>
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/moment/moment-2.24.0.min.js") %]"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link [% HTML.attributes(href => c.uri_for("/static/fontawesome/css/all.css")) %] rel="stylesheet" />
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/popper.min.js") %]"></script>
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/bootstrap/js/bootstrap.min.js") %]"></script>
|
||||
<link [% HTML.attributes(href => c.uri_for("/static/bootstrap/css/bootstrap.min.css")) %] rel="stylesheet" />
|
||||
|
||||
<!-- hydra.css may need to be moved to before boostrap to make the @media rule work. -->
|
||||
<link rel="stylesheet" [% HTML.attributes(href => c.uri_for("/static/css/hydra.css")) %] type="text/css" />
|
||||
<link rel="stylesheet" [% HTML.attributes(href => c.uri_for("/static/css/rotated-th.css")) %] type="text/css" />
|
||||
|
||||
<style>
|
||||
.popover { max-width: 40%; }
|
||||
</style>
|
||||
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/bootbox.min.js") %]"></script>
|
||||
|
||||
<link rel="stylesheet" [% HTML.attributes(href => c.uri_for("/static/css/tree.css")) %] type="text/css" />
|
||||
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/js/common.js") %]"></script>
|
||||
@@ -1,6 +1,6 @@
|
||||
[% BLOCK makeSubMenu %]
|
||||
<li class="nav-item dropdown" [% IF id; HTML.attributes(id => id); END %] >
|
||||
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown">[% title %]<b class="caret"></b></a>
|
||||
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown">[% HTML.escape(title) %]<b class="caret"></b></a>
|
||||
<div class="dropdown-menu[% IF align == 'right' %] dropdown-menu-right[% END %]">
|
||||
[% content %]
|
||||
</div>
|
||||
@@ -34,6 +34,9 @@
|
||||
[% INCLUDE menuItem
|
||||
uri = c.uri_for(c.controller('Root').action_for('steps'))
|
||||
title = "Latest steps" %]
|
||||
[% INCLUDE menuItem
|
||||
uri = c.uri_for(c.controller('Root').action_for('queue_runner_status'))
|
||||
title = "Queue Runner Status" %]
|
||||
[% END %]
|
||||
|
||||
[% IF project %]
|
||||
@@ -42,7 +45,7 @@
|
||||
<div class="dropdown-divider"></div>
|
||||
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('project'), [project.name]) title = "Overview" %]
|
||||
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('all'), [project.name]) title = "Latest builds" %]
|
||||
[% INCLUDE menuItem uri = c.uri_for('/project' project.name 'channel' 'latest') title = "Channel" %]
|
||||
[% IF localStore %][% INCLUDE menuItem uri = c.uri_for('/project' project.name 'channel' 'latest') title = "Channel" %][% END %]
|
||||
[% END %]
|
||||
[% END %]
|
||||
|
||||
@@ -59,7 +62,7 @@
|
||||
[% INCLUDE menuItem
|
||||
uri = c.uri_for(c.controller('Jobset').action_for('all'), [project.name, jobset.name])
|
||||
title = "Latest builds" %]
|
||||
[% INCLUDE menuItem uri = c.uri_for('/jobset' project.name jobset.name 'channel' 'latest') title = "Channel" %]
|
||||
[% IF localStore %][% INCLUDE menuItem uri = c.uri_for('/jobset' project.name jobset.name 'channel' 'latest') title = "Channel" %][% END %]
|
||||
[% END %]
|
||||
[% END %]
|
||||
|
||||
@@ -73,7 +76,7 @@
|
||||
[% INCLUDE menuItem
|
||||
uri = c.uri_for(c.controller('Job').action_for('all'), [project.name, jobset.name, job])
|
||||
title = "Latest builds" %]
|
||||
[% INCLUDE menuItem uri = c.uri_for('/job' project.name jobset.name job 'channel' 'latest') title = "Channel" %]
|
||||
[% IF localStore %][% INCLUDE menuItem uri = c.uri_for('/job' project.name jobset.name job 'channel' 'latest') title = "Channel" %][% END %]
|
||||
[% END %]
|
||||
[% END %]
|
||||
|
||||
@@ -133,12 +136,14 @@
|
||||
[% ELSE %]
|
||||
[% WRAPPER makeSubMenu title="Sign in" id="sign-in-menu" align="right" %]
|
||||
[% IF c.config.enable_google_login %]
|
||||
<div style="display: none" class="g-signin2" data-onsuccess="onGoogleSignIn" data-theme="dark"></div>
|
||||
<a class="dropdown-item" href="#" id="google-signin">Sign in with Google</a>
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
<div id="g_id_onload" data-client_id="[% c.config.google_client_id %]" data-auto_prompt="false" data-callback="onGoogleSignIn">
|
||||
</div>
|
||||
<div class="g_id_signin" data-type="standard"></div>
|
||||
<div class="dropdown-divider"></div>
|
||||
[% END %]
|
||||
[% IF c.config.github_client_id %]
|
||||
<a class="dropdown-item" href="/github-redirect?after=[% c.req.path %]">Sign in with GitHub</a>
|
||||
<a class="dropdown-item" href="/github-redirect?after=[% c.req.path | uri %]">Sign in with GitHub</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
[% END %]
|
||||
<a class="dropdown-item" href="#hydra-signin" data-toggle="modal">Sign in with a Hydra account</a>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
disabled="disabled"
|
||||
[% END %]
|
||||
[% HTML.attributes(id => "role-${role}", value => role) %] />
|
||||
<label [% HTML.attributes(for => "role-${role}") %]> [% role %]</label><br />
|
||||
<label [% HTML.attributes(for => "role-${role}") %]> [% HTML.escape(role) %]</label><br />
|
||||
[% END %]
|
||||
|
||||
<form>
|
||||
@@ -91,6 +91,7 @@
|
||||
[% INCLUDE roleoption mutable=mutable role="restart-jobs" %]
|
||||
[% INCLUDE roleoption mutable=mutable role="bump-to-front" %]
|
||||
[% INCLUDE roleoption mutable=mutable role="cancel-build" %]
|
||||
[% INCLUDE roleoption mutable=mutable role="eval-jobset" %]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
<tbody>
|
||||
[% FOREACH u IN users %]
|
||||
<tr>
|
||||
<td><a class="row-link" href="[% c.uri_for(c.controller('User').action_for('edit'), [u.username]) %]">[% HTML.escape(u.username) %]</a></td>
|
||||
<td><a class="row-link" [% HTML.attributes(href => c.uri_for(c.controller('User').action_for('edit'), [u.username])) %]>[% HTML.escape(u.username) %]</a></td>
|
||||
<td>[% HTML.escape(u.fullname) %]</td>
|
||||
<td>[% HTML.escape(u.emailaddress) %]</td>
|
||||
<td>[% FOREACH r IN u.userroles %]<i>[% r.role %]</i> [% END %]</td>
|
||||
<td>[% FOREACH r IN u.userroles %]<i>[% HTML.escape(r.role) %]</i> [% END %]</td>
|
||||
<td>[% IF u.emailonerror %]Yes[% ELSE %]No[% END %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a class="btn btn-primary" href="[% c.uri_for(c.controller('Root').action_for('register')) %]">
|
||||
<a class="btn btn-primary" [% HTML.attributes(href => c.uri_for(c.controller('Root').action_for('register'))) %]>
|
||||
<i class="fas fa-plus"></i> Add a new user
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
EXTRA_DIST = \
|
||||
$(distributable_scripts)
|
||||
|
||||
distributable_scripts = \
|
||||
hydra-backfill-ids \
|
||||
hydra-init \
|
||||
hydra-eval-jobset \
|
||||
hydra-server \
|
||||
hydra-update-gc-roots \
|
||||
hydra-s3-backup-collect-garbage \
|
||||
hydra-create-user \
|
||||
hydra-notify \
|
||||
hydra-send-stats \
|
||||
nix-prefetch-git \
|
||||
nix-prefetch-bzr \
|
||||
nix-prefetch-hg
|
||||
|
||||
bin_SCRIPTS = \
|
||||
$(distributable_scripts)
|
||||
@@ -17,6 +17,7 @@ use Hydra::Helper::Nix;
|
||||
use Hydra::Model::DB;
|
||||
use Hydra::Plugin;
|
||||
use Hydra::Schema;
|
||||
use IPC::Run;
|
||||
use JSON::MaybeXS;
|
||||
use Net::Statsd;
|
||||
use Nix::Store;
|
||||
@@ -85,14 +86,14 @@ sub attrsToSQL {
|
||||
# Fetch a store path from 'eval_substituter' if not already present.
|
||||
sub getPath {
|
||||
my ($path) = @_;
|
||||
return 1 if isValidPath($path);
|
||||
return 1 if $MACHINE_LOCAL_STORE->isValidPath($path);
|
||||
|
||||
my $substituter = $config->{eval_substituter};
|
||||
|
||||
system("nix", "--experimental-features", "nix-command", "copy", "--from", $substituter, "--", $path)
|
||||
if defined $substituter;
|
||||
|
||||
return isValidPath($path);
|
||||
return $MACHINE_LOCAL_STORE->isValidPath($path);
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +144,7 @@ sub fetchInputBuild {
|
||||
, version => $version
|
||||
, outputName => $mainOutput->name
|
||||
};
|
||||
if (isValidPath($prevBuild->drvpath)) {
|
||||
if ($MACHINE_LOCAL_STORE->isValidPath($prevBuild->drvpath)) {
|
||||
$result->{drvPath} = $prevBuild->drvpath;
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ sub fetchInputEval {
|
||||
my $out = $build->buildoutputs->find({ name => "out" });
|
||||
next unless defined $out;
|
||||
# FIXME: Should we fail if the path is not valid?
|
||||
next unless isValidPath($out->path);
|
||||
next unless $MACHINE_LOCAL_STORE->isValidPath($out->path);
|
||||
$jobs->{$build->get_column('job')} = $out->path;
|
||||
}
|
||||
|
||||
@@ -357,22 +358,39 @@ sub evalJobs {
|
||||
my @cmd;
|
||||
|
||||
if (defined $flakeRef) {
|
||||
@cmd = ("hydra-eval-jobs",
|
||||
"--flake", $flakeRef,
|
||||
"--gc-roots-dir", getGCRootsDir,
|
||||
"--max-jobs", 1);
|
||||
my $nix_expr =
|
||||
"let " .
|
||||
"flake = builtins.getFlake (toString \"$flakeRef\"); " .
|
||||
"in " .
|
||||
"flake.hydraJobs " .
|
||||
"or flake.checks " .
|
||||
"or (throw \"flake '$flakeRef' does not provide any Hydra jobs or checks\")";
|
||||
|
||||
@cmd = ("nix-eval-jobs",
|
||||
# Disable the eval cache to prevent SQLite database contention.
|
||||
# Since Hydra typically evaluates each revision only once,
|
||||
# parallel workers would compete for database locks without
|
||||
# providing any benefit from caching.
|
||||
"--option", "eval-cache", "false",
|
||||
"--expr", $nix_expr);
|
||||
} else {
|
||||
my $nixExprInput = $inputInfo->{$nixExprInputName}->[0]
|
||||
or die "cannot find the input containing the job expression\n";
|
||||
|
||||
@cmd = ("hydra-eval-jobs",
|
||||
@cmd = ("nix-eval-jobs",
|
||||
"--option", "restrict-eval", "true",
|
||||
"<" . $nixExprInputName . "/" . $nixExprPath . ">",
|
||||
"--gc-roots-dir", getGCRootsDir,
|
||||
"--max-jobs", 1,
|
||||
inputsToArgs($inputInfo));
|
||||
}
|
||||
|
||||
push @cmd, "--no-allow-import-from-derivation" if $config->{allow_import_from_derivation} // "true" ne "true";
|
||||
push @cmd, ("--gc-roots-dir", getGCRootsDir);
|
||||
push @cmd, ("--max-jobs", 1);
|
||||
push @cmd, "--meta";
|
||||
push @cmd, "--constituents";
|
||||
push @cmd, "--force-recurse";
|
||||
push @cmd, ("--option", "allow-import-from-derivation", "false") if ($config->{allow_import_from_derivation} // "false") ne "true";
|
||||
push @cmd, ("--workers", $config->{evaluator_workers} // 1);
|
||||
push @cmd, ("--max-memory-size", $config->{evaluator_max_memory_size} // 4096);
|
||||
|
||||
if (defined $ENV{'HYDRA_DEBUG'}) {
|
||||
sub escape {
|
||||
@@ -384,14 +402,45 @@ sub evalJobs {
|
||||
print STDERR "evaluator: @escaped\n";
|
||||
}
|
||||
|
||||
(my $res, my $jobsJSON, my $stderr) = captureStdoutStderr(21600, @cmd);
|
||||
die "hydra-eval-jobs returned " . ($res & 127 ? "signal $res" : "exit code " . ($res >> 8))
|
||||
. ":\n" . ($stderr ? decode("utf-8", $stderr) : "(no output)\n")
|
||||
if $res;
|
||||
# Unset NIX_PATH for nix-eval-jobs to ensure reproducible evaluations
|
||||
my %env = %ENV;
|
||||
delete $env{'NIX_PATH'};
|
||||
|
||||
print STDERR "$stderr";
|
||||
my $evalProc = IPC::Run::start \@cmd,
|
||||
'>', IPC::Run::new_chunker, \my $out,
|
||||
'2>', \my $err,
|
||||
init => sub { %ENV = %env; };
|
||||
|
||||
return decode_json($jobsJSON);
|
||||
return sub {
|
||||
while (1) {
|
||||
$evalProc->pump;
|
||||
if (!defined $out && !defined $err) {
|
||||
$evalProc->finish;
|
||||
if ($?) {
|
||||
die "nix-eval-jobs returned " . ($? & 127 ? "signal $?" : "exit code " . ($? >> 8)) . "\n";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (defined $err) {
|
||||
print STDERR "$err";
|
||||
undef $err;
|
||||
}
|
||||
|
||||
if (defined $out && $out ne '') {
|
||||
my $job;
|
||||
try {
|
||||
$job = decode_json($out);
|
||||
} catch {
|
||||
warn "nix-eval-jobs sent invalid JSON.\n parse error: $_\n invalid json: $out\n";
|
||||
};
|
||||
undef $out;
|
||||
if (defined $job) {
|
||||
return $job;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -420,7 +469,7 @@ sub checkBuild {
|
||||
my $firstOutputName = $outputNames[0];
|
||||
my $firstOutputPath = $buildInfo->{outputs}->{$firstOutputName};
|
||||
|
||||
my $jobName = $buildInfo->{jobName} or die;
|
||||
my $jobName = $buildInfo->{attr} or die;
|
||||
my $drvPath = $buildInfo->{drvPath} or die;
|
||||
|
||||
my $build;
|
||||
@@ -438,13 +487,17 @@ sub checkBuild {
|
||||
# new build to be scheduled if the meta.maintainers field is
|
||||
# changed?
|
||||
if (defined $prevEval) {
|
||||
my $pathOrDrvConstraint = defined $firstOutputPath
|
||||
? { path => $firstOutputPath }
|
||||
: { drvPath => $drvPath };
|
||||
|
||||
my ($prevBuild) = $prevEval->builds->search(
|
||||
# The "project" and "jobset" constraints are
|
||||
# semantically unnecessary (because they're implied by
|
||||
# the eval), but they give a factor 1000 speedup on
|
||||
# the Nixpkgs jobset with PostgreSQL.
|
||||
{ jobset_id => $jobset->get_column('id'), job => $jobName,
|
||||
name => $firstOutputName, path => $firstOutputPath },
|
||||
name => $firstOutputName, %$pathOrDrvConstraint },
|
||||
{ rows => 1, columns => ['id', 'finished'], join => ['buildoutputs'] });
|
||||
if (defined $prevBuild) {
|
||||
#print STDERR " already scheduled/built as build ", $prevBuild->id, "\n";
|
||||
@@ -470,9 +523,30 @@ sub checkBuild {
|
||||
|
||||
my $time = time();
|
||||
|
||||
sub null {
|
||||
my ($s) = @_;
|
||||
return $s eq "" ? undef : $s;
|
||||
sub getMeta {
|
||||
my ($s, $def) = @_;
|
||||
return ($s || "") eq "" ? $def : $s;
|
||||
}
|
||||
|
||||
sub getMetaStrings {
|
||||
my ($v, $k, $acc) = @_;
|
||||
my $t = ref $v;
|
||||
|
||||
if ($t eq 'HASH') {
|
||||
push @$acc, $v->{$k} if exists $v->{$k};
|
||||
} elsif ($t eq 'ARRAY') {
|
||||
getMetaStrings($_, $k, $acc) foreach @$v;
|
||||
} elsif (defined $v) {
|
||||
push @$acc, $v;
|
||||
}
|
||||
}
|
||||
|
||||
sub getMetaConcatStrings {
|
||||
my ($v, $k) = @_;
|
||||
|
||||
my @strings;
|
||||
getMetaStrings($v, $k, \@strings);
|
||||
return join(", ", @strings) || undef;
|
||||
}
|
||||
|
||||
# Add the build to the database.
|
||||
@@ -480,19 +554,19 @@ sub checkBuild {
|
||||
{ timestamp => $time
|
||||
, jobset_id => $jobset->id
|
||||
, job => $jobName
|
||||
, description => null($buildInfo->{description})
|
||||
, license => null($buildInfo->{license})
|
||||
, homepage => null($buildInfo->{homepage})
|
||||
, maintainers => null($buildInfo->{maintainers})
|
||||
, maxsilent => $buildInfo->{maxSilent}
|
||||
, timeout => $buildInfo->{timeout}
|
||||
, nixname => $buildInfo->{nixName}
|
||||
, description => getMeta($buildInfo->{meta}->{description}, undef)
|
||||
, license => getMetaConcatStrings($buildInfo->{meta}->{license}, "shortName")
|
||||
, homepage => getMeta($buildInfo->{meta}->{homepage}, undef)
|
||||
, maintainers => getMetaConcatStrings($buildInfo->{meta}->{maintainers}, "email")
|
||||
, maxsilent => getMeta($buildInfo->{meta}->{maxSilent}, 7200)
|
||||
, timeout => getMeta($buildInfo->{meta}->{timeout}, 36000)
|
||||
, nixname => $buildInfo->{name}
|
||||
, drvpath => $drvPath
|
||||
, system => $buildInfo->{system}
|
||||
, priority => $buildInfo->{schedulingPriority}
|
||||
, priority => getMeta($buildInfo->{meta}->{schedulingPriority}, 100)
|
||||
, finished => 0
|
||||
, iscurrent => 1
|
||||
, ischannel => $buildInfo->{isChannel}
|
||||
, ischannel => getMeta($buildInfo->{meta}->{isChannel}, 0)
|
||||
});
|
||||
|
||||
$build->buildoutputs->create({ name => $_, path => $buildInfo->{outputs}->{$_} })
|
||||
@@ -661,7 +735,7 @@ sub checkJobsetWrapped {
|
||||
return;
|
||||
}
|
||||
|
||||
# Hash the arguments to hydra-eval-jobs and check the
|
||||
# Hash the arguments to nix-eval-jobs and check the
|
||||
# JobsetInputHashes to see if the previous evaluation had the same
|
||||
# inputs. If so, bail out.
|
||||
my @args = ($jobset->nixexprinput // "", $jobset->nixexprpath // "", inputsToArgs($inputInfo));
|
||||
@@ -683,19 +757,12 @@ sub checkJobsetWrapped {
|
||||
|
||||
# Evaluate the job expression.
|
||||
my $evalStart = clock_gettime(CLOCK_MONOTONIC);
|
||||
my $jobs = evalJobs($project->name . ":" . $jobset->name, $inputInfo, $jobset->nixexprinput, $jobset->nixexprpath, $flakeRef);
|
||||
my $evalStop = clock_gettime(CLOCK_MONOTONIC);
|
||||
|
||||
if ($jobsetsJobset) {
|
||||
my @keys = keys %$jobs;
|
||||
die "The .jobsets jobset must only have a single job named 'jobsets'"
|
||||
unless (scalar @keys) == 1 && $keys[0] eq "jobsets";
|
||||
}
|
||||
Net::Statsd::timing("hydra.evaluator.eval_time", int(($evalStop - $evalStart) * 1000));
|
||||
my $evalStop;
|
||||
my $jobsIter = evalJobs($project->name . ":" . $jobset->name, $inputInfo, $jobset->nixexprinput, $jobset->nixexprpath, $flakeRef);
|
||||
|
||||
if ($dryRun) {
|
||||
foreach my $name (keys %{$jobs}) {
|
||||
my $job = $jobs->{$name};
|
||||
while (defined(my $job = $jobsIter->())) {
|
||||
my $name = $job->{attr};
|
||||
if (defined $job->{drvPath}) {
|
||||
print STDERR "good job $name: $job->{drvPath}\n";
|
||||
} else {
|
||||
@@ -705,36 +772,23 @@ sub checkJobsetWrapped {
|
||||
return;
|
||||
}
|
||||
|
||||
die "Jobset contains a job with an empty name. Make sure the jobset evaluates to an attrset of jobs.\n"
|
||||
if defined $jobs->{""};
|
||||
|
||||
$jobs->{$_}->{jobName} = $_ for keys %{$jobs};
|
||||
|
||||
my $jobOutPathMap = {};
|
||||
my $jobsetChanged = 0;
|
||||
my $dbStart = clock_gettime(CLOCK_MONOTONIC);
|
||||
|
||||
|
||||
# Store the error messages for jobs that failed to evaluate.
|
||||
my $evaluationErrorTime = time;
|
||||
my $evaluationErrorMsg = "";
|
||||
foreach my $job (values %{$jobs}) {
|
||||
next unless defined $job->{error};
|
||||
$evaluationErrorMsg .=
|
||||
($job->{jobName} ne "" ? "in job ‘$job->{jobName}’" : "at top-level") .
|
||||
":\n" . $job->{error} . "\n\n";
|
||||
}
|
||||
setJobsetError($jobset, $evaluationErrorMsg, $evaluationErrorTime);
|
||||
|
||||
my $evaluationErrorRecord = $db->resultset('EvaluationErrors')->create(
|
||||
{ errormsg => $evaluationErrorMsg
|
||||
, errortime => $evaluationErrorTime
|
||||
}
|
||||
);
|
||||
|
||||
my $jobOutPathMap = {};
|
||||
my $jobsetChanged = 0;
|
||||
my %buildMap;
|
||||
$db->txn_do(sub {
|
||||
|
||||
my @jobs;
|
||||
push @jobs, $_ while defined($_ = $jobsIter->());
|
||||
|
||||
$db->txn_do(sub {
|
||||
my $prevEval = getPrevJobsetEval($db, $jobset, 1);
|
||||
|
||||
# Clear the "current" flag on all builds. Since we're in a
|
||||
@@ -747,7 +801,7 @@ sub checkJobsetWrapped {
|
||||
, evaluationerror => $evaluationErrorRecord
|
||||
, timestamp => time
|
||||
, checkouttime => abs(int($checkoutStop - $checkoutStart))
|
||||
, evaltime => abs(int($evalStop - $evalStart))
|
||||
, evaltime => 0
|
||||
, hasnewbuilds => 0
|
||||
, nrbuilds => 0
|
||||
, flake => $flakeRef
|
||||
@@ -755,11 +809,24 @@ sub checkJobsetWrapped {
|
||||
, nixexprpath => $jobset->nixexprpath
|
||||
});
|
||||
|
||||
# Schedule each successfully evaluated job.
|
||||
foreach my $job (permute(values %{$jobs})) {
|
||||
next if defined $job->{error};
|
||||
#print STDERR "considering job " . $project->name, ":", $jobset->name, ":", $job->{jobName} . "\n";
|
||||
checkBuild($db, $jobset, $ev, $inputInfo, $job, \%buildMap, $prevEval, $jobOutPathMap, $plugins);
|
||||
my @jobsWithConstituents;
|
||||
|
||||
foreach my $job (@jobs) {
|
||||
if ($jobsetsJobset) {
|
||||
die "The .jobsets jobset must only have a single job named 'jobsets'"
|
||||
unless $job->{attr} eq "jobsets";
|
||||
}
|
||||
|
||||
$evaluationErrorMsg .=
|
||||
($job->{attr} ne "" ? "in job ‘$job->{attr}’" : "at top-level") .
|
||||
":\n" . $job->{error} . "\n\n" if defined $job->{error};
|
||||
|
||||
checkBuild($db, $jobset, $ev, $inputInfo, $job, \%buildMap, $prevEval, $jobOutPathMap, $plugins)
|
||||
unless defined $job->{error};
|
||||
|
||||
if (defined $job->{constituents}) {
|
||||
push @jobsWithConstituents, $job;
|
||||
}
|
||||
}
|
||||
|
||||
# Have any builds been added or removed since last time?
|
||||
@@ -797,21 +864,20 @@ sub checkJobsetWrapped {
|
||||
$drvPathToId{$x->{drvPath}} = $x;
|
||||
}
|
||||
|
||||
foreach my $job (values %{$jobs}) {
|
||||
next unless $job->{constituents};
|
||||
|
||||
foreach my $job (values @jobsWithConstituents) {
|
||||
next unless defined $job->{constituents};
|
||||
if (defined $job->{error}) {
|
||||
die "aggregate job ‘$job->{jobName}’ failed with the error: $job->{error}\n";
|
||||
die "aggregate job ‘$job->{attr}’ failed with the error: $job->{error}\n";
|
||||
}
|
||||
|
||||
my $x = $drvPathToId{$job->{drvPath}} or
|
||||
die "aggregate job ‘$job->{jobName}’ has no corresponding build record.\n";
|
||||
die "aggregate job ‘$job->{attr}’ has no corresponding build record.\n";
|
||||
foreach my $drvPath (@{$job->{constituents}}) {
|
||||
my $constituent = $drvPathToId{$drvPath};
|
||||
if (defined $constituent) {
|
||||
$db->resultset('AggregateConstituents')->update_or_create({aggregate => $x->{id}, constituent => $constituent->{id}});
|
||||
} else {
|
||||
warn "aggregate job ‘$job->{jobName}’ has a constituent ‘$drvPath’ that doesn't correspond to a Hydra build\n";
|
||||
warn "aggregate job ‘$job->{attr}’ has a constituent ‘$drvPath’ that doesn't correspond to a Hydra build\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -853,11 +919,15 @@ sub checkJobsetWrapped {
|
||||
$jobset->update({ enabled => 0 }) if $jobset->enabled == 2;
|
||||
|
||||
$jobset->update({ lastcheckedtime => time, forceeval => undef });
|
||||
|
||||
$evaluationErrorRecord->update({ errormsg => $evaluationErrorMsg });
|
||||
setJobsetError($jobset, $evaluationErrorMsg, $evaluationErrorTime);
|
||||
|
||||
$evalStop = clock_gettime(CLOCK_MONOTONIC);
|
||||
$ev->update({ evaltime => abs(int($evalStop - $evalStart)) });
|
||||
});
|
||||
|
||||
my $dbStop = clock_gettime(CLOCK_MONOTONIC);
|
||||
|
||||
Net::Statsd::timing("hydra.evaluator.db_time", int(($dbStop - $dbStart) * 1000));
|
||||
Net::Statsd::timing("hydra.evaluator.eval_time", int(($evalStop - $evalStart) * 1000));
|
||||
Net::Statsd::increment("hydra.evaluator.evals");
|
||||
Net::Statsd::increment("hydra.evaluator.cached_evals") unless $jobsetChanged;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Net::Statsd;
|
||||
use File::Slurper qw(read_text);
|
||||
use JSON::MaybeXS;
|
||||
use Getopt::Long qw(:config gnu_getopt);
|
||||
use IPC::Run3;
|
||||
|
||||
STDERR->autoflush(1);
|
||||
binmode STDERR, ":encoding(utf8)";
|
||||
@@ -25,10 +26,11 @@ sub gauge {
|
||||
}
|
||||
|
||||
sub sendQueueRunnerStats {
|
||||
my $s = `hydra-queue-runner --status`;
|
||||
die "cannot get queue runner stats\n" if $? != 0;
|
||||
my ($stdout, $stderr);
|
||||
run3(['hydra-queue-runner', '--status'], \undef, \$stdout, \$stderr);
|
||||
die "cannot get queue runner stats: $stderr\n" if $? != 0;
|
||||
|
||||
my $json = decode_json($s) or die "cannot decode queue runner status";
|
||||
my $json = decode_json($stdout) or die "cannot decode queue runner status";
|
||||
|
||||
gauge("hydra.queue.up", $json->{status} eq "up" ? 1 : 0);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use warnings;
|
||||
use File::Path;
|
||||
use File::stat;
|
||||
use File::Basename;
|
||||
use Nix::Store;
|
||||
use Hydra::Config;
|
||||
use Hydra::Schema;
|
||||
use Hydra::Helper::Nix;
|
||||
@@ -47,7 +46,7 @@ sub keepBuild {
|
||||
$build->finished && ($build->buildstatus == 0 || $build->buildstatus == 6))
|
||||
{
|
||||
foreach my $path (split / /, $build->get_column('outpaths')) {
|
||||
if (isValidPath($path)) {
|
||||
if ($MACHINE_LOCAL_STORE->isValidPath($path)) {
|
||||
addRoot $path;
|
||||
} else {
|
||||
print STDERR " warning: output ", $path, " has disappeared\n" if $build->finished;
|
||||
@@ -55,7 +54,7 @@ sub keepBuild {
|
||||
}
|
||||
}
|
||||
if (!$build->finished || ($keepFailedDrvs && $build->buildstatus != 0)) {
|
||||
if (isValidPath($build->drvpath)) {
|
||||
if ($MACHINE_LOCAL_STORE->isValidPath($build->drvpath)) {
|
||||
addRoot $build->drvpath;
|
||||
} else {
|
||||
print STDERR " warning: derivation ", $build->drvpath, " has disappeared\n";
|
||||
|
||||
@@ -78,7 +78,7 @@ fi
|
||||
|
||||
init_remote(){
|
||||
local url=$1;
|
||||
git init;
|
||||
git init --initial-branch=trunk;
|
||||
git remote add origin $url;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user