diff --git a/UserManual.md b/UserManual.md index c70a3f9..271a775 100644 --- a/UserManual.md +++ b/UserManual.md @@ -142,6 +142,14 @@ laminarc queue test-host test-target This is against the design principles of Laminar and was deliberately excluded. Laminar's web UI is strictly read-only, making it simple to deploy in mixed-permission or public environments without an authentication layer. Furthermore, Laminar tries to encourage ideal continuous integration, where manual triggering is an anti-pattern. Want to make a release? Push a git tag and implement a post-receive hook. Want to re-run a build due to sporadic failure/flaky tests? Fix the tests locally and push a patch. Experience shows that a manual trigger such as a "Build Now" button is often used as a crutch to avoid doing the correct thing, negatively impacting traceability and quality. +## Listing jobs from the command line + +`laminarc` may be used to inspect the server state: + +- `laminarc show-jobs`: Lists all files matching `/var/lib/laminar/cfg/jobs/*.run` on the server side. +- `laminarc show-running`: Lists all currently running jobs and their run numbers. +- `laminarc show-queued`: Lists all jobs waiting in the queue. + ## Triggering a job at a certain time This is what `cron` is for. To trigger a build of `hello` every day at 0300, add @@ -433,7 +441,9 @@ make -C src --- -# Abort on timeout +# Aborting running jobs + +## After a timeout To configure a maximum execution time in seconds for a job, add a line to `/var/lib/laminar/cfg/jobs/JOBNAME.conf`: @@ -441,6 +451,10 @@ To configure a maximum execution time in seconds for a job, add a line to `/var/ TIMEOUT=120 ``` +## Manually + +`laminarc abort $JOBNAME $NUMBER` + --- # Nodes and Tags @@ -607,5 +621,9 @@ Finally, variables supplied on the command-line call to `laminarc queue`, `lamin - `start [JOB [PARAMS...]]...` starts one or more jobs with optional parameters, returning when the jobs begin execution. - `run [JOB [PARAMS...]]...` triggers one or more jobs with optional parameters and waits for the completion of all jobs. Returns a non-zero error code if any job failed. - `set [VARIABLE=VALUE]...` sets one or more variables to be exported in subsequent scripts for the run identified by the `$JOB` and `$RUN` environment variables +- `show-jobs` shows the known jobs on the server (`$LAMINAR_HOME/cfg/jobs/*.run`). +- `show-running` shows the currently running jobs with their numbers. +- `show-queued` shows the names of the jobs waiting in the queue. +- `abort JOB NUMBER` manually aborts a currently running job by name and number. `laminarc` connects to `laminard` using the address supplied by the `LAMINAR_HOST` environment variable. If it is not set, `laminarc` will first attempt to use `LAMINAR_BIND_RPC`, which will be available if `laminarc` is executed from a script within `laminard`. If neither `LAMINAR_HOST` nor `LAMINAR_BIND_RPC` is set, `laminarc` will assume a default host of `unix-abstract:laminar`. diff --git a/src/client.cpp b/src/client.cpp index d72ecec..a243b62 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -173,8 +173,8 @@ int main(int argc, char** argv) { char* name = argv[2]; *eq++ = '\0'; char* val = eq; - req.setJobName(job); - req.setBuildNum(atoi(num)); + req.getRun().setJob(job); + req.getRun().setBuildNum(atoi(num)); req.getParam().setName(name); req.getParam().setValue(val); req.send().wait(waitScope); @@ -182,6 +182,40 @@ int main(int argc, char** argv) { fprintf(stderr, "Missing $JOB or $RUN or param is not in the format key=value\n"); return EINVAL; } + } else if(strcmp(argv[1], "abort") == 0) { + if(argc != 4) { + fprintf(stderr, "Usage %s abort \n", argv[0]); + return EINVAL; + } + auto req = laminar.abortRequest(); + req.getRun().setJob(argv[2]); + req.getRun().setBuildNum(atoi(argv[3])); + if(req.send().wait(waitScope).getResult() != LaminarCi::MethodResult::SUCCESS) + ret = EFAILED; + } else if(strcmp(argv[1], "show-jobs") == 0) { + if(argc != 2) { + fprintf(stderr, "Usage: %s show-jobs\n", argv[0]); + return EINVAL; + } + for(auto it : laminar.listKnownRequest().send().wait(waitScope).getResult()) { + printf("%s\n", it.cStr()); + } + } else if(strcmp(argv[1], "show-queued") == 0) { + if(argc != 2) { + fprintf(stderr, "Usage: %s show-queued\n", argv[0]); + return EINVAL; + } + for(auto it : laminar.listQueuedRequest().send().wait(waitScope).getResult()) { + printf("%s\n", it.cStr()); + } + } else if(strcmp(argv[1], "show-running") == 0) { + if(argc != 2) { + fprintf(stderr, "Usage: %s show-running\n", argv[0]); + return EINVAL; + } + for(auto it : laminar.listRunningRequest().send().wait(waitScope).getResult()) { + printf("%s:%d\n", it.getJob().cStr(), it.getBuildNum()); + } } else { fprintf(stderr, "Unknown command %s\n", argv[1]); return EINVAL; diff --git a/src/interface.h b/src/interface.h index 6bd840e..2fdae7a 100644 --- a/src/interface.h +++ b/src/interface.h @@ -128,6 +128,15 @@ struct LaminarInterface { // the environment of subsequent scripts. virtual bool setParam(std::string job, uint buildNum, std::string param, std::string value) = 0; + // Gets the list of jobs currently waiting in the execution queue + virtual const std::list>& listQueuedJobs() = 0; + + // Gets the list of currently executing jobs + virtual const RunSet& listRunningJobs() = 0; + + // Gets the list of known jobs - scans cfg/jobs for *.run files + virtual std::list listKnownJobs() = 0; + // Fetches the content of an artifact given its filename relative to // $LAMINAR_HOME/archive. Ideally, this would instead be served by a // proper web server which handles this url. @@ -143,6 +152,9 @@ struct LaminarInterface { // which handles this url. virtual std::string getCustomCss() = 0; + // Aborts a single job + virtual bool abort(std::string job, uint buildNum) = 0; + // Abort all running jobs virtual void abortAll() = 0; diff --git a/src/laminar.capnp b/src/laminar.capnp index 7f5d0e3..9e9e5c7 100644 --- a/src/laminar.capnp +++ b/src/laminar.capnp @@ -5,7 +5,16 @@ interface LaminarCi { queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult); start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32); run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32); - set @3 (jobName :Text, buildNum :UInt32, param :JobParam) -> (result :MethodResult); + set @3 (run :Run, param :JobParam) -> (result :MethodResult); + listQueued @4 () -> (result :List(Text)); + listRunning @5 () -> (result :List(Run)); + listKnown @6 () -> (result :List(Text)); + abort @7 (run :Run) -> (result :MethodResult); + + struct Run { + job @0 :Text; + buildNum @1 :UInt32; + } struct JobParam { name @0 :Text; diff --git a/src/laminar.cpp b/src/laminar.cpp index 99bdb48..a4f6ae1 100644 --- a/src/laminar.cpp +++ b/src/laminar.cpp @@ -130,6 +130,25 @@ bool Laminar::setParam(std::string job, uint buildNum, std::string param, std::s return false; } +const std::list>& Laminar::listQueuedJobs() { + return queuedJobs; +} + +const RunSet& Laminar::listRunningJobs() { + return activeJobs; +} + +std::list Laminar::listKnownJobs() { + std::list res; + KJ_IF_MAYBE(dir, fsHome->tryOpenSubdir(kj::Path{"cfg","jobs"})) { + for(kj::Directory::Entry& entry : (*dir)->listEntries()) { + if(entry.type == kj::FsNode::Type::FILE && entry.name.endsWith(".run")) { + res.emplace_back(entry.name.cStr(), entry.name.findLast('.').orDefault(0)); + } + } + } + return res; +} void Laminar::populateArtifacts(Json &j, std::string job, uint num) const { kj::Path runArchive{job,std::to_string(num)}; @@ -575,6 +594,14 @@ void Laminar::notifyConfigChanged() assignNewJobs(); } +bool Laminar::abort(std::string job, uint buildNum) { + if(Run* run = activeRun(job, buildNum)) { + run->abort(true); + return true; + } + return false; +} + void Laminar::abortAll() { for(std::shared_ptr run : activeJobs) { run->abort(false); diff --git a/src/laminar.h b/src/laminar.h index 2e2abc3..d5c2c63 100644 --- a/src/laminar.h +++ b/src/laminar.h @@ -56,9 +56,13 @@ public: void sendStatus(LaminarClient* client) override; bool setParam(std::string job, uint buildNum, std::string param, std::string value) override; + const std::list>& listQueuedJobs() override; + const RunSet& listRunningJobs() override; + std::list listKnownJobs() override; kj::Maybe> getArtefact(std::string path) override; bool handleBadgeRequest(std::string job, std::string& badge) override; std::string getCustomCss() override; + bool abort(std::string job, uint buildNum) override; void abortAll() override; void notifyConfigChanged() override; diff --git a/src/server.cpp b/src/server.cpp index cc7351e..a793dc0 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -117,8 +117,8 @@ public: // Set a parameter on a running build kj::Promise set(SetContext context) override { - std::string jobName = context.getParams().getJobName(); - uint buildNum = context.getParams().getBuildNum(); + 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, @@ -129,6 +129,52 @@ public: return kj::READY_NOW; } + // List jobs in queue + kj::Promise listQueued(ListQueuedContext context) override { + const std::list>& queue = laminar.listQueuedJobs(); + auto res = context.getResults().initResult(queue.size()); + int i = 0; + for(auto it : queue) { + res.set(i++, it->name); + } + return kj::READY_NOW; + } + + // List running jobs + kj::Promise listRunning(ListRunningContext context) override { + const RunSet& active = laminar.listRunningJobs(); + auto res = context.getResults().initResult(active.size()); + int i = 0; + for(auto it : active) { + res[i].setJob(it->name); + res[i].setBuildNum(it->build); + i++; + } + return kj::READY_NOW; + } + + // List known jobs + kj::Promise listKnown(ListKnownContext context) override { + std::list known = laminar.listKnownJobs(); + auto res = context.getResults().initResult(known.size()); + int i = 0; + for(auto it : known) { + res.set(i++, it); + } + return kj::READY_NOW; + } + + kj::Promise abort(AbortContext context) override { + std::string jobName = context.getParams().getRun().getJob(); + uint buildNum = context.getParams().getRun().getBuildNum(); + LLOG(INFO, "RPC abort", jobName, buildNum); + LaminarCi::MethodResult result = laminar.abort(jobName, buildNum) + ? LaminarCi::MethodResult::SUCCESS + : LaminarCi::MethodResult::FAILED; + context.getResults().setResult(result); + return kj::READY_NOW; + } + private: // Helper to convert an RPC parameter list to a hash map ParamMap params(const capnp::List::Reader& paramReader) { diff --git a/test/test-server.cpp b/test/test-server.cpp index 28b239a..81b5a78 100644 --- a/test/test-server.cpp +++ b/test/test-server.cpp @@ -49,8 +49,13 @@ public: MOCK_METHOD1(deregisterWaiter, void(LaminarWaiter* waiter)); MOCK_METHOD1(sendStatus, void(LaminarClient* client)); MOCK_METHOD4(setParam, bool(std::string job, uint buildNum, std::string param, std::string value)); + MOCK_METHOD0(listQueuedJobs, const std::list>&()); + MOCK_METHOD0(listRunningJobs, const RunSet&()); + MOCK_METHOD0(listKnownJobs, std::list()); + MOCK_METHOD0(getCustomCss, std::string()); MOCK_METHOD2(handleBadgeRequest, bool(std::string, std::string&)); + MOCK_METHOD2(abort, bool(std::string, uint)); MOCK_METHOD0(abortAll, void()); MOCK_METHOD0(notifyConfigChanged, void()); };