mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
job leader process
Implement a separate process, the "leader", which runs all the scripts for a job run, instead of directly from the main laminard process. This makes for a cleaner process tree view, where the owning job for a given script is clear; also the leader process acts as a subreaper to clean up any wayward descendent processes. Resolves #78.
This commit is contained in:
parent
304ef797b8
commit
3fde38c6b8
@ -86,14 +86,15 @@ generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
|||||||
# (see resources.cpp where these are fetched)
|
# (see resources.cpp where these are fetched)
|
||||||
|
|
||||||
set(LAMINARD_CORE_SOURCES
|
set(LAMINARD_CORE_SOURCES
|
||||||
src/database.cpp
|
|
||||||
src/server.cpp
|
|
||||||
src/laminar.cpp
|
|
||||||
src/conf.cpp
|
src/conf.cpp
|
||||||
|
src/database.cpp
|
||||||
|
src/laminar.cpp
|
||||||
|
src/leader.cpp
|
||||||
src/http.cpp
|
src/http.cpp
|
||||||
src/resources.cpp
|
src/resources.cpp
|
||||||
src/rpc.cpp
|
src/rpc.cpp
|
||||||
src/run.cpp
|
src/run.cpp
|
||||||
|
src/server.cpp
|
||||||
laminar.capnp.c++
|
laminar.capnp.c++
|
||||||
index_html_size.h
|
index_html_size.h
|
||||||
)
|
)
|
||||||
@ -111,7 +112,7 @@ set(BUILD_TESTS FALSE CACHE BOOL "Build tests")
|
|||||||
if(BUILD_TESTS)
|
if(BUILD_TESTS)
|
||||||
find_package(GTest REQUIRED)
|
find_package(GTest REQUIRED)
|
||||||
include_directories(${GTEST_INCLUDE_DIRS} src)
|
include_directories(${GTEST_INCLUDE_DIRS} src)
|
||||||
add_executable(laminar-tests ${LAMINARD_CORE_SOURCES} ${COMPRESSED_BINS} test/main.cpp test/laminar-functional.cpp test/unit-conf.cpp test/unit-database.cpp test/unit-run.cpp)
|
add_executable(laminar-tests ${LAMINARD_CORE_SOURCES} ${COMPRESSED_BINS} test/main.cpp test/laminar-functional.cpp test/unit-conf.cpp test/unit-database.cpp)
|
||||||
target_link_libraries(laminar-tests ${GTEST_LIBRARY} capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
target_link_libraries(laminar-tests ${GTEST_LIBRARY} capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
@ -323,8 +323,6 @@ Then in `example.run`
|
|||||||
echo $foo # prints "bar"
|
echo $foo # prints "bar"
|
||||||
```
|
```
|
||||||
|
|
||||||
This works because laminarc reads `$JOB` and `$NUM` and passes them to the laminar daemon as part of the `set` request. (It is thus possible to set environment variables on other jobs by overriding these variables, but this is not very useful).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Archiving artefacts
|
# Archiving artefacts
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#define EXIT_BAD_ARGUMENT 1
|
#define EXIT_BAD_ARGUMENT 1
|
||||||
#define EXIT_OPERATION_FAILED 2
|
#define EXIT_OPERATION_FAILED 2
|
||||||
@ -169,21 +170,10 @@ int main(int argc, char** argv) {
|
|||||||
fprintf(stderr, "Usage %s set param=value\n", argv[0]);
|
fprintf(stderr, "Usage %s set param=value\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
auto req = laminar.setRequest();
|
if(char* pipeNum = getenv("__LAMINAR_SETENV_PIPE")) {
|
||||||
char* eq = strchr(argv[2], '=');
|
write(atoi(pipeNum), argv[2], strlen(argv[2]));
|
||||||
char* job = getenv("JOB");
|
|
||||||
char* num = getenv("RUN");
|
|
||||||
if(job && num && eq) {
|
|
||||||
char* name = argv[2];
|
|
||||||
*eq++ = '\0';
|
|
||||||
char* val = eq;
|
|
||||||
req.getRun().setJob(job);
|
|
||||||
req.getRun().setBuildNum(atoi(num));
|
|
||||||
req.getParam().setName(name);
|
|
||||||
req.getParam().setValue(val);
|
|
||||||
req.send().wait(waitScope);
|
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Missing $JOB or $RUN or param is not in the format key=value\n");
|
fprintf(stderr, "Must be run from within a laminar job\n");
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
} else if(strcmp(argv[1], "abort") == 0) {
|
} else if(strcmp(argv[1], "abort") == 0) {
|
||||||
|
@ -5,11 +5,10 @@ interface LaminarCi {
|
|||||||
queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult);
|
queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult);
|
||||||
start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32);
|
start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32);
|
||||||
run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32);
|
run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32);
|
||||||
set @3 (run :Run, param :JobParam) -> (result :MethodResult);
|
listQueued @3 () -> (result :List(Text));
|
||||||
listQueued @4 () -> (result :List(Text));
|
listRunning @4 () -> (result :List(Run));
|
||||||
listRunning @5 () -> (result :List(Run));
|
listKnown @5 () -> (result :List(Text));
|
||||||
listKnown @6 () -> (result :List(Text));
|
abort @6 (run :Run) -> (result :MethodResult);
|
||||||
abort @7 (run :Run) -> (result :MethodResult);
|
|
||||||
|
|
||||||
struct Run {
|
struct Run {
|
||||||
job @0 :Text;
|
job @0 :Text;
|
||||||
|
@ -581,16 +581,14 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Laminar::abort(std::string job, uint buildNum) {
|
bool Laminar::abort(std::string job, uint buildNum) {
|
||||||
if(Run* run = activeRun(job, buildNum)) {
|
if(Run* run = activeRun(job, buildNum))
|
||||||
run->abort(true);
|
return run->abort();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Laminar::abortAll() {
|
void Laminar::abortAll() {
|
||||||
for(std::shared_ptr<Run> run : activeJobs) {
|
for(std::shared_ptr<Run> run : activeJobs) {
|
||||||
run->abort(false);
|
run->abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,7 +596,9 @@ bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
|||||||
for(auto& sc : contexts) {
|
for(auto& sc : contexts) {
|
||||||
std::shared_ptr<Context> ctx = sc.second;
|
std::shared_ptr<Context> ctx = sc.second;
|
||||||
|
|
||||||
if(ctx->canQueue(jobContexts.at(run->name)) && run->configure(buildNums[run->name] + 1, ctx, *fsHome)) {
|
if(ctx->canQueue(jobContexts.at(run->name))) {
|
||||||
|
kj::Promise<RunState> onRunFinished = run->start(buildNums[run->name] + 1, ctx, *fsHome,[this](kj::Maybe<pid_t>& pid){return srv.onChildExit(pid);});
|
||||||
|
|
||||||
ctx->busyExecutors++;
|
ctx->busyExecutors++;
|
||||||
// set the last known result if exists
|
// set the last known result if exists
|
||||||
db->stmt("SELECT result FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
db->stmt("SELECT result FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
||||||
@ -607,13 +607,20 @@ bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
|||||||
run->lastResult = RunState(result);
|
run->lastResult = RunState(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actually schedules the Run steps
|
kj::Promise<void> exec = srv.readDescriptor(run->output_fd, [this, run](const char*b, size_t n){
|
||||||
kj::Promise<void> exec = handleRunStep(run.get()).then([=]{
|
// handle log output
|
||||||
runFinished(run.get());
|
std::string s(b, n);
|
||||||
|
run->log += s;
|
||||||
|
http->notifyLog(run->name, run->build, s, false);
|
||||||
|
}).then([run, p = kj::mv(onRunFinished)]() mutable {
|
||||||
|
// wait until leader reaped
|
||||||
|
return kj::mv(p);
|
||||||
|
}).then([this, run](RunState){
|
||||||
|
handleRunFinished(run.get());
|
||||||
});
|
});
|
||||||
if(run->timeout > 0) {
|
if(run->timeout > 0) {
|
||||||
exec = exec.attach(srv.addTimeout(run->timeout, [r=run.get()](){
|
exec = exec.attach(srv.addTimeout(run->timeout, [r=run.get()](){
|
||||||
r->abort(true);
|
r->abort();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
srv.addTask(kj::mv(exec));
|
srv.addTask(kj::mv(exec));
|
||||||
@ -657,31 +664,7 @@ void Laminar::assignNewJobs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kj::Promise<void> Laminar::handleRunStep(Run* run) {
|
void Laminar::handleRunFinished(Run * r) {
|
||||||
if(run->step()) {
|
|
||||||
// no more steps
|
|
||||||
return kj::READY_NOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
kj::Promise<int> exited = srv.onChildExit(run->current_pid);
|
|
||||||
// promise is fulfilled when the process is reaped. But first we wait for all
|
|
||||||
// output from the pipe (Run::output_fd) to be consumed.
|
|
||||||
return srv.readDescriptor(run->output_fd, [this,run](const char*b,size_t n){
|
|
||||||
// handle log output
|
|
||||||
std::string s(b, n);
|
|
||||||
run->log += s;
|
|
||||||
http->notifyLog(run->name, run->build, s, false);
|
|
||||||
}).then([p = std::move(exited)]() mutable {
|
|
||||||
// wait until the process is reaped
|
|
||||||
return kj::mv(p);
|
|
||||||
}).then([this, run](int status){
|
|
||||||
run->reaped(status);
|
|
||||||
// next step in Run
|
|
||||||
return handleRunStep(run);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Laminar::runFinished(Run * r) {
|
|
||||||
std::shared_ptr<Context> ctx = r->context;
|
std::shared_ptr<Context> ctx = r->context;
|
||||||
|
|
||||||
ctx->busyExecutors--;
|
ctx->busyExecutors--;
|
||||||
|
@ -105,8 +105,7 @@ private:
|
|||||||
bool loadConfiguration();
|
bool loadConfiguration();
|
||||||
void assignNewJobs();
|
void assignNewJobs();
|
||||||
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
|
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
|
||||||
kj::Promise<void> handleRunStep(Run *run);
|
void handleRunFinished(Run*);
|
||||||
void runFinished(Run*);
|
|
||||||
// expects that Json has started an array
|
// expects that Json has started an array
|
||||||
void populateArtifacts(Json& out, std::string job, uint num) const;
|
void populateArtifacts(Json& out, std::string job, uint num) const;
|
||||||
|
|
||||||
|
295
src/leader.cpp
Normal file
295
src/leader.cpp
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
///
|
||||||
|
/// Copyright 2019 Oliver Giles
|
||||||
|
///
|
||||||
|
/// This file is part of Laminar
|
||||||
|
///
|
||||||
|
/// Laminar is free software: you can redistribute it and/or modify
|
||||||
|
/// it under the terms of the GNU General Public License as published by
|
||||||
|
/// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
/// (at your option) any later version.
|
||||||
|
///
|
||||||
|
/// Laminar is distributed in the hope that it will be useful,
|
||||||
|
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
/// GNU General Public License for more details.
|
||||||
|
///
|
||||||
|
/// You should have received a copy of the GNU General Public License
|
||||||
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
///
|
||||||
|
#include "log.h"
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <queue>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <kj/async-io.h>
|
||||||
|
#include <kj/async-unix.h>
|
||||||
|
#include <kj/filesystem.h>
|
||||||
|
|
||||||
|
#include "run.h"
|
||||||
|
|
||||||
|
// short syntax helper for kj::Path
|
||||||
|
template<typename T>
|
||||||
|
inline kj::Path operator/(const kj::Path& p, const T& ext) {
|
||||||
|
return p.append(ext);
|
||||||
|
}
|
||||||
|
template<typename T>
|
||||||
|
inline kj::Path operator/(const kj::PathPtr& p, const T& ext) {
|
||||||
|
return p.append(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Script {
|
||||||
|
kj::Path path;
|
||||||
|
kj::Path cwd;
|
||||||
|
bool runOnAbort;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Leader final : public kj::TaskSet::ErrorHandler {
|
||||||
|
public:
|
||||||
|
Leader(kj::AsyncIoContext& ioContext, kj::Filesystem& fs, const char* jobName, uint runNumber);
|
||||||
|
RunState run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void taskFailed(kj::Exception&& exception) override;
|
||||||
|
kj::Promise<void> step(std::queue<Script>& scripts);
|
||||||
|
kj::Promise<void> reapChildProcesses();
|
||||||
|
kj::Promise<void> readEnvPipe(kj::AsyncInputStream* stream, char* buffer);
|
||||||
|
|
||||||
|
kj::TaskSet tasks;
|
||||||
|
RunState result;
|
||||||
|
kj::AsyncIoContext& ioContext;
|
||||||
|
const kj::Directory& home;
|
||||||
|
kj::PathPtr rootPath;
|
||||||
|
std::string jobName;
|
||||||
|
uint runNumber;
|
||||||
|
pid_t currentGroupId;
|
||||||
|
pid_t currentScriptPid;
|
||||||
|
std::queue<Script> scripts;
|
||||||
|
int setEnvPipe[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
Leader::Leader(kj::AsyncIoContext &ioContext, kj::Filesystem &fs, const char *jobName, uint runNumber) :
|
||||||
|
tasks(*this),
|
||||||
|
result(RunState::SUCCESS),
|
||||||
|
ioContext(ioContext),
|
||||||
|
home(fs.getCurrent()),
|
||||||
|
rootPath(fs.getCurrentPath()),
|
||||||
|
jobName(jobName),
|
||||||
|
runNumber(runNumber)
|
||||||
|
{
|
||||||
|
tasks.add(ioContext.unixEventPort.onSignal(SIGTERM).then([this](siginfo_t) {
|
||||||
|
while(scripts.size() && (!scripts.front().runOnAbort))
|
||||||
|
scripts.pop();
|
||||||
|
// TODO: probably shouldn't do this if we are already in a runOnAbort script
|
||||||
|
kill(-currentGroupId, SIGTERM);
|
||||||
|
// TODO: wait a few seconds for exit, then send KILL?
|
||||||
|
}));
|
||||||
|
|
||||||
|
pipe(setEnvPipe);
|
||||||
|
auto event = ioContext.lowLevelProvider->wrapInputFd(setEnvPipe[0], kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP);
|
||||||
|
auto buffer = kj::heapArrayBuilder<char>(1024);
|
||||||
|
tasks.add(readEnvPipe(event, buffer.asPtr().begin()).attach(kj::mv(event), kj::mv(buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
RunState Leader::run()
|
||||||
|
{
|
||||||
|
kj::Path cfgDir{"cfg"};
|
||||||
|
|
||||||
|
// create the run directory
|
||||||
|
kj::Path rd{"run",jobName,std::to_string(runNumber)};
|
||||||
|
bool createWorkdir = true;
|
||||||
|
KJ_IF_MAYBE(ls, home.tryLstat(rd)) {
|
||||||
|
LASSERT(ls->type == kj::FsNode::Type::DIRECTORY);
|
||||||
|
LLOG(WARNING, "Working directory already exists, removing", rd.toString());
|
||||||
|
if(home.tryRemove(rd) == false) {
|
||||||
|
LLOG(WARNING, "Failed to remove working directory");
|
||||||
|
createWorkdir = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(createWorkdir && home.tryOpenSubdir(rd, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
||||||
|
LLOG(ERROR, "Could not create working directory", rd.toString());
|
||||||
|
return RunState::FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an archive directory
|
||||||
|
kj::Path archive = kj::Path{"archive",jobName,std::to_string(runNumber)};
|
||||||
|
if(home.exists(archive)) {
|
||||||
|
LLOG(WARNING, "Archive directory already exists", archive.toString());
|
||||||
|
} else if(home.tryOpenSubdir(archive, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
||||||
|
LLOG(ERROR, "Could not create archive directory", archive.toString());
|
||||||
|
return RunState::FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a workspace for this job if it doesn't exist
|
||||||
|
kj::Path ws{"run",jobName,"workspace"};
|
||||||
|
if(!home.exists(ws)) {
|
||||||
|
home.openSubdir(ws, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT);
|
||||||
|
// prepend the workspace init script
|
||||||
|
if(home.exists(cfgDir/"jobs"/(jobName+".init")))
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".init"), kj::mv(ws), false});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add scripts
|
||||||
|
// global before-run script
|
||||||
|
if(home.exists(cfgDir/"before"))
|
||||||
|
scripts.push({cfgDir/"before", rd.clone(), false});
|
||||||
|
// job before-run script
|
||||||
|
if(home.exists(cfgDir/"jobs"/(jobName+".before")))
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".before"), rd.clone(), false});
|
||||||
|
// main run script. must exist.
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".run"), rd.clone(), false});
|
||||||
|
// job after-run script
|
||||||
|
if(home.exists(cfgDir/"jobs"/(jobName+".after")))
|
||||||
|
scripts.push({cfgDir/"jobs"/(jobName+".after"), rd.clone(), true});
|
||||||
|
// global after-run script
|
||||||
|
if(home.exists(cfgDir/"after"))
|
||||||
|
scripts.push({cfgDir/"after", rd.clone(), true});
|
||||||
|
|
||||||
|
// Start executing scripts
|
||||||
|
return step(scripts).then([this](){
|
||||||
|
return result;
|
||||||
|
}).wait(ioContext.waitScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Leader::taskFailed(kj::Exception &&exception)
|
||||||
|
{
|
||||||
|
LLOG(ERROR, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<void> Leader::step(std::queue<Script> &scripts)
|
||||||
|
{
|
||||||
|
if(scripts.empty())
|
||||||
|
return kj::READY_NOW;
|
||||||
|
|
||||||
|
Script currentScript = kj::mv(scripts.front());
|
||||||
|
scripts.pop();
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if(pid == 0) { // child
|
||||||
|
// unblock all signals
|
||||||
|
sigset_t mask;
|
||||||
|
sigfillset(&mask);
|
||||||
|
sigprocmask(SIG_UNBLOCK, &mask, nullptr);
|
||||||
|
|
||||||
|
// create a new process group to help us deal with any wayward forks
|
||||||
|
setpgid(0, 0);
|
||||||
|
|
||||||
|
std::string buildNum = std::to_string(runNumber);
|
||||||
|
|
||||||
|
LSYSCALL(chdir(currentScript.cwd.toString(false).cStr()));
|
||||||
|
|
||||||
|
setenv("RESULT", to_string(result).c_str(), true);
|
||||||
|
|
||||||
|
// pass the pipe through a variable to allow laminarc to send new env back
|
||||||
|
char pipeNum[4];
|
||||||
|
sprintf(pipeNum, "%d", setEnvPipe[1]);
|
||||||
|
setenv("__LAMINAR_SETENV_PIPE", pipeNum, 1);
|
||||||
|
|
||||||
|
fprintf(stderr, "[laminar] Executing %s\n", currentScript.path.toString().cStr());
|
||||||
|
kj::String execPath = (rootPath/currentScript.path).toString(true);
|
||||||
|
|
||||||
|
execl(execPath.cStr(), execPath.cStr(), NULL);
|
||||||
|
fprintf(stderr, "[laminar] Failed to execute %s\n", currentScript.path.toString().cStr());
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentScriptPid = pid;
|
||||||
|
currentGroupId = pid;
|
||||||
|
|
||||||
|
return reapChildProcesses().then([&](){
|
||||||
|
return step(scripts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<void> Leader::reapChildProcesses()
|
||||||
|
{
|
||||||
|
return ioContext.unixEventPort.onSignal(SIGCHLD).then([this](siginfo_t) -> kj::Promise<void> {
|
||||||
|
while(true) {
|
||||||
|
int status;
|
||||||
|
errno = 0;
|
||||||
|
pid_t pid = waitpid(-1, &status, WNOHANG);
|
||||||
|
if(pid == -1 && errno == ECHILD) {
|
||||||
|
// all children exited
|
||||||
|
return kj::READY_NOW;
|
||||||
|
} else if(pid == 0) {
|
||||||
|
// child processes are still running
|
||||||
|
if(currentScriptPid) {
|
||||||
|
// We could get here if a more deeply nested process was reparented to us
|
||||||
|
// before the primary script executed. Quietly wait until the process we're
|
||||||
|
// waiting for is done
|
||||||
|
return reapChildProcesses();
|
||||||
|
}
|
||||||
|
// Otherwise, reparented orphans are on borrowed time
|
||||||
|
// TODO list wayward processes?
|
||||||
|
fprintf(stderr, "[laminar] sending SIGHUP to adopted child processes\n");
|
||||||
|
kill(-currentGroupId, SIGHUP);
|
||||||
|
return ioContext.provider->getTimer().afterDelay(5*kj::SECONDS).then([this]{
|
||||||
|
fprintf(stderr, "[laminar] sending SIGKILL to process group %d\n", currentGroupId);
|
||||||
|
// TODO: should we mark the job as failed if we had to kill reparented processes?
|
||||||
|
kill(-currentGroupId, SIGKILL);
|
||||||
|
return reapChildProcesses();
|
||||||
|
}).exclusiveJoin(reapChildProcesses());
|
||||||
|
} else if(pid == currentScriptPid) {
|
||||||
|
// the script we were waiting for is done
|
||||||
|
// if we already marked as failed, preserve that
|
||||||
|
if(result == RunState::SUCCESS) {
|
||||||
|
if(WIFSIGNALED(status) && (WTERMSIG(status) == SIGTERM || WTERMSIG(status) == SIGKILL))
|
||||||
|
result = RunState::ABORTED;
|
||||||
|
else if(WEXITSTATUS(status) != 0)
|
||||||
|
result = RunState::FAILED;
|
||||||
|
}
|
||||||
|
currentScriptPid = 0;
|
||||||
|
} else {
|
||||||
|
// some reparented process was reaped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<void> Leader::readEnvPipe(kj::AsyncInputStream *stream, char *buffer) {
|
||||||
|
return stream->tryRead(buffer, 1, 1024).then([this,stream,buffer](size_t sz) {
|
||||||
|
if(sz > 0) {
|
||||||
|
buffer[sz] = '\0';
|
||||||
|
if(char* eq = strchr(buffer, '=')) {
|
||||||
|
*eq++ = '\0';
|
||||||
|
setenv(buffer, eq, 1);
|
||||||
|
}
|
||||||
|
return readEnvPipe(stream, kj::mv(buffer));
|
||||||
|
}
|
||||||
|
return kj::Promise<void>(kj::READY_NOW);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int leader_main(void) {
|
||||||
|
auto ioContext = kj::setupAsyncIo();
|
||||||
|
auto fs = kj::newDiskFilesystem();
|
||||||
|
|
||||||
|
kj::UnixEventPort::captureSignal(SIGTERM);
|
||||||
|
// Don't use captureChildExit or onChildExit because they don't provide a way to
|
||||||
|
// reap orphaned child processes. Stick with the more fundamental onSignal.
|
||||||
|
kj::UnixEventPort::captureSignal(SIGCHLD);
|
||||||
|
|
||||||
|
// Becoming a subreaper means any descendent process whose parent process disappears
|
||||||
|
// will be reparented to this one instead of init (or higher layer subreaper).
|
||||||
|
// We do this so that the run will wait until all descedents exit before executing
|
||||||
|
// the next step.
|
||||||
|
prctl(PR_SET_CHILD_SUBREAPER, 1, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
// Become the leader of a new process group. This is so that all child processes
|
||||||
|
// will also get a kill signal when the run is aborted
|
||||||
|
setpgid(0, 0);
|
||||||
|
|
||||||
|
// Environment inherited from main laminard process
|
||||||
|
const char* jobName = getenv("JOB");
|
||||||
|
std::string name(jobName);
|
||||||
|
uint runNumber = atoi(getenv("RUN"));
|
||||||
|
|
||||||
|
if(!jobName || !runNumber)
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
|
||||||
|
Leader leader(ioContext, *fs, jobName, runNumber);
|
||||||
|
|
||||||
|
// Parent process will cast back to RunState
|
||||||
|
return int(leader.run());
|
||||||
|
}
|
36
src/leader.h
Normal file
36
src/leader.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
///
|
||||||
|
/// Copyright 2019 Oliver Giles
|
||||||
|
///
|
||||||
|
/// This file is part of Laminar
|
||||||
|
///
|
||||||
|
/// Laminar is free software: you can redistribute it and/or modify
|
||||||
|
/// it under the terms of the GNU General Public License as published by
|
||||||
|
/// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
/// (at your option) any later version.
|
||||||
|
///
|
||||||
|
/// Laminar is distributed in the hope that it will be useful,
|
||||||
|
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
/// GNU General Public License for more details.
|
||||||
|
///
|
||||||
|
/// You should have received a copy of the GNU General Public License
|
||||||
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
///
|
||||||
|
#ifndef LAMINAR_LEADER_H_
|
||||||
|
#define LAMINAR_LEADER_H_
|
||||||
|
|
||||||
|
// Main function for the leader process which is responsible for
|
||||||
|
// executing all the scripts which make up a Run. Separating this
|
||||||
|
// into its own process allows for a cleaner process tree view,
|
||||||
|
// where it's obvious which script belongs to which run of which
|
||||||
|
// job, and allows this leader process to act as a subreaper for
|
||||||
|
// any wayward child processes.
|
||||||
|
|
||||||
|
// This could have been implemented as a separate process, but
|
||||||
|
// instead we just fork & exec /proc/self/exe from the main laminar
|
||||||
|
// daemon, and distinguish based on argv[0]. This saves installing
|
||||||
|
// another binary and avoids some associated pitfalls.
|
||||||
|
|
||||||
|
int leader_main(void);
|
||||||
|
|
||||||
|
#endif // LAMINAR_LEADER_H_
|
@ -17,6 +17,7 @@
|
|||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#include "laminar.h"
|
#include "laminar.h"
|
||||||
|
#include "leader.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
@ -40,9 +41,10 @@ constexpr const char* INTADDR_HTTP_DEFAULT = "*:8080";
|
|||||||
constexpr const char* ARCHIVE_URL_DEFAULT = "/archive/";
|
constexpr const char* ARCHIVE_URL_DEFAULT = "/archive/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
|
if(argv[0][0] == '{')
|
||||||
|
return leader_main();
|
||||||
|
|
||||||
for(int i = 1; i < argc; ++i) {
|
for(int i = 1; i < argc; ++i) {
|
||||||
if(strcmp(argv[i], "-v") == 0) {
|
if(strcmp(argv[i], "-v") == 0) {
|
||||||
kj::_::Debug::setLogLevel(kj::_::Debug::Severity::INFO);
|
kj::_::Debug::setLogLevel(kj::_::Debug::Severity::INFO);
|
||||||
|
20
src/rpc.cpp
20
src/rpc.cpp
@ -81,10 +81,10 @@ public:
|
|||||||
std::string jobName = context.getParams().getJobName();
|
std::string jobName = context.getParams().getJobName();
|
||||||
LLOG(INFO, "RPC run", jobName);
|
LLOG(INFO, "RPC run", jobName);
|
||||||
std::shared_ptr<Run> run = laminar.queueJob(jobName, params(context.getParams().getParams()));
|
std::shared_ptr<Run> run = laminar.queueJob(jobName, params(context.getParams().getParams()));
|
||||||
if(Run* r = run.get()) {
|
if(run) {
|
||||||
return r->whenFinished().then([context,r](RunState state) mutable {
|
return run->whenFinished().then([context,run](RunState state) mutable {
|
||||||
context.getResults().setResult(fromRunState(state));
|
context.getResults().setResult(fromRunState(state));
|
||||||
context.getResults().setBuildNum(r->build);
|
context.getResults().setBuildNum(run->build);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
context.getResults().setResult(LaminarCi::JobResult::UNKNOWN);
|
context.getResults().setResult(LaminarCi::JobResult::UNKNOWN);
|
||||||
@ -92,20 +92,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a parameter on a running build
|
|
||||||
kj::Promise<void> set(SetContext context) override {
|
|
||||||
std::string jobName = context.getParams().getRun().getJob();
|
|
||||||
uint buildNum = context.getParams().getRun().getBuildNum();
|
|
||||||
LLOG(INFO, "RPC set", jobName, buildNum);
|
|
||||||
|
|
||||||
LaminarCi::MethodResult result = laminar.setParam(jobName, buildNum,
|
|
||||||
context.getParams().getParam().getName(), context.getParams().getParam().getValue())
|
|
||||||
? LaminarCi::MethodResult::SUCCESS
|
|
||||||
: LaminarCi::MethodResult::FAILED;
|
|
||||||
context.getResults().setResult(result);
|
|
||||||
return kj::READY_NOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
// List jobs in queue
|
// List jobs in queue
|
||||||
kj::Promise<void> listQueued(ListQueuedContext context) override {
|
kj::Promise<void> listQueued(ListQueuedContext context) override {
|
||||||
const std::list<std::shared_ptr<Run>>& queue = laminar.listQueuedJobs();
|
const std::list<std::shared_ptr<Run>>& queue = laminar.listQueuedJobs();
|
||||||
|
242
src/run.cpp
242
src/run.cpp
@ -52,7 +52,9 @@ Run::Run(std::string name, ParamMap pm, kj::Path&& rootPath) :
|
|||||||
queuedAt(time(nullptr)),
|
queuedAt(time(nullptr)),
|
||||||
rootPath(kj::mv(rootPath)),
|
rootPath(kj::mv(rootPath)),
|
||||||
started(kj::newPromiseAndFulfiller<void>()),
|
started(kj::newPromiseAndFulfiller<void>()),
|
||||||
finished(kj::newPromiseAndFulfiller<RunState>())
|
startedFork(started.promise.fork()),
|
||||||
|
finished(kj::newPromiseAndFulfiller<RunState>()),
|
||||||
|
finishedFork(finished.promise.fork())
|
||||||
{
|
{
|
||||||
for(auto it = params.begin(); it != params.end();) {
|
for(auto it = params.begin(); it != params.end();) {
|
||||||
if(it->first[0] == '=') {
|
if(it->first[0] == '=') {
|
||||||
@ -75,186 +77,116 @@ Run::~Run() {
|
|||||||
LLOG(INFO, "Run destroyed");
|
LLOG(INFO, "Run destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Run::configure(uint buildNum, std::shared_ptr<Context> nd, const kj::Directory& fsHome)
|
static void setEnvFromFile(const kj::Path& rootPath, kj::Path file) {
|
||||||
|
StringMap vars = parseConfFile((rootPath/file).toString(true).cStr());
|
||||||
|
for(auto& it : vars) {
|
||||||
|
setenv(it.first.c_str(), it.second.c_str(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::Promise<RunState> Run::start(uint buildNum, std::shared_ptr<Context> ctx, const kj::Directory &fsHome, std::function<kj::Promise<int>(kj::Maybe<pid_t>&)> getPromise)
|
||||||
{
|
{
|
||||||
kj::Path cfgDir{"cfg"};
|
kj::Path cfgDir{"cfg"};
|
||||||
|
|
||||||
// create the run directory
|
|
||||||
kj::Path rd{"run",name,std::to_string(buildNum)};
|
|
||||||
bool createWorkdir = true;
|
|
||||||
KJ_IF_MAYBE(ls, fsHome.tryLstat(rd)) {
|
|
||||||
LASSERT(ls->type == kj::FsNode::Type::DIRECTORY);
|
|
||||||
LLOG(WARNING, "Working directory already exists, removing", rd.toString());
|
|
||||||
if(fsHome.tryRemove(rd) == false) {
|
|
||||||
LLOG(WARNING, "Failed to remove working directory");
|
|
||||||
createWorkdir = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(createWorkdir && fsHome.tryOpenSubdir(rd, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
|
||||||
LLOG(ERROR, "Could not create working directory", rd.toString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create an archive directory
|
|
||||||
kj::Path archive = kj::Path{"archive",name,std::to_string(buildNum)};
|
|
||||||
if(fsHome.exists(archive)) {
|
|
||||||
LLOG(WARNING, "Archive directory already exists", archive.toString());
|
|
||||||
} else if(fsHome.tryOpenSubdir(archive, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT) == nullptr) {
|
|
||||||
LLOG(ERROR, "Could not create archive directory", archive.toString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a workspace for this job if it doesn't exist
|
|
||||||
kj::Path ws{"run",name,"workspace"};
|
|
||||||
if(!fsHome.exists(ws)) {
|
|
||||||
fsHome.openSubdir(ws, kj::WriteMode::CREATE|kj::WriteMode::CREATE_PARENT);
|
|
||||||
// prepend the workspace init script
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".init")))
|
|
||||||
addScript(cfgDir/"jobs"/(name+".init"), kj::mv(ws));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add scripts
|
|
||||||
// global before-run script
|
|
||||||
if(fsHome.exists(cfgDir/"before"))
|
|
||||||
addScript(cfgDir/"before", rd.clone());
|
|
||||||
// job before-run script
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".before")))
|
|
||||||
addScript(cfgDir/"jobs"/(name+".before"), rd.clone());
|
|
||||||
// main run script. must exist.
|
|
||||||
addScript(cfgDir/"jobs"/(name+".run"), rd.clone());
|
|
||||||
// job after-run script
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".after")))
|
|
||||||
addScript(cfgDir/"jobs"/(name+".after"), rd.clone(), true);
|
|
||||||
// global after-run script
|
|
||||||
if(fsHome.exists(cfgDir/"after"))
|
|
||||||
addScript(cfgDir/"after", rd.clone(), true);
|
|
||||||
|
|
||||||
// add environment files
|
|
||||||
if(fsHome.exists(cfgDir/"env"))
|
|
||||||
addEnv(cfgDir/"env");
|
|
||||||
if(fsHome.exists(cfgDir/"contexts"/(nd->name+".env")))
|
|
||||||
addEnv(cfgDir/"contexts"/(nd->name+".env"));
|
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".env")))
|
|
||||||
addEnv(cfgDir/"jobs"/(name+".env"));
|
|
||||||
|
|
||||||
// add job timeout if specified
|
// add job timeout if specified
|
||||||
if(fsHome.exists(cfgDir/"jobs"/(name+".conf"))) {
|
if(fsHome.exists(cfgDir/"jobs"/(name+".conf"))) {
|
||||||
timeout = parseConfFile((rootPath/cfgDir/"jobs"/(name+".conf")).toString(true).cStr()).get<int>("TIMEOUT", 0);
|
timeout = parseConfFile((rootPath/cfgDir/"jobs"/(name+".conf")).toString(true).cStr()).get<int>("TIMEOUT", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All good, we've "started"
|
int plog[2];
|
||||||
startedAt = time(nullptr);
|
LSYSCALL(pipe(plog));
|
||||||
build = buildNum;
|
|
||||||
context = nd;
|
|
||||||
|
|
||||||
// notifies the rpc client if the start command was used
|
// Fork a process leader to run all the steps of the job. This gives us a nice
|
||||||
started.fulfiller->fulfill();
|
// process tree output (job name and number as the process name) and helps
|
||||||
|
// contain any wayward descendent processes.
|
||||||
|
pid_t leader;
|
||||||
|
LSYSCALL(leader = fork());
|
||||||
|
|
||||||
return true;
|
if(leader == 0) {
|
||||||
|
// All output from this process will be captured in the plog pipe
|
||||||
|
close(plog[0]);
|
||||||
|
dup2(plog[1], STDOUT_FILENO);
|
||||||
|
dup2(plog[1], STDERR_FILENO);
|
||||||
|
close(plog[1]);
|
||||||
|
|
||||||
|
// All initial/fixed env vars can be set here. Dynamic ones, including
|
||||||
|
// "RESULT" and any set by `laminarc set` have to be handled in the subprocess.
|
||||||
|
|
||||||
|
// add environment files
|
||||||
|
if(fsHome.exists(cfgDir/"env"))
|
||||||
|
setEnvFromFile(rootPath, cfgDir/"env");
|
||||||
|
if(fsHome.exists(cfgDir/"contexts"/(ctx->name+".env")))
|
||||||
|
setEnvFromFile(rootPath, cfgDir/"contexts"/(ctx->name+".env"));
|
||||||
|
if(fsHome.exists(cfgDir/"jobs"/(name+".env")))
|
||||||
|
setEnvFromFile(rootPath, cfgDir/"jobs"/(name+".env"));
|
||||||
|
|
||||||
|
// parameterized vars
|
||||||
|
for(auto& pair : params) {
|
||||||
|
setenv(pair.first.c_str(), pair.second.c_str(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Run::reason() const {
|
|
||||||
return reasonMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Run::step() {
|
|
||||||
if(!scripts.size())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
Script currentScript = kj::mv(scripts.front());
|
|
||||||
scripts.pop();
|
|
||||||
|
|
||||||
int pfd[2];
|
|
||||||
pipe(pfd);
|
|
||||||
pid_t pid = fork();
|
|
||||||
if(pid == 0) { // child
|
|
||||||
// reset signal mask (SIGCHLD blocked in Laminar::start)
|
|
||||||
sigset_t mask;
|
|
||||||
sigemptyset(&mask);
|
|
||||||
sigaddset(&mask, SIGCHLD);
|
|
||||||
sigprocmask(SIG_UNBLOCK, &mask, nullptr);
|
|
||||||
|
|
||||||
// set pgid == pid for easy killing on abort
|
|
||||||
setpgid(0, 0);
|
|
||||||
|
|
||||||
close(pfd[0]);
|
|
||||||
dup2(pfd[1], 1);
|
|
||||||
dup2(pfd[1], 2);
|
|
||||||
close(pfd[1]);
|
|
||||||
std::string buildNum = std::to_string(build);
|
|
||||||
|
|
||||||
std::string PATH = (rootPath/"cfg"/"scripts").toString(true).cStr();
|
std::string PATH = (rootPath/"cfg"/"scripts").toString(true).cStr();
|
||||||
if(const char* p = getenv("PATH")) {
|
if(const char* p = getenv("PATH")) {
|
||||||
PATH.append(":");
|
PATH.append(":");
|
||||||
PATH.append(p);
|
PATH.append(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
LSYSCALL(chdir((rootPath/currentScript.cwd).toString(true).cStr()));
|
std::string runNumStr = std::to_string(buildNum);
|
||||||
|
|
||||||
// conf file env vars
|
|
||||||
for(kj::Path& file : env) {
|
|
||||||
StringMap vars = parseConfFile((rootPath/file).toString(true).cStr());
|
|
||||||
for(auto& it : vars) {
|
|
||||||
setenv(it.first.c_str(), it.second.c_str(), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// parameterized vars
|
|
||||||
for(auto& pair : params) {
|
|
||||||
setenv(pair.first.c_str(), pair.second.c_str(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
setenv("PATH", PATH.c_str(), true);
|
setenv("PATH", PATH.c_str(), true);
|
||||||
setenv("RUN", buildNum.c_str(), true);
|
setenv("RUN", runNumStr.c_str(), true);
|
||||||
setenv("JOB", name.c_str(), true);
|
setenv("JOB", name.c_str(), true);
|
||||||
setenv("CONTEXT", context->name.c_str(), true);
|
setenv("CONTEXT", ctx->name.c_str(), true);
|
||||||
setenv("RESULT", to_string(result).c_str(), true);
|
|
||||||
setenv("LAST_RESULT", to_string(lastResult).c_str(), true);
|
setenv("LAST_RESULT", to_string(lastResult).c_str(), true);
|
||||||
setenv("WORKSPACE", (rootPath/"run"/name/"workspace").toString(true).cStr(), true);
|
setenv("WORKSPACE", (rootPath/"run"/name/"workspace").toString(true).cStr(), true);
|
||||||
setenv("ARCHIVE", (rootPath/"archive"/name/buildNum).toString(true).cStr(), true);
|
setenv("ARCHIVE", (rootPath/"archive"/name/runNumStr).toString(true).cStr(), true);
|
||||||
|
// RESULT set in leader process
|
||||||
|
|
||||||
fprintf(stderr, "[laminar] Executing %s\n", currentScript.path.toString().cStr());
|
// leader process assumes $LAMINAR_HOME as CWD
|
||||||
kj::String execPath = (rootPath/currentScript.path).toString(true);
|
LSYSCALL(chdir(rootPath.toString(true).cStr()));
|
||||||
execl(execPath.cStr(), execPath.cStr(), NULL);
|
setenv("PWD", rootPath.toString(true).cStr(), 1);
|
||||||
// cannot use LLOG because stdout/stderr are captured
|
|
||||||
fprintf(stderr, "[laminar] Failed to execute %s\n", currentScript.path.toString().cStr());
|
// We could just fork/wait over all the steps here directly, but then we
|
||||||
_exit(1);
|
// can't set a nice name for the process tree. There is pthread_setname_np,
|
||||||
|
// but it's limited to 16 characters, which most of the time probably isn't
|
||||||
|
// enough. Instead, we'll just exec ourselves and handle that in laminard's
|
||||||
|
// main() by calling leader_main()
|
||||||
|
char* procName;
|
||||||
|
asprintf(&procName, "{laminar} %s:%d", name.data(), buildNum);
|
||||||
|
execl("/proc/self/exe", procName, NULL); // does not return
|
||||||
|
_exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
LLOG(INFO, "Forked", currentScript.path, currentScript.cwd, pid);
|
// All good, we've "started"
|
||||||
close(pfd[1]);
|
startedAt = time(nullptr);
|
||||||
|
build = buildNum;
|
||||||
|
context = ctx;
|
||||||
|
|
||||||
current_pid = pid;
|
output_fd = plog[0];
|
||||||
output_fd = pfd[0];
|
close(plog[1]);
|
||||||
|
pid = leader;
|
||||||
|
|
||||||
|
// notifies the rpc client if the start command was used
|
||||||
|
started.fulfiller->fulfill();
|
||||||
|
|
||||||
|
return getPromise(pid).then([this](int status){
|
||||||
|
// The leader process passes a RunState through the return value.
|
||||||
|
// Check it didn't die abnormally, then cast to get it back.
|
||||||
|
result = WIFEXITED(status) ? RunState(WEXITSTATUS(status)) : RunState::ABORTED;
|
||||||
|
finished.fulfiller->fulfill(RunState(result));
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Run::reason() const {
|
||||||
|
return reasonMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Run::abort() {
|
||||||
|
// if the Maybe is empty, wait() was already called on this process
|
||||||
|
KJ_IF_MAYBE(p, pid) {
|
||||||
|
kill(-*p, SIGTERM);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Run::addScript(kj::Path scriptPath, kj::Path scriptWorkingDir, bool runOnAbort) {
|
|
||||||
scripts.push({kj::mv(scriptPath), kj::mv(scriptWorkingDir), runOnAbort});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Run::addEnv(kj::Path path) {
|
|
||||||
env.push_back(kj::mv(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Run::abort(bool respectRunOnAbort) {
|
|
||||||
while(scripts.size() && (!respectRunOnAbort || !scripts.front().runOnAbort))
|
|
||||||
scripts.pop();
|
|
||||||
// if the Maybe is empty, wait() was already called on this process
|
|
||||||
KJ_IF_MAYBE(p, current_pid) {
|
|
||||||
kill(-*p, SIGTERM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Run::reaped(int status) {
|
|
||||||
// once state is non-success it cannot change again
|
|
||||||
if(result != RunState::SUCCESS)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(WIFSIGNALED(status) && (WTERMSIG(status) == SIGTERM || WTERMSIG(status) == SIGKILL))
|
|
||||||
result = RunState::ABORTED;
|
|
||||||
else if(status != 0)
|
|
||||||
result = RunState::FAILED;
|
|
||||||
// otherwise preserve earlier status
|
|
||||||
|
|
||||||
finished.fulfiller->fulfill(RunState(result));
|
|
||||||
}
|
|
||||||
|
29
src/run.h
29
src/run.h
@ -57,24 +57,15 @@ public:
|
|||||||
Run(const Run&) = delete;
|
Run(const Run&) = delete;
|
||||||
Run& operator=(const Run&) = delete;
|
Run& operator=(const Run&) = delete;
|
||||||
|
|
||||||
// Call this to "start" the run with a specific number and context
|
kj::Promise<RunState> start(uint buildNum, std::shared_ptr<Context> ctx, const kj::Directory &fsHome, std::function<kj::Promise<int>(kj::Maybe<pid_t>&)> getPromise);
|
||||||
bool configure(uint buildNum, std::shared_ptr<Context> context, const kj::Directory &fsHome);
|
|
||||||
|
|
||||||
// executes the next script (if any), returning true if there is nothing
|
|
||||||
// more to be done.
|
|
||||||
bool step();
|
|
||||||
|
|
||||||
// aborts this run
|
// aborts this run
|
||||||
void abort(bool respectRunOnAbort);
|
bool abort();
|
||||||
|
|
||||||
// called when a process owned by this run has been reaped. The status
|
|
||||||
// may be used to set the run's job status
|
|
||||||
void reaped(int status);
|
|
||||||
|
|
||||||
std::string reason() const;
|
std::string reason() const;
|
||||||
|
|
||||||
kj::Promise<void>&& whenStarted() { return kj::mv(started.promise); }
|
kj::Promise<void> whenStarted() { return startedFork.addBranch(); }
|
||||||
kj::Promise<RunState>&& whenFinished() { return kj::mv(finished.promise); }
|
kj::Promise<RunState> whenFinished() { return finishedFork.addBranch(); }
|
||||||
|
|
||||||
std::shared_ptr<Context> context;
|
std::shared_ptr<Context> context;
|
||||||
RunState result;
|
RunState result;
|
||||||
@ -84,7 +75,7 @@ public:
|
|||||||
int parentBuild = 0;
|
int parentBuild = 0;
|
||||||
uint build = 0;
|
uint build = 0;
|
||||||
std::string log;
|
std::string log;
|
||||||
kj::Maybe<pid_t> current_pid;
|
kj::Maybe<pid_t> pid;
|
||||||
int output_fd;
|
int output_fd;
|
||||||
std::unordered_map<std::string, std::string> params;
|
std::unordered_map<std::string, std::string> params;
|
||||||
int timeout = 0;
|
int timeout = 0;
|
||||||
@ -105,13 +96,13 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
kj::Path rootPath;
|
kj::Path rootPath;
|
||||||
std::queue<Script> scripts;
|
|
||||||
std::list<kj::Path> env;
|
|
||||||
std::string reasonMsg;
|
std::string reasonMsg;
|
||||||
kj::PromiseFulfillerPair<void> started;
|
|
||||||
kj::PromiseFulfillerPair<RunState> finished;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
kj::PromiseFulfillerPair<void> started;
|
||||||
|
kj::ForkedPromise<void> startedFork;
|
||||||
|
kj::PromiseFulfillerPair<RunState> finished;
|
||||||
|
kj::ForkedPromise<RunState> finishedFork;
|
||||||
|
};
|
||||||
|
|
||||||
// All this below is a somewhat overengineered method of keeping track of
|
// All this below is a somewhat overengineered method of keeping track of
|
||||||
// currently executing builds (Run objects). This would probably scale
|
// currently executing builds (Run objects). This would probably scale
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
#include "tempdir.h"
|
#include "tempdir.h"
|
||||||
#include "laminar.h"
|
#include "laminar.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
#include "conf.h"
|
||||||
|
|
||||||
class LaminarFixture : public ::testing::Test {
|
class LaminarFixture : public ::testing::Test {
|
||||||
public:
|
public:
|
||||||
@ -38,7 +39,7 @@ public:
|
|||||||
settings.bind_rpc = bind_rpc.c_str();
|
settings.bind_rpc = bind_rpc.c_str();
|
||||||
settings.bind_http = bind_http.c_str();
|
settings.bind_http = bind_http.c_str();
|
||||||
settings.archive_url = "/test-archive/";
|
settings.archive_url = "/test-archive/";
|
||||||
server = new Server(ioContext);
|
server = new Server(*ioContext);
|
||||||
laminar = new Laminar(*server, settings);
|
laminar = new Laminar(*server, settings);
|
||||||
}
|
}
|
||||||
~LaminarFixture() noexcept(true) {
|
~LaminarFixture() noexcept(true) {
|
||||||
@ -47,7 +48,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
kj::Own<EventSource> eventSource(const char* path) {
|
kj::Own<EventSource> eventSource(const char* path) {
|
||||||
return kj::heap<EventSource>(ioContext, bind_http.c_str(), path);
|
return kj::heap<EventSource>(*ioContext, bind_http.c_str(), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void defineJob(const char* name, const char* scriptContent) {
|
void defineJob(const char* name, const char* scriptContent) {
|
||||||
@ -57,9 +58,61 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RunExec {
|
||||||
|
LaminarCi::JobResult result;
|
||||||
|
kj::String log;
|
||||||
|
};
|
||||||
|
|
||||||
|
RunExec runJob(const char* name, kj::Maybe<StringMap> params = nullptr) {
|
||||||
|
auto req = client().runRequest();
|
||||||
|
req.setJobName(name);
|
||||||
|
KJ_IF_MAYBE(p, params) {
|
||||||
|
auto params = req.initParams(p->size());
|
||||||
|
int i = 0;
|
||||||
|
for(auto kv : *p) {
|
||||||
|
params[i].setName(kv.first);
|
||||||
|
params[i].setValue(kv.second);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto res = req.send().wait(ioContext->waitScope);
|
||||||
|
std::string path = std::string{"/log/"} + name + "/" + std::to_string(res.getBuildNum());
|
||||||
|
kj::HttpHeaderTable headerTable;
|
||||||
|
kj::String log = kj::newHttpClient(ioContext->lowLevelProvider->getTimer(), headerTable,
|
||||||
|
*ioContext->provider->getNetwork().parseAddress(bind_http.c_str()).wait(ioContext->waitScope))
|
||||||
|
->request(kj::HttpMethod::GET, path, kj::HttpHeaders(headerTable)).response.wait(ioContext->waitScope).body
|
||||||
|
->readAllText().wait(ioContext->waitScope);
|
||||||
|
return { res.getResult(), kj::mv(log) };
|
||||||
|
}
|
||||||
|
|
||||||
|
kj::String stripLaminarLogLines(const kj::String& str) {
|
||||||
|
auto out = kj::heapString(str.size());
|
||||||
|
char *o = out.begin();
|
||||||
|
for(const char *p = str.cStr(), *e = p + str.size(); p < e;) {
|
||||||
|
const char *nl = strchrnul(p, '\n');
|
||||||
|
if(!kj::StringPtr{p}.startsWith("[laminar]")) {
|
||||||
|
memcpy(o, p, nl - p + 1);
|
||||||
|
o += nl - p + 1;
|
||||||
|
}
|
||||||
|
p = nl + 1;
|
||||||
|
}
|
||||||
|
*o = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringMap parseFromString(kj::StringPtr content) {
|
||||||
|
char tmp[16] = "/tmp/lt.XXXXXX";
|
||||||
|
int fd = mkstemp(tmp);
|
||||||
|
write(fd, content.begin(), content.size());
|
||||||
|
close(fd);
|
||||||
|
StringMap map = parseConfFile(tmp);
|
||||||
|
unlink(tmp);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
LaminarCi::Client client() {
|
LaminarCi::Client client() {
|
||||||
if(!rpc) {
|
if(!rpc) {
|
||||||
auto stream = ioContext.provider->getNetwork().parseAddress(bind_rpc).wait(ioContext.waitScope)->connect().wait(ioContext.waitScope);
|
auto stream = ioContext->provider->getNetwork().parseAddress(bind_rpc).wait(ioContext->waitScope)->connect().wait(ioContext->waitScope);
|
||||||
auto net = kj::heap<capnp::TwoPartyVatNetwork>(*stream, capnp::rpc::twoparty::Side::CLIENT);
|
auto net = kj::heap<capnp::TwoPartyVatNetwork>(*stream, capnp::rpc::twoparty::Side::CLIENT);
|
||||||
rpc = kj::heap<capnp::RpcSystem<capnp::rpc::twoparty::VatId>>(*net, nullptr).attach(kj::mv(net), kj::mv(stream));
|
rpc = kj::heap<capnp::RpcSystem<capnp::rpc::twoparty::VatId>>(*net, nullptr).attach(kj::mv(net), kj::mv(stream));
|
||||||
}
|
}
|
||||||
@ -76,7 +129,7 @@ public:
|
|||||||
Settings settings;
|
Settings settings;
|
||||||
Server* server;
|
Server* server;
|
||||||
Laminar* laminar;
|
Laminar* laminar;
|
||||||
static kj::AsyncIoContext ioContext;
|
static kj::AsyncIoContext* ioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LAMINAR_FIXTURE_H_
|
#endif // LAMINAR_FIXTURE_H_
|
||||||
|
@ -18,13 +18,14 @@
|
|||||||
///
|
///
|
||||||
#include <kj/async-unix.h>
|
#include <kj/async-unix.h>
|
||||||
#include "laminar-fixture.h"
|
#include "laminar-fixture.h"
|
||||||
|
#include "conf.h"
|
||||||
|
|
||||||
// TODO: consider handling this differently
|
// TODO: consider handling this differently
|
||||||
kj::AsyncIoContext LaminarFixture::ioContext = kj::setupAsyncIo();
|
kj::AsyncIoContext* LaminarFixture::ioContext;
|
||||||
|
|
||||||
TEST_F(LaminarFixture, EmptyStatusMessageStructure) {
|
TEST_F(LaminarFixture, EmptyStatusMessageStructure) {
|
||||||
auto es = eventSource("/");
|
auto es = eventSource("/");
|
||||||
ioContext.waitScope.poll();
|
ioContext->waitScope.poll();
|
||||||
ASSERT_EQ(1, es->messages().size());
|
ASSERT_EQ(1, es->messages().size());
|
||||||
|
|
||||||
auto json = es->messages().front().GetObject();
|
auto json = es->messages().front().GetObject();
|
||||||
@ -51,12 +52,7 @@ TEST_F(LaminarFixture, JobNotifyHomePage) {
|
|||||||
defineJob("foo", "true");
|
defineJob("foo", "true");
|
||||||
auto es = eventSource("/");
|
auto es = eventSource("/");
|
||||||
|
|
||||||
auto req = client().runRequest();
|
runJob("foo");
|
||||||
req.setJobName("foo");
|
|
||||||
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, req.send().wait(ioContext.waitScope).getResult());
|
|
||||||
|
|
||||||
// wait for job completed
|
|
||||||
ioContext.waitScope.poll();
|
|
||||||
|
|
||||||
ASSERT_EQ(4, es->messages().size());
|
ASSERT_EQ(4, es->messages().size());
|
||||||
|
|
||||||
@ -84,13 +80,8 @@ TEST_F(LaminarFixture, OnlyRelevantNotifications) {
|
|||||||
auto es1Run = eventSource("/jobs/job1/1");
|
auto es1Run = eventSource("/jobs/job1/1");
|
||||||
auto es2Run = eventSource("/jobs/job2/1");
|
auto es2Run = eventSource("/jobs/job2/1");
|
||||||
|
|
||||||
auto req1 = client().runRequest();
|
runJob("job1");
|
||||||
req1.setJobName("job1");
|
runJob("job2");
|
||||||
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, req1.send().wait(ioContext.waitScope).getResult());
|
|
||||||
auto req2 = client().runRequest();
|
|
||||||
req2.setJobName("job2");
|
|
||||||
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, req2.send().wait(ioContext.waitScope).getResult());
|
|
||||||
ioContext.waitScope.poll();
|
|
||||||
|
|
||||||
EXPECT_EQ(7, esHome->messages().size());
|
EXPECT_EQ(7, esHome->messages().size());
|
||||||
EXPECT_EQ(7, esJobs->messages().size());
|
EXPECT_EQ(7, esJobs->messages().size());
|
||||||
@ -101,3 +92,62 @@ TEST_F(LaminarFixture, OnlyRelevantNotifications) {
|
|||||||
EXPECT_EQ(4, es1Run->messages().size());
|
EXPECT_EQ(4, es1Run->messages().size());
|
||||||
EXPECT_EQ(4, es2Run->messages().size());
|
EXPECT_EQ(4, es2Run->messages().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, FailedStatus) {
|
||||||
|
defineJob("job1", "false");
|
||||||
|
auto run = runJob("job1");
|
||||||
|
ASSERT_EQ(LaminarCi::JobResult::FAILED, run.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, WorkingDirectory) {
|
||||||
|
defineJob("job1", "pwd");
|
||||||
|
auto run = runJob("job1");
|
||||||
|
ASSERT_EQ(LaminarCi::JobResult::SUCCESS, run.result);
|
||||||
|
std::string cwd{tmp.path.append(kj::Path{"run","job1","1"}).toString(true).cStr()};
|
||||||
|
EXPECT_EQ(cwd + "\n", stripLaminarLogLines(run.log).cStr());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, Environment) {
|
||||||
|
defineJob("foo", "env");
|
||||||
|
auto run = runJob("foo");
|
||||||
|
|
||||||
|
std::string ws{tmp.path.append(kj::Path{"run","foo","workspace"}).toString(true).cStr()};
|
||||||
|
std::string archive{tmp.path.append(kj::Path{"archive","foo","1"}).toString(true).cStr()};
|
||||||
|
|
||||||
|
StringMap map = parseFromString(run.log);
|
||||||
|
EXPECT_EQ("1", map["RUN"]);
|
||||||
|
EXPECT_EQ("foo", map["JOB"]);
|
||||||
|
EXPECT_EQ("success", map["RESULT"]);
|
||||||
|
EXPECT_EQ("unknown", map["LAST_RESULT"]);
|
||||||
|
EXPECT_EQ(ws, map["WORKSPACE"]);
|
||||||
|
EXPECT_EQ(archive, map["ARCHIVE"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, ParamsToEnv) {
|
||||||
|
defineJob("foo", "env");
|
||||||
|
StringMap params;
|
||||||
|
params["foo"] = "bar";
|
||||||
|
auto run = runJob("foo", params);
|
||||||
|
StringMap map = parseFromString(run.log);
|
||||||
|
EXPECT_EQ("bar", map["foo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, Abort) {
|
||||||
|
defineJob("job1", "yes");
|
||||||
|
auto req = client().runRequest();
|
||||||
|
req.setJobName("job1");
|
||||||
|
auto res = req.send();
|
||||||
|
// There isn't a nice way of knowing when the leader process is ready to
|
||||||
|
// handle SIGTERM. Just wait until it prints something to the log
|
||||||
|
ioContext->waitScope.poll();
|
||||||
|
kj::HttpHeaderTable headerTable;
|
||||||
|
char _;
|
||||||
|
kj::newHttpClient(ioContext->lowLevelProvider->getTimer(), headerTable,
|
||||||
|
*ioContext->provider->getNetwork().parseAddress(bind_http.c_str()).wait(ioContext->waitScope))
|
||||||
|
->request(kj::HttpMethod::GET, "/log/job1/1", kj::HttpHeaders(headerTable)).response.wait(ioContext->waitScope).body
|
||||||
|
->tryRead(&_, 1, 1).wait(ioContext->waitScope);
|
||||||
|
// now it should be ready to abort
|
||||||
|
ASSERT_TRUE(laminar->abort("job1", 1));
|
||||||
|
EXPECT_EQ(LaminarCi::JobResult::ABORTED, res.wait(ioContext->waitScope).getResult());
|
||||||
|
}
|
||||||
|
@ -18,10 +18,22 @@
|
|||||||
///
|
///
|
||||||
#include <kj/async-unix.h>
|
#include <kj/async-unix.h>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <kj/debug.h>
|
||||||
|
|
||||||
// gtest main supplied in order to call captureChildExit
|
#include "laminar-fixture.h"
|
||||||
|
#include "leader.h"
|
||||||
|
|
||||||
|
// gtest main supplied in order to call captureChildExit and handle process leader
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
|
if(argv[0][0] == '{')
|
||||||
|
return leader_main();
|
||||||
|
|
||||||
|
// TODO: consider handling this differently
|
||||||
|
auto ioContext = kj::setupAsyncIo();
|
||||||
|
LaminarFixture::ioContext = &ioContext;
|
||||||
|
|
||||||
kj::UnixEventPort::captureChildExit();
|
kj::UnixEventPort::captureChildExit();
|
||||||
|
//kj::_::Debug::setLogLevel(kj::_::Debug::Severity::INFO);
|
||||||
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
return RUN_ALL_TESTS();
|
return RUN_ALL_TESTS();
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
///
|
|
||||||
/// Copyright 2018 Oliver Giles
|
|
||||||
///
|
|
||||||
/// This file is part of Laminar
|
|
||||||
///
|
|
||||||
/// Laminar is free software: you can redistribute it and/or modify
|
|
||||||
/// it under the terms of the GNU General Public License as published by
|
|
||||||
/// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
/// (at your option) any later version.
|
|
||||||
///
|
|
||||||
/// Laminar is distributed in the hope that it will be useful,
|
|
||||||
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
/// GNU General Public License for more details.
|
|
||||||
///
|
|
||||||
/// You should have received a copy of the GNU General Public License
|
|
||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
///
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
#include "run.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "context.h"
|
|
||||||
#include "conf.h"
|
|
||||||
#include "tempdir.h"
|
|
||||||
|
|
||||||
class RunTest : public testing::Test {
|
|
||||||
protected:
|
|
||||||
RunTest() :
|
|
||||||
testing::Test(),
|
|
||||||
context(std::make_shared<Context>()),
|
|
||||||
tmp(),
|
|
||||||
run("foo", ParamMap{}, tmp.path.clone())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
~RunTest() noexcept {}
|
|
||||||
|
|
||||||
void wait() {
|
|
||||||
int state = -1;
|
|
||||||
waitpid(run.current_pid.orDefault(0), &state, 0);
|
|
||||||
run.reaped(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
void runAll() {
|
|
||||||
while(!run.step())
|
|
||||||
wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string readAllOutput() {
|
|
||||||
std::string res;
|
|
||||||
char tmp[64];
|
|
||||||
for(ssize_t n = read(run.output_fd, tmp, 64); n > 0; n = read(run.output_fd, tmp, 64))
|
|
||||||
res += std::string(tmp, n);
|
|
||||||
// strip the first "[laminar] executing.. line
|
|
||||||
return strchr(res.c_str(), '\n') + 1;
|
|
||||||
}
|
|
||||||
StringMap parseFromString(std::string content) {
|
|
||||||
char tmp[16] = "/tmp/lt.XXXXXX";
|
|
||||||
int fd = mkstemp(tmp);
|
|
||||||
write(fd, content.data(), content.size());
|
|
||||||
close(fd);
|
|
||||||
StringMap map = parseConfFile(tmp);
|
|
||||||
unlink(tmp);
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Context> context;
|
|
||||||
TempDir tmp;
|
|
||||||
class Run run;
|
|
||||||
|
|
||||||
void setRunLink(const char * path) {
|
|
||||||
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", run.name + ".run"},
|
|
||||||
kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT | kj::WriteMode::EXECUTABLE)) {
|
|
||||||
(f->get())->writeAll(std::string("#!/bin/sh\nexec ") + path + "\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST_F(RunTest, WorkingDirectory) {
|
|
||||||
setRunLink("pwd");
|
|
||||||
run.configure(1, context, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
std::string cwd{tmp.path.append(kj::Path{"run","foo","1"}).toString(true).cStr()};
|
|
||||||
EXPECT_EQ(cwd + "\n", readAllOutput());
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, SuccessStatus) {
|
|
||||||
setRunLink("true");
|
|
||||||
run.configure(1, context, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
EXPECT_EQ(RunState::SUCCESS, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, FailedStatus) {
|
|
||||||
setRunLink("false");
|
|
||||||
run.configure(1, context, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
EXPECT_EQ(RunState::FAILED, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, Environment) {
|
|
||||||
setRunLink("env");
|
|
||||||
run.configure(1234, context, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
|
|
||||||
std::string ws{tmp.path.append(kj::Path{"run","foo","workspace"}).toString(true).cStr()};
|
|
||||||
std::string archive{tmp.path.append(kj::Path{"archive","foo","1234"}).toString(true).cStr()};
|
|
||||||
|
|
||||||
StringMap map = parseFromString(readAllOutput());
|
|
||||||
EXPECT_EQ("1234", map["RUN"]);
|
|
||||||
EXPECT_EQ("foo", map["JOB"]);
|
|
||||||
EXPECT_EQ("success", map["RESULT"]);
|
|
||||||
EXPECT_EQ("unknown", map["LAST_RESULT"]);
|
|
||||||
EXPECT_EQ(ws, map["WORKSPACE"]);
|
|
||||||
EXPECT_EQ(archive, map["ARCHIVE"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, ParamsToEnv) {
|
|
||||||
setRunLink("env");
|
|
||||||
run.params["foo"] = "bar";
|
|
||||||
run.configure(1, context, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
StringMap map = parseFromString(readAllOutput());
|
|
||||||
EXPECT_EQ("bar", map["foo"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, Abort) {
|
|
||||||
setRunLink("yes");
|
|
||||||
run.configure(1, context, *tmp.fs);
|
|
||||||
run.step();
|
|
||||||
usleep(200); // TODO fix
|
|
||||||
run.abort(false);
|
|
||||||
wait();
|
|
||||||
EXPECT_EQ(RunState::ABORTED, run.result);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user