mirror of
https://github.com/ohwgiles/laminar.git
synced 2026-10-23 20:08:44 +00:00
Compare commits
27 Commits
feature/co
...
0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f7e8d2455 | ||
|
|
d6be7f5079 | ||
|
|
09a208ebeb | ||
|
|
ae560b9de4 | ||
|
|
4554039703 | ||
|
|
cd64be2cea | ||
|
|
559a6480ee | ||
|
|
1bb545e3f9 | ||
|
|
de948c149e | ||
|
|
0f2e601692 | ||
|
|
c690be3a90 | ||
|
|
1cc6cc6ae9 | ||
|
|
d5cfa3b94e | ||
|
|
081becf23a | ||
|
|
6c61fb3111 | ||
|
|
eadcdbdf9c | ||
|
|
2e54773e83 | ||
|
|
f981491a34 | ||
|
|
a7e6094d01 | ||
|
|
6f366e21cc | ||
|
|
2bf04d8157 | ||
|
|
40b708c594 | ||
|
|
b8ab43b508 | ||
|
|
3fde38c6b8 | ||
|
|
304ef797b8 | ||
|
|
e6f338ceb2 | ||
|
|
0384fc9a0a |
@@ -1,5 +1,5 @@
|
|||||||
###
|
###
|
||||||
### Copyright 2015-2019 Oliver Giles
|
### Copyright 2015-2020 Oliver Giles
|
||||||
###
|
###
|
||||||
### This file is part of Laminar
|
### This file is part of Laminar
|
||||||
###
|
###
|
||||||
@@ -22,9 +22,22 @@ cmake_policy(SET CMP0058 NEW)
|
|||||||
|
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||||
|
|
||||||
add_definitions("-std=c++14 -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare")
|
add_definitions("-std=c++17 -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare")
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror -DDEBUG")
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror -DDEBUG")
|
||||||
|
|
||||||
|
# Allow passing in the version string, for e.g. patched/packaged versions
|
||||||
|
if(NOT LAMINAR_VERSION AND EXISTS ${CMAKE_SOURCE_DIR}/.git)
|
||||||
|
execute_process(COMMAND git describe --tags --abbrev=8 --dirty
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE LAMINAR_VERSION
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
endif()
|
||||||
|
if(NOT LAMINAR_VERSION)
|
||||||
|
set(LAMINAR_VERSION xx-unversioned)
|
||||||
|
endif()
|
||||||
|
set_source_files_properties(src/version.cpp PROPERTIES COMPILE_DEFINITIONS
|
||||||
|
LAMINAR_VERSION=${LAMINAR_VERSION})
|
||||||
|
|
||||||
# This macro takes a list of files, gzips them and converts the output into
|
# This macro takes a list of files, gzips them and converts the output into
|
||||||
# object files so they can be linked directly into the application.
|
# object files so they can be linked directly into the application.
|
||||||
# ld generates symbols based on the string argument given to its executable,
|
# ld generates symbols based on the string argument given to its executable,
|
||||||
@@ -62,7 +75,7 @@ add_custom_command(OUTPUT laminar.capnp.c++ laminar.capnp.h
|
|||||||
|
|
||||||
# Zip and compile statically served resources
|
# Zip and compile statically served resources
|
||||||
generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js
|
generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js
|
||||||
favicon.ico favicon-152.png icon.png)
|
style.css manifest.webmanifest favicon.ico favicon-152.png icon.png)
|
||||||
|
|
||||||
# The code that allows dynamic modifying of index.html requires knowing its original size
|
# The code that allows dynamic modifying of index.html requires knowing its original size
|
||||||
add_custom_command(OUTPUT index_html_size.h
|
add_custom_command(OUTPUT index_html_size.h
|
||||||
@@ -78,22 +91,22 @@ file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v1.3.0/ansi_up.js
|
|||||||
js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6)
|
js/ansi_up.js EXPECTED_MD5 158566dc1ff8f2804de972f7e841e2f6)
|
||||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
||||||
js/Chart.min.js EXPECTED_MD5 f6c8efa65711e0cbbc99ba72997ecd0e)
|
js/Chart.min.js EXPECTED_MD5 f6c8efa65711e0cbbc99ba72997ecd0e)
|
||||||
file(DOWNLOAD https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css
|
|
||||||
css/bootstrap.min.css EXPECTED_MD5 5d5357cb3704e1f43a1f5bfed2aebf42)
|
|
||||||
# ...and compile them
|
# ...and compile them
|
||||||
generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
||||||
js/ansi_up.js js/Chart.min.js css/bootstrap.min.css)
|
js/ansi_up.js js/Chart.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
|
||||||
|
src/version.cpp
|
||||||
laminar.capnp.c++
|
laminar.capnp.c++
|
||||||
index_html_size.h
|
index_html_size.h
|
||||||
)
|
)
|
||||||
@@ -103,7 +116,7 @@ add_executable(laminard ${LAMINARD_CORE_SOURCES} src/main.cpp ${COMPRESSED_BINS}
|
|||||||
target_link_libraries(laminard capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
target_link_libraries(laminard capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
||||||
|
|
||||||
## Client
|
## Client
|
||||||
add_executable(laminarc src/client.cpp laminar.capnp.c++)
|
add_executable(laminarc src/client.cpp src/version.cpp laminar.capnp.c++)
|
||||||
target_link_libraries(laminarc capnp-rpc capnp kj-async kj pthread)
|
target_link_libraries(laminarc capnp-rpc capnp kj-async kj pthread)
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
@@ -111,15 +124,18 @@ 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()
|
||||||
|
|
||||||
set(SYSTEMD_UNITDIR lib/systemd/system CACHE PATH "Path to systemd unit files")
|
set(SYSTEMD_UNITDIR /lib/systemd/system CACHE PATH "Path to systemd unit files")
|
||||||
set(BASH_COMPLETIONS_DIR usr/share/bash-completion/completions CACHE PATH "Path to bash completions directory")
|
set(BASH_COMPLETIONS_DIR /usr/share/bash-completion/completions CACHE PATH "Path to bash completions directory")
|
||||||
set(ZSH_COMPLETIONS_DIR usr/share/zsh/site-functions CACHE PATH "Path to zsh completions directory")
|
set(ZSH_COMPLETIONS_DIR /usr/share/zsh/site-functions CACHE PATH "Path to zsh completions directory")
|
||||||
install(TARGETS laminard laminarc RUNTIME DESTINATION usr/bin)
|
install(TARGETS laminard RUNTIME DESTINATION sbin)
|
||||||
install(FILES etc/laminar.service DESTINATION ${SYSTEMD_UNITDIR})
|
install(TARGETS laminarc RUNTIME DESTINATION bin)
|
||||||
install(FILES etc/laminar.conf DESTINATION etc)
|
install(FILES etc/laminar.conf DESTINATION /etc)
|
||||||
install(FILES etc/laminarc-completion.bash DESTINATION ${BASH_COMPLETIONS_DIR} RENAME laminarc)
|
install(FILES etc/laminarc-completion.bash DESTINATION ${BASH_COMPLETIONS_DIR} RENAME laminarc)
|
||||||
install(FILES etc/laminarc-completion.zsh DESTINATION ${ZSH_COMPLETIONS_DIR} RENAME _laminarc)
|
install(FILES etc/laminarc-completion.zsh DESTINATION ${ZSH_COMPLETIONS_DIR} RENAME _laminarc)
|
||||||
|
|
||||||
|
configure_file(etc/laminar.service.in laminar.service @ONLY)
|
||||||
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/laminar.service DESTINATION ${SYSTEMD_UNITDIR})
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ Then compile and install laminar with:
|
|||||||
```bash
|
```bash
|
||||||
git clone https://github.com/ohwgiles/laminar.git
|
git clone https://github.com/ohwgiles/laminar.git
|
||||||
cd laminar
|
cd laminar
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/
|
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||||
make -j4
|
make -j4
|
||||||
|
# Warning: the following will overwrite an existing /etc/laminar.conf
|
||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -34,3 +35,7 @@ sudo make install
|
|||||||
## Packaging for distributions
|
## Packaging for distributions
|
||||||
|
|
||||||
The `pkg` directory contains shell scripts which use docker to build native packages (deb,rpm) for common Linux distributions. Note that these are very simple packages which may not completely conform to the distribution's packaging guidelines, however they may serve as a starting point for creating an official package, or may be useful if the official package lags.
|
The `pkg` directory contains shell scripts which use docker to build native packages (deb,rpm) for common Linux distributions. Note that these are very simple packages which may not completely conform to the distribution's packaging guidelines, however they may serve as a starting point for creating an official package, or may be useful if the official package lags.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Issues and pull requests via GitHub are most welcome. All pull requests must adhere to the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||||
|
|||||||
142
UserManual.md
142
UserManual.md
@@ -96,10 +96,6 @@ Laminar uses Sever Sent Events to provide a responsive, auto-updating display wi
|
|||||||
|
|
||||||
If you use a reverse proxy to host Laminar at a subfolder instead of a subdomain root, the `<base href>` needs to be updated to ensure all links point to their proper targets. This can be done by setting `LAMINAR_BASE_URL` in `/etc/laminar.conf`.
|
If you use a reverse proxy to host Laminar at a subfolder instead of a subdomain root, the `<base href>` needs to be updated to ensure all links point to their proper targets. This can be done by setting `LAMINAR_BASE_URL` in `/etc/laminar.conf`.
|
||||||
|
|
||||||
## Set the page title
|
|
||||||
|
|
||||||
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. For further WebUI customization, consider using a [custom style sheet](#Customizing-the-WebUI).
|
|
||||||
|
|
||||||
## More configuration options
|
## More configuration options
|
||||||
|
|
||||||
See the [reference section](#Service-configuration-file)
|
See the [reference section](#Service-configuration-file)
|
||||||
@@ -327,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
|
||||||
@@ -439,7 +433,7 @@ echo Initializing workspace
|
|||||||
git clone git@example.com:company/project.git .
|
git clone git@example.com:company/project.git .
|
||||||
```
|
```
|
||||||
|
|
||||||
**CAUTION**: By default, laminar permits multiple simultaneous runs of the same job. If a job can **modify** the workspace, this might result in inconsistent builds when simultaneous runs access the same content. This is unlikely to be an issue for nightly builds, but for SCM-triggered builds it will be. To solve this, use [nodes](#Nodes-and-Tags) to restrict simultaneous execution of jobs, or consider [flock](https://linux.die.net/man/1/flock).
|
**CAUTION**: By default, laminar permits multiple simultaneous runs of the same job. If a job can **modify** the workspace, this might result in inconsistent builds when simultaneous runs access the same content. This is unlikely to be an issue for nightly builds, but for SCM-triggered builds it will be. To solve this, use [contexts](#Contexts) to restrict simultaneous execution of jobs, or consider [flock](https://linux.die.net/man/1/flock).
|
||||||
|
|
||||||
The following example uses [flock](https://linux.die.net/man/1/flock) to efficiently share a git repository workspace between multiple simultaneous builds:
|
The following example uses [flock](https://linux.die.net/man/1/flock) to efficiently share a git repository workspace between multiple simultaneous builds:
|
||||||
|
|
||||||
@@ -490,62 +484,65 @@ TIMEOUT=120
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Nodes and Tags
|
# Contexts
|
||||||
|
|
||||||
In Laminar, a *node* is an abstract concept allowing more fine-grained control over job execution scheduling. Each node can be defined to support an integer number of *executors*, which defines how many runs can be executed simultaneously.
|
In Laminar, each run of a job is associated with a context. The context defines an integer number of *executors*, which is the amount of runs which the context will accept simultaneously. A context may also provide additional environment variables.
|
||||||
|
|
||||||
A typical example would be to allow only a few concurrent CPU-intensive jobs (such as compilation), while simultaneously allowing many more less-intensive jobs (such as monitoring or remote jobs). To create a node named `build` with 3 executors, create the file `/var/lib/laminar/cfg/nodes/build.conf` with the following content:
|
Uses for this feature include limiting the amount of concurrent CPU-intensive jobs (such as compilation); and controlling access to jobs [executed remotely](#Remote-jobs).
|
||||||
|
|
||||||
```
|
If no contexts are defined, Laminar will behave as if there is a single context named "default", with `6` executors. This is a reasonable default that allows simple setups to work without any consideration of contexts.
|
||||||
EXECUTORS=3
|
|
||||||
```
|
|
||||||
|
|
||||||
To associate jobs with nodes, laminar uses *tags*. Tags may be applied to nodes and jobs. If a node has tags, only jobs with a matching tag will be executed on it. If a node has no tags, it will accept any job. To tag a node, add them to `/var/lib/laminar/cfg/nodes/NODENAME.conf`:
|
## Defining a context
|
||||||
|
|
||||||
```
|
To create a context named "my-env" which only allows a single run at once, create `/var/lib/laminar/cfg/contexts/my-env.conf` with the content:
|
||||||
EXECUTORS=3
|
|
||||||
TAGS=tag1,tag2
|
|
||||||
```
|
|
||||||
|
|
||||||
To add a tag to a job, add the following to `/var/lib/laminar/cfg/jobs/JOBNAME.conf`:
|
|
||||||
|
|
||||||
```
|
|
||||||
TAGS=tag2
|
|
||||||
```
|
|
||||||
|
|
||||||
If Laminar cannot find any node configuration, it will assume a single node with 6 executors and no tags.
|
|
||||||
|
|
||||||
## Grouping jobs with tags
|
|
||||||
|
|
||||||
Tags are also used to group jobs in the web UI. Each tag will presented as a tab in the "Jobs" page.
|
|
||||||
|
|
||||||
## Node scripts
|
|
||||||
|
|
||||||
If `/var/lib/laminar/cfg/nodes/NODENAME.before` exists, it will be executed before the run script of a job scheduled to that node. Similarly, if `/var/lib/laminar/cfg/nodes/NODENAME.after` exists, it will be executed after the run script of a job scheduled to that node.
|
|
||||||
|
|
||||||
## Node environment
|
|
||||||
|
|
||||||
If `/var/lib/laminar/cfg/nodes/NODENAME.env` exists and can be parsed as a list of `KEY=VALUE` pairs, these variables will be exposed as part of the run's environment.
|
|
||||||
|
|
||||||
# Remote jobs
|
|
||||||
|
|
||||||
Laminar provides no specific support, `bash`, `ssh` and possibly NFS are all you need. For example, consider two identical target devices on which test jobs can be run in parallel. You might create a [node](#Nodes-and-Tags) for each, `/var/lib/laminar/cfg/nodes/target{1,2}.conf` with a common tag:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
EXECUTORS=1
|
EXECUTORS=1
|
||||||
TAGS=remote-target
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In each node's `.env` file, set the individual device's IP address:
|
## Associating a job with a context
|
||||||
|
|
||||||
|
When trying to start a job, laminar will wait until the job can be matched to a context which has at least one free executor. You can define which contexts the job will associate with by setting, for example,
|
||||||
|
|
||||||
|
```
|
||||||
|
CONTEXTS=my-env-*,special_context
|
||||||
|
```
|
||||||
|
|
||||||
|
in `/var/lib/laminar/cfg/jobs/JOB.conf`. For each of the patterns in the comma-separated list `CONTEXTS`, Laminar will iterate over the known contexts and associate the run with the first context with free executors. Patterns are [glob expressions](http://man7.org/linux/man-pages/man7/glob.7.html).
|
||||||
|
|
||||||
|
If `CONTEXTS` is empty or absent (or if `JOB.conf` doesn't exist), laminar will behave as if `CONTEXTS=default` were defined.
|
||||||
|
|
||||||
|
## Adding environment to a context
|
||||||
|
|
||||||
|
Append desired environment variables to `/var/lib/laminar/cfg/contexts/CONTEXT_NAME.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
DUT_IP=192.168.3.2
|
||||||
|
FOO=bar
|
||||||
|
```
|
||||||
|
|
||||||
|
This environment will then be available the run script of jobs associated with this context. Note that these definitions are not expanded by a shell, so `FOO="bar"` would result in a variable `FOO` whose contents *include* double-quotes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remote jobs
|
||||||
|
|
||||||
|
Laminar provides no specific support, `bash`, `ssh` and possibly NFS are all you need. For example, consider two identical target devices on which test jobs can be run in parallel. You might create a [context](#Contexts) for each, `/var/lib/laminar/cfg/contexts/target{1,2}.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
EXECUTORS=1
|
||||||
|
```
|
||||||
|
|
||||||
|
In each context's `.env` file, set the individual device's IP address:
|
||||||
|
|
||||||
```
|
```
|
||||||
TARGET_IP=192.168.0.123
|
TARGET_IP=192.168.0.123
|
||||||
```
|
```
|
||||||
|
|
||||||
And tag the job accordingly in `/var/lib/laminar/cfg/jobs/myproject-test.conf`:
|
And mark the job accordingly in `/var/lib/laminar/cfg/jobs/myproject-test.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
TAGS=remote-target
|
CONTEXTS=target*
|
||||||
```
|
```
|
||||||
|
|
||||||
This means the job script `/var/lib/laminar/cfg/jobs/myproject-test.run` can be generic:
|
This means the job script `/var/lib/laminar/cfg/jobs/myproject-test.run` can be generic:
|
||||||
@@ -579,11 +576,51 @@ EOF
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Colours in log output
|
||||||
|
|
||||||
|
Laminar's frontend supports ANSI colours using the [ansi-up library](https://github.com/drudru/ansi_up). Unfortunately, there is no standard way of convincing applications to output colours when not connected to a tty. It is recommended to set [CLICOLOR_FORCE=1](https://bixense.com/clicolors/) in Laminar's [global environment file](#Environment-variables), plus any of the following environment variables that may be relevant (please submit more):
|
||||||
|
|
||||||
|
* git: `GIT_CONFIG_PARAMETERS='color.status=always' 'color.ui=always'`
|
||||||
|
* google test: `GTEST_COLOR=1`
|
||||||
|
* grep: `GREP_OPTIONS=--color=always`
|
||||||
|
|
||||||
|
More intrusive options for other common tools which do not support enabling colours via environment variable:
|
||||||
|
|
||||||
|
* gcc and clang: Add `-fdiagnostics-color=always` to compile flags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Customizing the WebUI
|
# Customizing the WebUI
|
||||||
|
|
||||||
If it exists, the file `/var/lib/laminar/custom/style.css` will be served by laminar and may be used to change the appearance of Laminar's WebUI.
|
## Organising jobs into groups
|
||||||
|
|
||||||
This directory is also a good place to add any extra assets needed for this customization, but note that in this case you will need to serve this directory directly from your [HTTP reverse proxy](#Service-configuration) (highly recommended).
|
*Groups* may be used to organise the "Jobs" page into tabs. Edit `/var/lib/laminar/cfg/groups.conf` and define the matched jobs as a [javascript regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Builds=compile-\w+
|
||||||
|
My Fav Jobs=^(target-foo-(build|deploy)|run-benchmarks)$
|
||||||
|
All=.*
|
||||||
|
```
|
||||||
|
|
||||||
|
Changes to this file are detected immediately and will be visible on next page refresh.
|
||||||
|
|
||||||
|
## Adding a description to a job
|
||||||
|
|
||||||
|
Edit `/var/lib/laminar/cfg/jobs/$JOBNAME.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
DESCRIPTION=Anything here will appear on the job page in the frontend <em>unescaped</em>.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting the page title
|
||||||
|
|
||||||
|
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. Laminar must be restarted for this change to take effect.
|
||||||
|
|
||||||
|
## Custom HTML template
|
||||||
|
|
||||||
|
If it exists, the file `/var/lib/laminar/custom/index.html` will be served by laminar instead of the default markup that is bundled into the Laminar binary. This file can be used to change any aspect of Laminar's WebUI, for example adding menu links or adding a custom stylesheet. Any required assets will need to be served directly from your [HTTP reverse proxy](#Service-configuration) or other HTTP server.
|
||||||
|
|
||||||
|
An example customization can be found at [cweagans/semantic-laminar-theme](https://github.com/cweagans/semantic-laminar-theme).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -614,15 +651,13 @@ Laminar will serve a job's current status as a pretty badge at the url `/badge/J
|
|||||||
|
|
||||||
## Script execution order
|
## Script execution order
|
||||||
|
|
||||||
When `$JOB` is triggered on `$NODE`, the following scripts (relative to `$LAMINAR_HOME/cfg`) may be executed:
|
When `$JOB` is triggered, the following scripts (relative to `$LAMINAR_HOME/cfg`) may be executed:
|
||||||
|
|
||||||
- `jobs/$JOB.init` if the [workspace](#Data-sharing-and-Workspaces) did not exist
|
- `jobs/$JOB.init` if the [workspace](#Data-sharing-and-Workspaces) did not exist
|
||||||
- `before`
|
- `before`
|
||||||
- `nodes/$NODE.before`
|
|
||||||
- `jobs/$JOB.before`
|
- `jobs/$JOB.before`
|
||||||
- `jobs/$JOB.run`
|
- `jobs/$JOB.run`
|
||||||
- `jobs/$JOB.after`
|
- `jobs/$JOB.after`
|
||||||
- `nodes/$NODE.after`
|
|
||||||
- `after`
|
- `after`
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
@@ -635,15 +670,18 @@ The following variables are available in run scripts:
|
|||||||
- `LAST_RESULT` string previous run status
|
- `LAST_RESULT` string previous run status
|
||||||
- `WORKSPACE` path to this job's workspace
|
- `WORKSPACE` path to this job's workspace
|
||||||
- `ARCHIVE` path to this run's archive
|
- `ARCHIVE` path to this run's archive
|
||||||
|
- `CONTEXT` the context of this run
|
||||||
|
|
||||||
In addition, `$LAMINAR_HOME/cfg/scripts` is prepended to `$PATH`. See [helper scripts](#Helper-scripts).
|
In addition, `$LAMINAR_HOME/cfg/scripts` is prepended to `$PATH`. See [helper scripts](#Helper-scripts).
|
||||||
|
|
||||||
Laminar will also export variables in the form `KEY=VALUE` found in these files:
|
Laminar will also export variables in the form `KEY=VALUE` found in these files:
|
||||||
|
|
||||||
- `env`
|
- `env`
|
||||||
- `nodes/$NODE.env`
|
- `contexts/$CONTEXT.env`
|
||||||
- `jobs/$JOB.env`
|
- `jobs/$JOB.env`
|
||||||
|
|
||||||
|
Note that definitions in these files are not expanded by a shell, so `FOO="bar"` would result in a variable `FOO` whose contents *include* double-quotes.
|
||||||
|
|
||||||
Finally, variables supplied on the command-line call to `laminarc queue`, `laminarc start` or `laminarc run` will be available. See [parameterized runs](#Parameterized-runs)
|
Finally, variables supplied on the command-line call to `laminarc queue`, `laminarc start` or `laminarc run` will be available. See [parameterized runs](#Parameterized-runs)
|
||||||
|
|
||||||
## laminarc
|
## laminarc
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ RUN apk add --no-cache --virtual .build -X http://dl-3.alpinelinux.org/alpine/ed
|
|||||||
zlib-dev \
|
zlib-dev \
|
||||||
rapidjson-dev && \
|
rapidjson-dev && \
|
||||||
cd /build/laminar && \
|
cd /build/laminar && \
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ && \
|
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && \
|
||||||
make -j4 && \
|
make -j4 && \
|
||||||
make install && \
|
make install && \
|
||||||
apk del .build && \
|
apk del .build && \
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
User=laminar
|
User=laminar
|
||||||
EnvironmentFile=-/etc/laminar.conf
|
EnvironmentFile=-/etc/laminar.conf
|
||||||
ExecStart=/usr/bin/laminard
|
ExecStart=@CMAKE_INSTALL_PREFIX@/sbin/laminard
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -54,7 +54,7 @@ Lightweight Continuous Integration Service
|
|||||||
%prep
|
%prep
|
||||||
|
|
||||||
%build
|
%build
|
||||||
cmake3 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ -DSYSTEMD_UNITDIR=%{_unitdir} %{_sourcedir}/laminar-$VERSION
|
cmake3 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -DSYSTEMD_UNITDIR=%{_unitdir} %{_sourcedir}/laminar-$VERSION
|
||||||
pwd
|
pwd
|
||||||
make
|
make
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ make
|
|||||||
|
|
||||||
%files
|
%files
|
||||||
%{_bindir}/laminarc
|
%{_bindir}/laminarc
|
||||||
%{_bindir}/laminard
|
%{_sbindir}/laminard
|
||||||
%{_unitdir}/laminar.service
|
%{_unitdir}/laminar.service
|
||||||
%config(noreplace) %{_sysconfdir}/laminar.conf
|
%config(noreplace) %{_sysconfdir}/laminar.conf
|
||||||
%{_datarootdir}/bash-completion/completions/laminarc
|
%{_datarootdir}/bash-completion/completions/laminarc
|
||||||
@@ -72,7 +72,7 @@ make
|
|||||||
%post
|
%post
|
||||||
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
||||||
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
||||||
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: %{_sharedstatedir}/laminar
|
chown -R laminar: %{_sharedstatedir}/laminar
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Lightweight Continuous Integration Service
|
|||||||
%prep
|
%prep
|
||||||
|
|
||||||
%build
|
%build
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ -DSYSTEMD_UNITDIR=%{_unitdir} %{_sourcedir}/laminar-$VERSION
|
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -DSYSTEMD_UNITDIR=%{_unitdir} %{_sourcedir}/laminar-$VERSION
|
||||||
pwd
|
pwd
|
||||||
make
|
make
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ make
|
|||||||
|
|
||||||
%files
|
%files
|
||||||
%{_bindir}/laminarc
|
%{_bindir}/laminarc
|
||||||
%{_bindir}/laminard
|
%{_sbindir}/laminard
|
||||||
%{_unitdir}/laminar.service
|
%{_unitdir}/laminar.service
|
||||||
%config(noreplace) %{_sysconfdir}/laminar.conf
|
%config(noreplace) %{_sysconfdir}/laminar.conf
|
||||||
%{_datarootdir}/bash-completion/completions/laminarc
|
%{_datarootdir}/bash-completion/completions/laminarc
|
||||||
@@ -69,7 +69,7 @@ make
|
|||||||
%post
|
%post
|
||||||
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
echo Creating laminar user with home in %{_sharedstatedir}/laminar
|
||||||
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
useradd -r -d %{_sharedstatedir}/laminar -s %{_sbindir}/nologin laminar
|
||||||
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p %{_sharedstatedir}/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: %{_sharedstatedir}/laminar
|
chown -R laminar: %{_sharedstatedir}/laminar
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG
|
|||||||
mkdir /build
|
mkdir /build
|
||||||
cd /build
|
cd /build
|
||||||
|
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ -DZSH_COMPLETIONS_DIR=/usr/share/zsh/functions/Completion/Unix /laminar
|
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -DZSH_COMPLETIONS_DIR=/usr/share/zsh/functions/Completion/Unix /laminar
|
||||||
make -j4
|
make -j4
|
||||||
mkdir laminar
|
mkdir laminar
|
||||||
make DESTDIR=laminar install/strip
|
make DESTDIR=laminar install/strip
|
||||||
@@ -38,7 +38,7 @@ cat <<EOF > laminar/DEBIAN/postinst
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
echo Creating laminar user with home in /var/lib/laminar
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p /var/lib/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: /var/lib/laminar
|
chown -R laminar: /var/lib/laminar
|
||||||
EOF
|
EOF
|
||||||
chmod +x laminar/DEBIAN/postinst
|
chmod +x laminar/DEBIAN/postinst
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ cmake \
|
|||||||
-DCMAKE_OBJCOPY=/usr/bin/arm-linux-gnueabihf-objcopy \
|
-DCMAKE_OBJCOPY=/usr/bin/arm-linux-gnueabihf-objcopy \
|
||||||
-DCMAKE_STRIP=/usr/bin/arm-linux-gnueabihf-strip \
|
-DCMAKE_STRIP=/usr/bin/arm-linux-gnueabihf-strip \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_INSTALL_PREFIX=/ \
|
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||||
|
-DLAMINAR_VERSION=$VERSION \
|
||||||
-DZSH_COMPLETIONS_DIR=/usr/share/zsh/functions/Completion/Unix \
|
-DZSH_COMPLETIONS_DIR=/usr/share/zsh/functions/Completion/Unix \
|
||||||
/laminar
|
/laminar
|
||||||
make -j4
|
make -j4
|
||||||
@@ -53,7 +54,7 @@ cat <<EOF > laminar/DEBIAN/postinst
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
echo Creating laminar user with home in /var/lib/laminar
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
mkdir -p /var/lib/laminar/cfg/{jobs,contexts,scripts}
|
||||||
chown -R laminar: /var/lib/laminar
|
chown -R laminar: /var/lib/laminar
|
||||||
EOF
|
EOF
|
||||||
chmod +x laminar/DEBIAN/postinst
|
chmod +x laminar/DEBIAN/postinst
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
OUTPUT_DIR=$PWD
|
|
||||||
|
|
||||||
SOURCE_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]})/..)
|
|
||||||
|
|
||||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian9
|
|
||||||
|
|
||||||
DOCKER_TAG=$(docker build -q - <<EOS
|
|
||||||
FROM debian:9-slim
|
|
||||||
RUN apt-get update && apt-get install -y wget cmake g++ libsqlite3-dev libboost-dev zlib1g-dev
|
|
||||||
EOS
|
|
||||||
)
|
|
||||||
|
|
||||||
docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG bash -xe <<EOS
|
|
||||||
|
|
||||||
mkdir /build
|
|
||||||
cd /build
|
|
||||||
|
|
||||||
wget -O capnproto.tar.gz https://github.com/capnproto/capnproto/archive/v0.7.0.tar.gz
|
|
||||||
wget -O rapidjson.tar.gz https://github.com/miloyip/rapidjson/archive/v1.1.0.tar.gz
|
|
||||||
md5sum -c <<EOF
|
|
||||||
a9de5f042f4cf05515c2d7dfc7f5df21 capnproto.tar.gz
|
|
||||||
badd12c511e081fec6c89c43a7027bce rapidjson.tar.gz
|
|
||||||
EOF
|
|
||||||
|
|
||||||
tar xzf capnproto.tar.gz
|
|
||||||
tar xzf rapidjson.tar.gz
|
|
||||||
|
|
||||||
cd /build/capnproto-0.7.0/c++/
|
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=off .
|
|
||||||
make -j4
|
|
||||||
make install
|
|
||||||
|
|
||||||
cd /build/rapidjson-1.1.0/
|
|
||||||
cmake -DRAPIDJSON_BUILD_EXAMPLES=off .
|
|
||||||
make install
|
|
||||||
|
|
||||||
cd /build
|
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ -DZSH_COMPLETIONS_DIR=/usr/share/zsh/functions/Completion/Unix /laminar
|
|
||||||
make -j4
|
|
||||||
mkdir laminar
|
|
||||||
make DESTDIR=laminar install/strip
|
|
||||||
|
|
||||||
mkdir laminar/DEBIAN
|
|
||||||
cat <<EOF > laminar/DEBIAN/control
|
|
||||||
Package: laminar
|
|
||||||
Version: $VERSION
|
|
||||||
Section:
|
|
||||||
Priority: optional
|
|
||||||
Architecture: amd64
|
|
||||||
Maintainer: Oliver Giles <web ohwg net>
|
|
||||||
Depends: libsqlite3-0, zlib1g
|
|
||||||
Description: Lightweight Continuous Integration Service
|
|
||||||
EOF
|
|
||||||
echo /etc/laminar.conf > laminar/DEBIAN/conffiles
|
|
||||||
cat <<EOF > laminar/DEBIAN/postinst
|
|
||||||
#!/bin/bash
|
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
|
||||||
chown -R laminar: /var/lib/laminar
|
|
||||||
EOF
|
|
||||||
chmod +x laminar/DEBIAN/postinst
|
|
||||||
|
|
||||||
dpkg-deb --build laminar
|
|
||||||
mv laminar.deb /output/laminar_${VERSION}_amd64.deb
|
|
||||||
EOS
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
OUTPUT_DIR=$PWD
|
|
||||||
|
|
||||||
SOURCE_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]})/..)
|
|
||||||
|
|
||||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian9
|
|
||||||
|
|
||||||
DOCKER_TAG=$(docker build -q - <<EOS
|
|
||||||
FROM debian:9-slim
|
|
||||||
RUN dpkg --add-architecture armhf && apt-get update && apt-get install -y wget cmake crossbuild-essential-armhf libsqlite3-dev:armhf libboost-dev:armhf zlib1g-dev:armhf
|
|
||||||
EOS
|
|
||||||
)
|
|
||||||
|
|
||||||
docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG bash -xe <<EOS
|
|
||||||
|
|
||||||
mkdir /build
|
|
||||||
cd /build
|
|
||||||
|
|
||||||
cat > toolchain.cmake <<EOF
|
|
||||||
SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
|
|
||||||
SET(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
|
|
||||||
set(CMAKE_LIBRARY_ARCHITECTURE arm-linux-gnueabihf)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
wget -O capnproto.tar.gz https://github.com/capnproto/capnproto/archive/v0.7.0.tar.gz
|
|
||||||
wget -O rapidjson.tar.gz https://github.com/miloyip/rapidjson/archive/v1.1.0.tar.gz
|
|
||||||
md5sum -c <<EOF
|
|
||||||
a9de5f042f4cf05515c2d7dfc7f5df21 capnproto.tar.gz
|
|
||||||
badd12c511e081fec6c89c43a7027bce rapidjson.tar.gz
|
|
||||||
EOF
|
|
||||||
|
|
||||||
tar xzf capnproto.tar.gz
|
|
||||||
tar xzf rapidjson.tar.gz
|
|
||||||
|
|
||||||
mkdir capnproto-host
|
|
||||||
cd capnproto-host
|
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=off /build/capnproto-0.7.0/c++/
|
|
||||||
make -j4
|
|
||||||
make install
|
|
||||||
|
|
||||||
cd /build/capnproto-0.7.0/c++/
|
|
||||||
cmake -DCMAKE_TOOLCHAIN_FILE=../../toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf/ .
|
|
||||||
make -j4
|
|
||||||
make install
|
|
||||||
|
|
||||||
cd /build/rapidjson-1.1.0/
|
|
||||||
cmake -DRAPIDJSON_BUILD_EXAMPLES=off -DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf/ .
|
|
||||||
make install
|
|
||||||
|
|
||||||
cd /build
|
|
||||||
cmake \
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \
|
|
||||||
-DCMAKE_LINKER=/usr/bin/arm-linux-gnueabihf-ld \
|
|
||||||
-DCMAKE_OBJCOPY=/usr/bin/arm-linux-gnueabihf-objcopy \
|
|
||||||
-DCMAKE_STRIP=/usr/bin/arm-linux-gnueabihf-strip \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DCMAKE_INSTALL_PREFIX=/ \
|
|
||||||
-DZSH_COMPLETIONS_DIR=/usr/share/zsh/functions/Completion/Unix \
|
|
||||||
/laminar
|
|
||||||
make -j4
|
|
||||||
mkdir laminar
|
|
||||||
make DESTDIR=laminar install/strip
|
|
||||||
|
|
||||||
mkdir laminar/DEBIAN
|
|
||||||
cat <<EOF > laminar/DEBIAN/control
|
|
||||||
Package: laminar
|
|
||||||
Version: $VERSION
|
|
||||||
Section:
|
|
||||||
Priority: optional
|
|
||||||
Architecture: armhf
|
|
||||||
Maintainer: Oliver Giles <web ohwg net>
|
|
||||||
Depends: libsqlite3-0, zlib1g
|
|
||||||
Description: Lightweight Continuous Integration Service
|
|
||||||
EOF
|
|
||||||
echo /etc/laminar.conf > laminar/DEBIAN/conffiles
|
|
||||||
cat <<EOF > laminar/DEBIAN/postinst
|
|
||||||
#!/bin/bash
|
|
||||||
echo Creating laminar user with home in /var/lib/laminar
|
|
||||||
useradd -r -d /var/lib/laminar -s /usr/sbin/nologin laminar
|
|
||||||
mkdir -p /var/lib/laminar/cfg/{jobs,nodes,scripts}
|
|
||||||
chown -R laminar: /var/lib/laminar
|
|
||||||
EOF
|
|
||||||
chmod +x laminar/DEBIAN/postinst
|
|
||||||
|
|
||||||
dpkg-deb --build laminar
|
|
||||||
mv laminar.deb /output/laminar_${VERSION}_armhf.deb
|
|
||||||
EOS
|
|
||||||
131
src/client.cpp
131
src/client.cpp
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2018 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -17,12 +17,15 @@
|
|||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#include "laminar.capnp.h"
|
#include "laminar.capnp.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
#include <capnp/ez-rpc.h>
|
#include <capnp/ez-rpc.h>
|
||||||
#include <kj/vector.h>
|
#include <kj/vector.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
#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
|
||||||
@@ -71,18 +74,57 @@ static int setParams(int argc, char** argv, T& request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void printTriggerLink(const char* job, uint run) {
|
static void printTriggerLink(const char* job, uint run) {
|
||||||
// use a private ANSI CSI sequence to mark the JOB:NUM so the
|
if(getenv("__LAMINAR_SETENV_PIPE")) {
|
||||||
// frontend can recognise it and generate a hyperlink.
|
// use a private ANSI CSI sequence to mark the JOB:NUM so the
|
||||||
printf("\033[{%s:%d\033\\\n", job, run);
|
// frontend can recognise it and generate a hyperlink.
|
||||||
|
printf("\033[{%s:%d\033\\\n", job, run);
|
||||||
|
} else {
|
||||||
|
// not called from within a laminar job, let's not confuse
|
||||||
|
// scripts with ANSI sequences.
|
||||||
|
printf("%s:%d\n", job, run);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(std::ostream& out) {
|
||||||
|
out << "laminarc version " << laminar_version() << "\n";
|
||||||
|
out << "Usage: laminarc [-h|--help] COMMAND [PARAMETERS...]]\n";
|
||||||
|
out << " -h|--help show this help message\n";
|
||||||
|
out << "where COMMAND is:\n";
|
||||||
|
out << " queue JOB_LIST... queues one or more jobs for execution and returns immediately.\n";
|
||||||
|
out << " start JOB_LIST... queues one or more jobs for execution and blocks until it starts.\n";
|
||||||
|
out << " run JOB_LIST... queues one or more jobs for execution and blocks until it finishes.\n";
|
||||||
|
out << " set PARAMETER_LIST... sets the given parameters as environment variables in the currently\n";
|
||||||
|
out << " runing job. Fails if run outside of a job context.\n";
|
||||||
|
out << " abort NAME NUMBER aborts the run identified by NAME and NUMBER.\n";
|
||||||
|
out << " show-jobs lists all known jobs.\n";
|
||||||
|
out << " show-queued lists currently queued jobs.\n";
|
||||||
|
out << " show-running lists currently running jobs.\n";
|
||||||
|
out << "JOB_LIST is of the form:\n";
|
||||||
|
out << " [JOB_NAME [PARAMETER_LIST...]]...\n";
|
||||||
|
out << "PARAMETER_LIST is of the form:\n";
|
||||||
|
out << " [KEY=VALUE]...\n";
|
||||||
|
out << "Example:\n";
|
||||||
|
out << " laminarc start \\\n";
|
||||||
|
out << " nightly-build branch=master type=release \\\n";
|
||||||
|
out << " nightly-build branch=master type=debug\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
if(argc < 2) {
|
if(argc < 2)
|
||||||
fprintf(stderr, "Usage: %s <command> [parameters...]\n", argv[0]);
|
return usage(std::cerr), EXIT_BAD_ARGUMENT;
|
||||||
return EXIT_BAD_ARGUMENT;
|
else if(strcmp("-h", argv[1]) == 0 || strcmp("--help", argv[1]) == 0)
|
||||||
}
|
return usage(std::cout), EXIT_SUCCESS;
|
||||||
|
|
||||||
|
struct: public kj::TaskSet::ErrorHandler {
|
||||||
|
void taskFailed(kj::Exception&& e) override {
|
||||||
|
fprintf(stderr, "%s\n", e.getDescription().cStr());
|
||||||
|
ret = EXIT_OPERATION_FAILED;
|
||||||
|
}
|
||||||
|
int ret = 0;
|
||||||
|
} errorHandler;
|
||||||
|
kj::TaskSet ts(errorHandler);
|
||||||
|
int& ret = errorHandler.ret;
|
||||||
|
|
||||||
int ret = 0;
|
|
||||||
const char* address = getenv("LAMINAR_HOST") ?: getenv("LAMINAR_BIND_RPC") ?: "unix-abstract:laminar";
|
const char* address = getenv("LAMINAR_HOST") ?: getenv("LAMINAR_BIND_RPC") ?: "unix-abstract:laminar";
|
||||||
|
|
||||||
capnp::EzRpcClient client(address);
|
capnp::EzRpcClient client(address);
|
||||||
@@ -95,23 +137,20 @@ int main(int argc, char** argv) {
|
|||||||
fprintf(stderr, "Usage %s queue <jobName>\n", argv[0]);
|
fprintf(stderr, "Usage %s queue <jobName>\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
kj::Vector<capnp::RemotePromise<LaminarCi::QueueResults>> promises;
|
|
||||||
int jobNameIndex = 2;
|
int jobNameIndex = 2;
|
||||||
// make a request for each job specified on the commandline
|
// make a request for each job specified on the commandline
|
||||||
do {
|
do {
|
||||||
auto req = laminar.queueRequest();
|
auto req = laminar.queueRequest();
|
||||||
req.setJobName(argv[jobNameIndex]);
|
req.setJobName(argv[jobNameIndex]);
|
||||||
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
||||||
promises.add(req.send());
|
ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::QueueResults> resp){
|
||||||
|
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) {
|
||||||
|
fprintf(stderr, "Failed to queue job '%s'\n", argv[jobNameIndex]);
|
||||||
|
ret = EXIT_OPERATION_FAILED;
|
||||||
|
}
|
||||||
|
}));
|
||||||
jobNameIndex += n + 1;
|
jobNameIndex += n + 1;
|
||||||
} while(jobNameIndex < argc);
|
} while(jobNameIndex < argc);
|
||||||
// pend on the promises
|
|
||||||
for(auto& p : promises) {
|
|
||||||
if(p.wait(waitScope).getResult() != LaminarCi::MethodResult::SUCCESS) {
|
|
||||||
fprintf(stderr, "Failed to queue job '%s'\n", argv[2]);
|
|
||||||
return EXIT_OPERATION_FAILED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if(strcmp(argv[1], "start") == 0 || strcmp(argv[1], "trigger") == 0) {
|
} else if(strcmp(argv[1], "start") == 0 || strcmp(argv[1], "trigger") == 0) {
|
||||||
if(strcmp(argv[1], "trigger") == 0)
|
if(strcmp(argv[1], "trigger") == 0)
|
||||||
fprintf(stderr, "Warning: 'trigger' is deprecated, use 'queue' for the old behavior\n");
|
fprintf(stderr, "Warning: 'trigger' is deprecated, use 'queue' for the old behavior\n");
|
||||||
@@ -120,10 +159,6 @@ int main(int argc, char** argv) {
|
|||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
kj::Vector<capnp::RemotePromise<LaminarCi::StartResults>> promises;
|
kj::Vector<capnp::RemotePromise<LaminarCi::StartResults>> promises;
|
||||||
struct: public kj::TaskSet::ErrorHandler {
|
|
||||||
void taskFailed(kj::Exception&&) override {}
|
|
||||||
} ignoreFailed;
|
|
||||||
kj::TaskSet ts(ignoreFailed);
|
|
||||||
int jobNameIndex = 2;
|
int jobNameIndex = 2;
|
||||||
// make a request for each job specified on the commandline
|
// make a request for each job specified on the commandline
|
||||||
do {
|
do {
|
||||||
@@ -134,21 +169,16 @@ int main(int argc, char** argv) {
|
|||||||
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) {
|
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) {
|
||||||
fprintf(stderr, "Failed to start job '%s'\n", argv[2]);
|
fprintf(stderr, "Failed to start job '%s'\n", argv[2]);
|
||||||
ret = EXIT_OPERATION_FAILED;
|
ret = EXIT_OPERATION_FAILED;
|
||||||
}
|
} else
|
||||||
printTriggerLink(argv[jobNameIndex], resp.getBuildNum());
|
printTriggerLink(argv[jobNameIndex], resp.getBuildNum());
|
||||||
}));
|
}));
|
||||||
jobNameIndex += n + 1;
|
jobNameIndex += n + 1;
|
||||||
} while(jobNameIndex < argc);
|
} while(jobNameIndex < argc);
|
||||||
ts.onEmpty().wait(waitScope);
|
|
||||||
} else if(strcmp(argv[1], "run") == 0) {
|
} else if(strcmp(argv[1], "run") == 0) {
|
||||||
if(argc < 3) {
|
if(argc < 3) {
|
||||||
fprintf(stderr, "Usage %s run <jobName>\n", argv[0]);
|
fprintf(stderr, "Usage %s run <jobName>\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
struct: public kj::TaskSet::ErrorHandler {
|
|
||||||
void taskFailed(kj::Exception&&) override {}
|
|
||||||
} ignoreFailed;
|
|
||||||
kj::TaskSet ts(ignoreFailed);
|
|
||||||
int jobNameIndex = 2;
|
int jobNameIndex = 2;
|
||||||
// make a request for each job specified on the commandline
|
// make a request for each job specified on the commandline
|
||||||
do {
|
do {
|
||||||
@@ -156,34 +186,24 @@ int main(int argc, char** argv) {
|
|||||||
req.setJobName(argv[jobNameIndex]);
|
req.setJobName(argv[jobNameIndex]);
|
||||||
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
||||||
ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::RunResults> resp){
|
ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::RunResults> resp){
|
||||||
printTriggerLink(argv[jobNameIndex], resp.getBuildNum());
|
if(resp.getResult() == LaminarCi::JobResult::UNKNOWN)
|
||||||
if(resp.getResult() != LaminarCi::JobResult::SUCCESS) {
|
fprintf(stderr, "Failed to start job '%s'\n", argv[2]);
|
||||||
|
else
|
||||||
|
printTriggerLink(argv[jobNameIndex], resp.getBuildNum());
|
||||||
|
if(resp.getResult() != LaminarCi::JobResult::SUCCESS)
|
||||||
ret = EXIT_RUN_FAILED;
|
ret = EXIT_RUN_FAILED;
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
jobNameIndex += n + 1;
|
jobNameIndex += n + 1;
|
||||||
} while(jobNameIndex < argc);
|
} while(jobNameIndex < argc);
|
||||||
ts.onEmpty().wait(waitScope);
|
|
||||||
} else if(strcmp(argv[1], "set") == 0) {
|
} else if(strcmp(argv[1], "set") == 0) {
|
||||||
if(argc < 3) {
|
if(argc < 3) {
|
||||||
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], '=');
|
LSYSCALL(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) {
|
||||||
@@ -194,14 +214,17 @@ int main(int argc, char** argv) {
|
|||||||
auto req = laminar.abortRequest();
|
auto req = laminar.abortRequest();
|
||||||
req.getRun().setJob(argv[2]);
|
req.getRun().setJob(argv[2]);
|
||||||
req.getRun().setBuildNum(atoi(argv[3]));
|
req.getRun().setBuildNum(atoi(argv[3]));
|
||||||
if(req.send().wait(waitScope).getResult() != LaminarCi::MethodResult::SUCCESS)
|
ts.add(req.send().then([&ret](capnp::Response<LaminarCi::AbortResults> resp){
|
||||||
ret = EXIT_OPERATION_FAILED;
|
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS)
|
||||||
|
ret = EXIT_OPERATION_FAILED;
|
||||||
|
}));
|
||||||
} else if(strcmp(argv[1], "show-jobs") == 0) {
|
} else if(strcmp(argv[1], "show-jobs") == 0) {
|
||||||
if(argc != 2) {
|
if(argc != 2) {
|
||||||
fprintf(stderr, "Usage: %s show-jobs\n", argv[0]);
|
fprintf(stderr, "Usage: %s show-jobs\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
for(auto it : laminar.listKnownRequest().send().wait(waitScope).getResult()) {
|
auto jobs = laminar.listKnownRequest().send().wait(waitScope);
|
||||||
|
for(auto it : jobs.getResult()) {
|
||||||
printf("%s\n", it.cStr());
|
printf("%s\n", it.cStr());
|
||||||
}
|
}
|
||||||
} else if(strcmp(argv[1], "show-queued") == 0) {
|
} else if(strcmp(argv[1], "show-queued") == 0) {
|
||||||
@@ -209,7 +232,8 @@ int main(int argc, char** argv) {
|
|||||||
fprintf(stderr, "Usage: %s show-queued\n", argv[0]);
|
fprintf(stderr, "Usage: %s show-queued\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
for(auto it : laminar.listQueuedRequest().send().wait(waitScope).getResult()) {
|
auto queued = laminar.listQueuedRequest().send().wait(waitScope);
|
||||||
|
for(auto it : queued.getResult()) {
|
||||||
printf("%s\n", it.cStr());
|
printf("%s\n", it.cStr());
|
||||||
}
|
}
|
||||||
} else if(strcmp(argv[1], "show-running") == 0) {
|
} else if(strcmp(argv[1], "show-running") == 0) {
|
||||||
@@ -217,7 +241,8 @@ int main(int argc, char** argv) {
|
|||||||
fprintf(stderr, "Usage: %s show-running\n", argv[0]);
|
fprintf(stderr, "Usage: %s show-running\n", argv[0]);
|
||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
for(auto it : laminar.listRunningRequest().send().wait(waitScope).getResult()) {
|
auto running = laminar.listRunningRequest().send().wait(waitScope);
|
||||||
|
for(auto it : running.getResult()) {
|
||||||
printf("%s:%d\n", it.getJob().cStr(), it.getBuildNum());
|
printf("%s:%d\n", it.getJob().cStr(), it.getBuildNum());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -225,5 +250,7 @@ int main(int argc, char** argv) {
|
|||||||
return EXIT_BAD_ARGUMENT;
|
return EXIT_BAD_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ts.onEmpty().wait(waitScope);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#ifndef LAMINAR_CONF_H_
|
#ifndef LAMINAR_CONF_H_
|
||||||
#define LAMINAR_CONF_H_
|
#define LAMINAR_CONF_H_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
class StringMap : public std::unordered_map<std::string, std::string> {
|
class StringMap : public std::unordered_map<std::string, std::string> {
|
||||||
|
|||||||
56
src/context.h
Normal file
56
src/context.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
///
|
||||||
|
/// Copyright 2015-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_CONTEXT_H_
|
||||||
|
#define LAMINAR_CONTEXT_H_
|
||||||
|
|
||||||
|
#include <fnmatch.h>
|
||||||
|
#include <string>
|
||||||
|
#include <set>
|
||||||
|
class Run;
|
||||||
|
|
||||||
|
// FNM_EXTMATCH isn't supported under musl
|
||||||
|
#if !defined(FNM_EXTMATCH)
|
||||||
|
#define FNM_EXTMATCH 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Represents a context within which a Run will be executed. Allows applying
|
||||||
|
// a certain environment to a set of Jobs, or setting a limit on the number
|
||||||
|
// of parallel Runs
|
||||||
|
class Context {
|
||||||
|
public:
|
||||||
|
Context() {}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
int numExecutors;
|
||||||
|
int busyExecutors = 0;
|
||||||
|
|
||||||
|
bool canQueue(std::set<std::string>& patterns) {
|
||||||
|
if(busyExecutors >= numExecutors)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for(std::string pattern : patterns) {
|
||||||
|
if(fnmatch(pattern.c_str(), name.c_str(), FNM_EXTMATCH) == 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif // LAMINAR_CONTEXT_H_
|
||||||
@@ -268,8 +268,8 @@ Http::Http(Laminar &li) :
|
|||||||
|
|
||||||
Http::~Http()
|
Http::~Http()
|
||||||
{
|
{
|
||||||
KJ_ASSERT(logWatchers.size() == 0);
|
LASSERT(logWatchers.size() == 0);
|
||||||
KJ_ASSERT(eventPeers.size() == 0);
|
LASSERT(eventPeers.size() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
kj::Promise<void> Http::startServer(kj::Timer& timer, kj::Own<kj::ConnectionReceiver>&& listener)
|
kj::Promise<void> Http::startServer(kj::Timer& timer, kj::Own<kj::ConnectionReceiver>&& listener)
|
||||||
@@ -297,3 +297,8 @@ void Http::notifyLog(std::string job, uint run, std::string log_chunk, bool eot)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Http::setHtmlTemplate(std::string tmpl)
|
||||||
|
{
|
||||||
|
resources->setHtmlTemplate(tmpl);
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ public:
|
|||||||
void notifyEvent(const char* data, std::string job = nullptr);
|
void notifyEvent(const char* data, std::string job = nullptr);
|
||||||
void notifyLog(std::string job, uint run, std::string log_chunk, bool eot);
|
void notifyLog(std::string job, uint run, std::string log_chunk, bool eot);
|
||||||
|
|
||||||
|
// Allows supplying a custom HTML template. Pass an empty string to use the default.
|
||||||
|
void setHtmlTemplate(std::string tmpl = std::string());
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual kj::Promise<void> request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
|
virtual kj::Promise<void> request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
|
||||||
kj::AsyncInputStream& requestBody, Response& response) override;
|
kj::AsyncInputStream& requestBody, Response& response) override;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
263
src/laminar.cpp
263
src/laminar.cpp
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2019 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -76,6 +76,11 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
{
|
{
|
||||||
LASSERT(settings.home[0] == '/');
|
LASSERT(settings.home[0] == '/');
|
||||||
|
|
||||||
|
if(fsHome->exists(homePath/"cfg"/"nodes")) {
|
||||||
|
LLOG(ERROR, "Found node configuration directory cfg/nodes. Nodes have been deprecated, please migrate to contexts. Laminar will now exit.");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
archiveUrl = settings.archive_url;
|
archiveUrl = settings.archive_url;
|
||||||
if(archiveUrl.back() != '/')
|
if(archiveUrl.back() != '/')
|
||||||
archiveUrl.append("/");
|
archiveUrl.append("/");
|
||||||
@@ -104,8 +109,15 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
loadConfiguration();
|
loadConfiguration();
|
||||||
// config change may allow stuck jobs to dequeue
|
// config change may allow stuck jobs to dequeue
|
||||||
assignNewJobs();
|
assignNewJobs();
|
||||||
}).addPath((homePath/"cfg"/"nodes").toString(true).cStr())
|
}).addPath((homePath/"cfg"/"contexts").toString(true).cStr())
|
||||||
.addPath((homePath/"cfg"/"jobs").toString(true).cStr());
|
.addPath((homePath/"cfg"/"jobs").toString(true).cStr())
|
||||||
|
.addPath((homePath/"cfg").toString(true).cStr()); // for groups.conf
|
||||||
|
|
||||||
|
loadCustomizations();
|
||||||
|
srv.watchPaths([this]{
|
||||||
|
LLOG(INFO, "Reloading customizations");
|
||||||
|
loadCustomizations();
|
||||||
|
}).addPath((homePath/"custom").toString(true).cStr());
|
||||||
|
|
||||||
srv.listenRpc(*rpc, settings.bind_rpc);
|
srv.listenRpc(*rpc, settings.bind_rpc);
|
||||||
srv.listenHttp(*http, settings.bind_http);
|
srv.listenHttp(*http, settings.bind_http);
|
||||||
@@ -115,6 +127,14 @@ Laminar::Laminar(Server &server, Settings settings) :
|
|||||||
loadConfiguration();
|
loadConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Laminar::loadCustomizations() {
|
||||||
|
KJ_IF_MAYBE(templ, fsHome->tryOpenFile(kj::Path{"custom","index.html"})) {
|
||||||
|
http->setHtmlTemplate((*templ)->readAllText().cStr());
|
||||||
|
} else {
|
||||||
|
http->setHtmlTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint Laminar::latestRun(std::string job) {
|
uint Laminar::latestRun(std::string job) {
|
||||||
auto it = activeJobs.byJobName().equal_range(job);
|
auto it = activeJobs.byJobName().equal_range(job);
|
||||||
if(it.first == it.second) {
|
if(it.first == it.second) {
|
||||||
@@ -207,6 +227,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
Json j;
|
Json j;
|
||||||
j.set("type", "status");
|
j.set("type", "status");
|
||||||
j.set("title", getenv("LAMINAR_TITLE") ?: "Laminar");
|
j.set("title", getenv("LAMINAR_TITLE") ?: "Laminar");
|
||||||
|
j.set("version", laminar_version());
|
||||||
j.set("time", time(nullptr));
|
j.set("time", time(nullptr));
|
||||||
j.startObject("data");
|
j.startObject("data");
|
||||||
if(scope.type == MonitorScope::RUN) {
|
if(scope.type == MonitorScope::RUN) {
|
||||||
@@ -232,7 +253,8 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.set("etc", run->startedAt + lastRuntime);
|
j.set("etc", run->startedAt + lastRuntime);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
j.set("latestNum", int(buildNums[scope.job]));
|
if(auto it = buildNums.find(scope.job); it != buildNums.end())
|
||||||
|
j.set("latestNum", int(it->second));
|
||||||
j.startArray("artifacts");
|
j.startArray("artifacts");
|
||||||
populateArtifacts(j, scope.job, scope.num);
|
populateArtifacts(j, scope.job, scope.num);
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
@@ -283,7 +305,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
const std::shared_ptr<Run> run = *it;
|
const std::shared_ptr<Run> run = *it;
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("context", run->context->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
j.set("result", to_string(RunState::RUNNING));
|
j.set("result", to_string(RunState::RUNNING));
|
||||||
j.set("reason", run->reason());
|
j.set("reason", run->reason());
|
||||||
@@ -311,7 +333,8 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.set("number", build).set("started", started);
|
j.set("number", build).set("started", started);
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
});
|
});
|
||||||
|
auto desc = jobDescriptions.find(scope.job);
|
||||||
|
j.set("description", desc == jobDescriptions.end() ? "" : desc->second);
|
||||||
} else if(scope.type == MonitorScope::ALL) {
|
} else if(scope.type == MonitorScope::ALL) {
|
||||||
j.startArray("jobs");
|
j.startArray("jobs");
|
||||||
db->stmt("SELECT name,number,startedAt,completedAt,result FROM builds b JOIN (SELECT name n,MAX(number) l FROM builds GROUP BY n) q ON b.name = q.n AND b.number = q.l")
|
db->stmt("SELECT name,number,startedAt,completedAt,result FROM builds b JOIN (SELECT name n,MAX(number) l FROM builds GROUP BY n) q ON b.name = q.n AND b.number = q.l")
|
||||||
@@ -322,11 +345,6 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.set("result", to_string(RunState(result)));
|
j.set("result", to_string(RunState(result)));
|
||||||
j.set("started", started);
|
j.set("started", started);
|
||||||
j.set("completed", completed);
|
j.set("completed", completed);
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
});
|
});
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
@@ -335,24 +353,23 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", run->name);
|
j.set("name", run->name);
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("context", run->context->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[run->name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
}
|
}
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
|
j.startObject("groups");
|
||||||
|
for(const auto& group : jobGroups)
|
||||||
|
j.set(group.first.c_str(), group.second);
|
||||||
|
j.EndObject();
|
||||||
} else { // Home page
|
} else { // Home page
|
||||||
j.startArray("recent");
|
j.startArray("recent");
|
||||||
db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15")
|
db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15")
|
||||||
.fetch<str,uint,str,time_t,time_t,time_t,int>([&](str name,uint build,str node,time_t,time_t started,time_t completed,int result){
|
.fetch<str,uint,str,time_t,time_t,time_t,int>([&](str name,uint build,str context,time_t,time_t started,time_t completed,int result){
|
||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", name)
|
j.set("name", name)
|
||||||
.set("number", build)
|
.set("number", build)
|
||||||
.set("node", node)
|
.set("context", context)
|
||||||
.set("started", started)
|
.set("started", started)
|
||||||
.set("completed", completed)
|
.set("completed", completed)
|
||||||
.set("result", to_string(RunState(result)))
|
.set("result", to_string(RunState(result)))
|
||||||
@@ -364,7 +381,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.StartObject();
|
j.StartObject();
|
||||||
j.set("name", run->name);
|
j.set("name", run->name);
|
||||||
j.set("number", run->build);
|
j.set("number", run->build);
|
||||||
j.set("node", run->node->name);
|
j.set("context", run->context->name);
|
||||||
j.set("started", run->startedAt);
|
j.set("started", run->startedAt);
|
||||||
db->stmt("SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
db->stmt("SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
|
||||||
.bind(run->name)
|
.bind(run->name)
|
||||||
@@ -383,10 +400,10 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
|||||||
j.EndArray();
|
j.EndArray();
|
||||||
int execTotal = 0;
|
int execTotal = 0;
|
||||||
int execBusy = 0;
|
int execBusy = 0;
|
||||||
for(const auto& it : nodes) {
|
for(const auto& it : contexts) {
|
||||||
const std::shared_ptr<Node>& node = it.second;
|
const std::shared_ptr<Context>& context = it.second;
|
||||||
execTotal += node->numExecutors;
|
execTotal += context->numExecutors;
|
||||||
execBusy += node->busyExecutors;
|
execBusy += context->busyExecutors;
|
||||||
}
|
}
|
||||||
j.set("executorsTotal", execTotal);
|
j.set("executorsTotal", execTotal);
|
||||||
j.set("executorsBusy", execBusy);
|
j.set("executorsBusy", execBusy);
|
||||||
@@ -488,51 +505,41 @@ bool Laminar::loadConfiguration() {
|
|||||||
if(const char* ndirs = getenv("LAMINAR_KEEP_RUNDIRS"))
|
if(const char* ndirs = getenv("LAMINAR_KEEP_RUNDIRS"))
|
||||||
numKeepRunDirs = static_cast<uint>(atoi(ndirs));
|
numKeepRunDirs = static_cast<uint>(atoi(ndirs));
|
||||||
|
|
||||||
std::set<std::string> knownNodes;
|
std::set<std::string> knownContexts;
|
||||||
|
|
||||||
KJ_IF_MAYBE(nodeDir, fsHome->tryOpenSubdir(kj::Path{"cfg","nodes"})) {
|
KJ_IF_MAYBE(contextsDir, fsHome->tryOpenSubdir(kj::Path{"cfg","contexts"})) {
|
||||||
for(kj::Directory::Entry& entry : (*nodeDir)->listEntries()) {
|
for(kj::Directory::Entry& entry : (*contextsDir)->listEntries()) {
|
||||||
if(!entry.name.endsWith(".conf"))
|
if(!entry.name.endsWith(".conf"))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
StringMap conf = parseConfFile((homePath/"cfg"/"nodes"/entry.name).toString(true).cStr());
|
StringMap conf = parseConfFile((homePath/"cfg"/"contexts"/entry.name).toString(true).cStr());
|
||||||
|
|
||||||
std::string nodeName(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
std::string name(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
||||||
auto existingNode = nodes.find(nodeName);
|
auto existing = contexts.find(name);
|
||||||
std::shared_ptr<Node> node = existingNode == nodes.end() ? nodes.emplace(nodeName, std::shared_ptr<Node>(new Node)).first->second : existingNode->second;
|
std::shared_ptr<Context> context = existing == contexts.end() ? contexts.emplace(name, std::shared_ptr<Context>(new Context)).first->second : existing->second;
|
||||||
node->name = nodeName;
|
context->name = name;
|
||||||
node->numExecutors = conf.get<int>("EXECUTORS", 6);
|
context->numExecutors = conf.get<int>("EXECUTORS", 6);
|
||||||
|
|
||||||
std::string tagString = conf.get<std::string>("TAGS");
|
knownContexts.insert(name);
|
||||||
std::set<std::string> tagList;
|
|
||||||
if(!tagString.empty()) {
|
|
||||||
std::istringstream iss(tagString);
|
|
||||||
std::string tag;
|
|
||||||
while(std::getline(iss, tag, ','))
|
|
||||||
tagList.insert(tag);
|
|
||||||
}
|
|
||||||
std::swap(node->tags, tagList);
|
|
||||||
|
|
||||||
knownNodes.insert(nodeName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any nodes whose config files disappeared.
|
// remove any contexts whose config files disappeared.
|
||||||
// if there are no known nodes, take care not to remove and re-add the default node
|
// if there are no known contexts, take care not to remove and re-add the default context.
|
||||||
for(auto it = nodes.begin(); it != nodes.end();) {
|
for(auto it = contexts.begin(); it != contexts.end();) {
|
||||||
if((it->first == "" && knownNodes.size() == 0) || knownNodes.find(it->first) != knownNodes.end())
|
if((it->first == "default" && knownContexts.size() == 0) || knownContexts.find(it->first) != knownContexts.end())
|
||||||
it++;
|
it++;
|
||||||
else
|
else
|
||||||
it = nodes.erase(it);
|
it = contexts.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a default node
|
// add a default context
|
||||||
if(nodes.empty()) {
|
if(contexts.empty()) {
|
||||||
LLOG(INFO, "Creating a default node with 6 executors");
|
LLOG(INFO, "Creating a default context with 6 executors");
|
||||||
std::shared_ptr<Node> node(new Node);
|
std::shared_ptr<Context> context(new Context);
|
||||||
node->name = "";
|
context->name = "default";
|
||||||
node->numExecutors = 6;
|
context->numExecutors = 6;
|
||||||
nodes.emplace("", node);
|
contexts.emplace("default", context);
|
||||||
}
|
}
|
||||||
|
|
||||||
KJ_IF_MAYBE(jobsDir, fsHome->tryOpenSubdir(kj::Path{"cfg","jobs"})) {
|
KJ_IF_MAYBE(jobsDir, fsHome->tryOpenSubdir(kj::Path{"cfg","jobs"})) {
|
||||||
@@ -543,19 +550,29 @@ bool Laminar::loadConfiguration() {
|
|||||||
|
|
||||||
std::string jobName(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
std::string jobName(entry.name.cStr(), entry.name.findLast('.').orDefault(0));
|
||||||
|
|
||||||
std::string tags = conf.get<std::string>("TAGS");
|
std::string ctxPtns = conf.get<std::string>("CONTEXTS");
|
||||||
if(!tags.empty()) {
|
|
||||||
std::istringstream iss(tags);
|
|
||||||
std::set<std::string> tagList;
|
|
||||||
std::string tag;
|
|
||||||
while(std::getline(iss, tag, ','))
|
|
||||||
tagList.insert(tag);
|
|
||||||
jobTags[jobName] = tagList;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if(!ctxPtns.empty()) {
|
||||||
|
std::istringstream iss(ctxPtns);
|
||||||
|
std::set<std::string> ctxPtnList;
|
||||||
|
std::string ctx;
|
||||||
|
while(std::getline(iss, ctx, ','))
|
||||||
|
ctxPtnList.insert(ctx);
|
||||||
|
jobContexts[jobName].swap(ctxPtnList);
|
||||||
|
}
|
||||||
|
std::string desc = conf.get<std::string>("DESCRIPTION");
|
||||||
|
if(!desc.empty()) {
|
||||||
|
jobDescriptions[jobName] = desc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobGroups.clear();
|
||||||
|
KJ_IF_MAYBE(groupsConf, fsHome->tryOpenFile(kj::Path{"cfg","groups.conf"}))
|
||||||
|
jobGroups = parseConfFile((homePath/"cfg"/"groups.conf").toString(true).cStr());
|
||||||
|
if(jobGroups.empty())
|
||||||
|
jobGroups["All Jobs"] = ".*";
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +582,10 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the job has no contexts (maybe there is no .conf file at all), add the default context
|
||||||
|
if(jobContexts[name].empty())
|
||||||
|
jobContexts.at(name).insert("default");
|
||||||
|
|
||||||
std::shared_ptr<Run> run = std::make_shared<Run>(name, kj::mv(params), homePath.clone());
|
std::shared_ptr<Run> run = std::make_shared<Run>(name, kj::mv(params), homePath.clone());
|
||||||
queuedJobs.push_back(run);
|
queuedJobs.push_back(run);
|
||||||
|
|
||||||
@@ -581,48 +602,25 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Laminar::nodeCanQueue(const Node& node, std::string jobName) const {
|
|
||||||
// if a node is too busy, it can't take the job
|
|
||||||
if(node.busyExecutors >= node.numExecutors)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// if the node has no tags, allow the build
|
|
||||||
if(node.tags.size() == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
auto it = jobTags.find(jobName);
|
|
||||||
// if the job has no tags, it cannot be run on this node
|
|
||||||
if(it == jobTags.end())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// otherwise, allow the build if job and node have a tag in common
|
|
||||||
for(const std::string& tag : it->second) {
|
|
||||||
if(node.tags.find(tag) != node.tags.end())
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
||||||
for(auto& sn : nodes) {
|
for(auto& sc : contexts) {
|
||||||
std::shared_ptr<Node> node = sn.second;
|
std::shared_ptr<Context> ctx = sc.second;
|
||||||
|
|
||||||
if(nodeCanQueue(*node.get(), run->name) && run->configure(buildNums[run->name] + 1, node, *fsHome)) {
|
if(ctx->canQueue(jobContexts.at(run->name))) {
|
||||||
node->busyExecutors++;
|
kj::Promise<RunState> onRunFinished = run->start(buildNums[run->name] + 1, ctx, *fsHome,[this](kj::Maybe<pid_t>& pid){return srv.onChildExit(pid);});
|
||||||
|
|
||||||
|
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")
|
||||||
.bind(run->name)
|
.bind(run->name)
|
||||||
@@ -630,17 +628,24 @@ 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));
|
||||||
LLOG(INFO, "Started job on node", run->name, run->build, node->name);
|
LLOG(INFO, "Started job", run->name, run->build, ctx->name);
|
||||||
|
|
||||||
// update next build number
|
// update next build number
|
||||||
buildNums[run->name]++;
|
buildNums[run->name]++;
|
||||||
@@ -660,11 +665,6 @@ bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
|
|||||||
.fetch<uint>([&](uint etc){
|
.fetch<uint>([&](uint etc){
|
||||||
j.set("etc", time(nullptr) + etc);
|
j.set("etc", time(nullptr) + etc);
|
||||||
});
|
});
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[run->name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.EndObject();
|
j.EndObject();
|
||||||
http->notifyEvent(j.str(), run->name.c_str());
|
http->notifyEvent(j.str(), run->name.c_str());
|
||||||
return true;
|
return true;
|
||||||
@@ -685,34 +685,10 @@ void Laminar::assignNewJobs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kj::Promise<void> Laminar::handleRunStep(Run* run) {
|
void Laminar::handleRunFinished(Run * r) {
|
||||||
if(run->step()) {
|
std::shared_ptr<Context> ctx = r->context;
|
||||||
// no more steps
|
|
||||||
return kj::READY_NOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
kj::Promise<int> exited = srv.onChildExit(run->current_pid);
|
ctx->busyExecutors--;
|
||||||
// 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<Node> node = r->node;
|
|
||||||
|
|
||||||
node->busyExecutors--;
|
|
||||||
LLOG(INFO, "Run completed", r->name, to_string(r->result));
|
LLOG(INFO, "Run completed", r->name, to_string(r->result));
|
||||||
time_t completedAt = time(nullptr);
|
time_t completedAt = time(nullptr);
|
||||||
|
|
||||||
@@ -731,7 +707,7 @@ void Laminar::runFinished(Run * r) {
|
|||||||
|
|
||||||
std::string reason = r->reason();
|
std::string reason = r->reason();
|
||||||
db->stmt("INSERT INTO builds VALUES(?,?,?,?,?,?,?,?,?,?,?,?)")
|
db->stmt("INSERT INTO builds VALUES(?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||||
.bind(r->name, r->build, node->name, r->queuedAt, r->startedAt, completedAt, int(r->result),
|
.bind(r->name, r->build, ctx->name, r->queuedAt, r->startedAt, completedAt, int(r->result),
|
||||||
maybeZipped, logsize, r->parentName, r->parentBuild, reason)
|
maybeZipped, logsize, r->parentName, r->parentBuild, reason)
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
@@ -746,11 +722,6 @@ void Laminar::runFinished(Run * r) {
|
|||||||
.set("started", r->startedAt)
|
.set("started", r->startedAt)
|
||||||
.set("result", to_string(r->result))
|
.set("result", to_string(r->result))
|
||||||
.set("reason", r->reason());
|
.set("reason", r->reason());
|
||||||
j.startArray("tags");
|
|
||||||
for(const str& t: jobTags[r->name]) {
|
|
||||||
j.String(t.c_str());
|
|
||||||
}
|
|
||||||
j.EndArray();
|
|
||||||
j.startArray("artifacts");
|
j.startArray("artifacts");
|
||||||
populateArtifacts(j, r->name, r->build);
|
populateArtifacts(j, r->name, r->build);
|
||||||
j.EndArray();
|
j.EndArray();
|
||||||
@@ -809,7 +780,7 @@ bool Laminar::handleBadgeRequest(std::string job, std::string &badge) {
|
|||||||
const char* gradient1 = (rs == RunState::SUCCESS) ? "#2aff4d" : "#ff2a2a";
|
const char* gradient1 = (rs == RunState::SUCCESS) ? "#2aff4d" : "#ff2a2a";
|
||||||
const char* gradient2 = (rs == RunState::SUCCESS) ? "#24b43c" : "#b42424";
|
const char* gradient2 = (rs == RunState::SUCCESS) ? "#24b43c" : "#b42424";
|
||||||
char* svg = NULL;
|
char* svg = NULL;
|
||||||
asprintf(&svg,
|
if(asprintf(&svg,
|
||||||
R"x(
|
R"x(
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
|
<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
|
||||||
<clipPath id="clip">
|
<clipPath id="clip">
|
||||||
@@ -829,13 +800,21 @@ R"x(
|
|||||||
<rect x="%d" width="%d" height="20" fill="url(#status)"/>
|
<rect x="%d" width="%d" height="20" fill="url(#status)"/>
|
||||||
<text x="%d" y="14" fill="#000">%s</text>
|
<text x="%d" y="14" fill="#000">%s</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>)x", jobNameWidth+statusWidth, jobNameWidth+statusWidth, gradient1, gradient2, jobNameWidth, jobNameWidth/2+1, job.data(), jobNameWidth, statusWidth, jobNameWidth+statusWidth/2, status.data());
|
</svg>)x", jobNameWidth+statusWidth, jobNameWidth+statusWidth, gradient1, gradient2, jobNameWidth, jobNameWidth/2+1, job.data(), jobNameWidth, statusWidth, jobNameWidth+statusWidth/2, status.data()) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
badge = svg;
|
badge = svg;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: deprecate
|
||||||
std::string Laminar::getCustomCss() {
|
std::string Laminar::getCustomCss() {
|
||||||
KJ_IF_MAYBE(cssFile, fsHome->tryOpenFile(kj::Path{"custom","style.css"})) {
|
KJ_IF_MAYBE(cssFile, fsHome->tryOpenFile(kj::Path{"custom","style.css"})) {
|
||||||
|
static bool warningShown = false;
|
||||||
|
if(!warningShown) {
|
||||||
|
LLOG(WARNING, "Custom CSS has been deprecated and will be removed from a future release. Use a custom HTML template instead.");
|
||||||
|
warningShown = true;
|
||||||
|
}
|
||||||
return (*cssFile)->readAllText().cStr();
|
return (*cssFile)->readAllText().cStr();
|
||||||
} else {
|
} else {
|
||||||
return std::string();
|
return std::string();
|
||||||
|
|||||||
@@ -21,15 +21,15 @@
|
|||||||
|
|
||||||
#include "run.h"
|
#include "run.h"
|
||||||
#include "monitorscope.h"
|
#include "monitorscope.h"
|
||||||
#include "node.h"
|
#include "context.h"
|
||||||
#include "database.h"
|
#include "database.h"
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <kj/filesystem.h>
|
#include <kj/filesystem.h>
|
||||||
#include <kj/async-io.h>
|
#include <kj/async-io.h>
|
||||||
|
|
||||||
// Node name to node object map
|
// Context name to context object map
|
||||||
typedef std::unordered_map<std::string, std::shared_ptr<Node>> NodeMap;
|
typedef std::unordered_map<std::string, std::shared_ptr<Context>> ContextMap;
|
||||||
|
|
||||||
struct Server;
|
struct Server;
|
||||||
class Json;
|
class Json;
|
||||||
@@ -103,11 +103,10 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool loadConfiguration();
|
bool loadConfiguration();
|
||||||
|
void loadCustomizations();
|
||||||
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*);
|
|
||||||
bool nodeCanQueue(const Node&, std::string jobName) const;
|
|
||||||
// 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;
|
||||||
|
|
||||||
@@ -120,13 +119,17 @@ private:
|
|||||||
|
|
||||||
std::unordered_map<std::string, uint> buildNums;
|
std::unordered_map<std::string, uint> buildNums;
|
||||||
|
|
||||||
std::unordered_map<std::string, std::set<std::string>> jobTags;
|
std::unordered_map<std::string, std::set<std::string>> jobContexts;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> jobDescriptions;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> jobGroups;
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
RunSet activeJobs;
|
RunSet activeJobs;
|
||||||
Database* db;
|
Database* db;
|
||||||
Server& srv;
|
Server& srv;
|
||||||
NodeMap nodes;
|
ContextMap contexts;
|
||||||
kj::Path homePath;
|
kj::Path homePath;
|
||||||
kj::Own<const kj::Directory> fsHome;
|
kj::Own<const kj::Directory> fsHome;
|
||||||
uint numKeepRunDirs;
|
uint numKeepRunDirs;
|
||||||
|
|||||||
298
src/leader.cpp
Normal file
298
src/leader.cpp
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
///
|
||||||
|
/// Copyright 2019-2020 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);
|
||||||
|
return this->ioContext.provider->getTimer().afterDelay(2*kj::SECONDS).then([this]{
|
||||||
|
fprintf(stderr, "[laminar] sending SIGKILL to process group %d\n", currentGroupId);
|
||||||
|
kill(-currentGroupId, SIGKILL);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
LSYSCALL(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_
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2019 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -67,5 +67,7 @@ namespace _ {
|
|||||||
for (::kj::_::Debug::Fault f(__FILE_BASE__, __LINE__, \
|
for (::kj::_::Debug::Fault f(__FILE_BASE__, __LINE__, \
|
||||||
_kjSyscallResult.getErrorNumber(), #call, #__VA_ARGS__, ##__VA_ARGS__);; f.fatal())
|
_kjSyscallResult.getErrorNumber(), #call, #__VA_ARGS__, ##__VA_ARGS__);; f.fatal())
|
||||||
|
|
||||||
|
const char* laminar_version();
|
||||||
|
|
||||||
#endif // LAMINAR_LOG_H_
|
#endif // LAMINAR_LOG_H_
|
||||||
|
|
||||||
|
|||||||
35
src/main.cpp
35
src/main.cpp
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2016 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -17,11 +17,17 @@
|
|||||||
/// 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 <fcntl.h>
|
||||||
|
#include <iostream>
|
||||||
#include <kj/async-unix.h>
|
#include <kj/async-unix.h>
|
||||||
#include <kj/filesystem.h>
|
#include <kj/filesystem.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
static Laminar* laminar;
|
static Laminar* laminar;
|
||||||
static Server* server;
|
static Server* server;
|
||||||
@@ -40,15 +46,36 @@ constexpr const char* INTADDR_HTTP_DEFAULT = "*:8080";
|
|||||||
constexpr const char* ARCHIVE_URL_DEFAULT = "/archive/";
|
constexpr const char* ARCHIVE_URL_DEFAULT = "/archive/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void usage(std::ostream& out) {
|
||||||
|
out << "laminard version " << laminar_version() << "\n";
|
||||||
|
out << "Usage:\n";
|
||||||
|
out << " -h|--help show this help message\n";
|
||||||
|
out << " -v enable verbose output\n";
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
} else if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||||
|
return usage(std::cout), EXIT_SUCCESS;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Unknown argument " << argv[i] << "\n";
|
||||||
|
return usage(std::cerr), EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The parent process hopefully connected stdin to /dev/null, but
|
||||||
|
// do it again here just in case. This is important because stdin
|
||||||
|
// is inherited to job runs via the leader process, and some
|
||||||
|
// processes misbehave if they can successfully block on reading
|
||||||
|
// from stdin.
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
LASSERT(open("/dev/null", O_RDONLY) == STDIN_FILENO);
|
||||||
|
|
||||||
auto ioContext = kj::setupAsyncIo();
|
auto ioContext = kj::setupAsyncIo();
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
@@ -66,6 +93,8 @@ int main(int argc, char** argv) {
|
|||||||
signal(SIGINT, &laminar_quit);
|
signal(SIGINT, &laminar_quit);
|
||||||
signal(SIGTERM, &laminar_quit);
|
signal(SIGTERM, &laminar_quit);
|
||||||
|
|
||||||
|
printf("laminard version %s started\n", laminar_version());
|
||||||
|
|
||||||
server->start();
|
server->start();
|
||||||
|
|
||||||
delete laminar;
|
delete laminar;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2019 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -27,17 +27,17 @@
|
|||||||
extern const char _binary_##name##_z_end[]; \
|
extern const char _binary_##name##_z_end[]; \
|
||||||
resources.emplace(route, Resource{_binary_ ## name ## _z_start, _binary_ ## name ## _z_end, content_type})
|
resources.emplace(route, Resource{_binary_ ## name ## _z_start, _binary_ ## name ## _z_end, content_type})
|
||||||
|
|
||||||
#define CONTENT_TYPE_HTML "text/html; charset=utf-8"
|
#define CONTENT_TYPE_HTML "text/html; charset=utf-8"
|
||||||
#define CONTENT_TYPE_ICO "image/x-icon"
|
#define CONTENT_TYPE_ICO "image/x-icon"
|
||||||
#define CONTENT_TYPE_PNG "image/png"
|
#define CONTENT_TYPE_PNG "image/png"
|
||||||
#define CONTENT_TYPE_JS "application/javascript; charset=utf-8"
|
#define CONTENT_TYPE_JS "application/javascript; charset=utf-8"
|
||||||
#define CONTENT_TYPE_CSS "text/css; charset=utf-8"
|
#define CONTENT_TYPE_CSS "text/css; charset=utf-8"
|
||||||
|
#define CONTENT_TYPE_MANIFEST "application/manifest+json; charset=utf-8"
|
||||||
|
|
||||||
#define GZIP_FORMAT 16
|
#define GZIP_FORMAT 16
|
||||||
|
|
||||||
Resources::Resources()
|
Resources::Resources()
|
||||||
{
|
{
|
||||||
INIT_RESOURCE("/", index_html, CONTENT_TYPE_HTML);
|
|
||||||
INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO);
|
INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO);
|
||||||
INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG);
|
INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG);
|
||||||
INIT_RESOURCE("/icon.png", icon_png, CONTENT_TYPE_PNG);
|
INIT_RESOURCE("/icon.png", icon_png, CONTENT_TYPE_PNG);
|
||||||
@@ -47,47 +47,72 @@ Resources::Resources()
|
|||||||
INIT_RESOURCE("/js/vue-router.min.js", js_vue_router_min_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/vue-router.min.js", js_vue_router_min_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
|
INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
|
||||||
INIT_RESOURCE("/css/bootstrap.min.css", css_bootstrap_min_css, CONTENT_TYPE_CSS);
|
INIT_RESOURCE("/style.css", style_css, CONTENT_TYPE_CSS);
|
||||||
|
INIT_RESOURCE("/manifest.webmanifest", manifest_webmanifest, CONTENT_TYPE_MANIFEST);
|
||||||
|
// Configure the default template
|
||||||
|
setHtmlTemplate(std::string());
|
||||||
|
}
|
||||||
|
|
||||||
if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) {
|
void Resources::setHtmlTemplate(std::string tmpl) {
|
||||||
// The administrator needs to customize the <base href>. Unfortunately this seems
|
extern const char _binary_index_html_z_start[];
|
||||||
// to be the only thing that needs to be customizable but cannot be done via dynamic
|
extern const char _binary_index_html_z_end[];
|
||||||
// DOM manipulation without heavy compromises. So replace the static char array with
|
|
||||||
// a modified buffer accordingly.
|
z_stream strm;
|
||||||
z_stream strm;
|
memset(&strm, 0, sizeof(z_stream));
|
||||||
memset(&strm, 0, sizeof(z_stream));
|
|
||||||
std::string tmp;
|
if(!tmpl.empty()) {
|
||||||
tmp.resize(INDEX_HTML_UNCOMPRESSED_SIZE);
|
|
||||||
// inflate
|
|
||||||
inflateInit2(&strm, MAX_WBITS|GZIP_FORMAT);
|
|
||||||
strm.next_in = (unsigned char*) _binary_index_html_z_start;
|
|
||||||
strm.avail_in = _binary_index_html_z_end - _binary_index_html_z_start;
|
|
||||||
strm.next_out = (unsigned char*) tmp.data();
|
|
||||||
strm.avail_out = INDEX_HTML_UNCOMPRESSED_SIZE;
|
|
||||||
if(inflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
|
||||||
LLOG(FATAL, "Failed to uncompress index_html");
|
|
||||||
}
|
|
||||||
// replace
|
|
||||||
// There's no validation on the replacement string, so you can completely mangle
|
|
||||||
// the html if you like. This isn't really an issue because if you can modify laminar's
|
|
||||||
// environment you already have elevated permissions
|
|
||||||
if(auto it = tmp.find("base href=\"/"))
|
|
||||||
tmp.replace(it+11, 1, baseUrl);
|
|
||||||
// deflate
|
// deflate
|
||||||
index_html.resize(tmp.size());
|
index_html.resize(tmpl.size());
|
||||||
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
|
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
|
||||||
strm.next_in = (unsigned char*) tmp.data();
|
strm.next_in = (unsigned char*) tmpl.data();
|
||||||
strm.avail_in = tmp.size();
|
strm.avail_in = tmpl.size();
|
||||||
strm.next_out = (unsigned char*) index_html.data();
|
strm.next_out = (unsigned char*) index_html.data();
|
||||||
strm.avail_out = tmp.size();
|
strm.avail_out = tmpl.size();
|
||||||
if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||||
LLOG(FATAL, "Failed to compress index.html");
|
LLOG(FATAL, "Failed to compress index.html");
|
||||||
}
|
}
|
||||||
index_html.resize(strm.total_out);
|
index_html.resize(strm.total_out);
|
||||||
// update resource map
|
} else {
|
||||||
resources["/"].start = index_html.data();
|
// use the default template from compile-time asset
|
||||||
resources["/"].end = index_html.data() + index_html.size();
|
if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) {
|
||||||
|
// The administrator needs to customize the <base href>. Unfortunately this seems
|
||||||
|
// to be the only thing that needs to be customizable but cannot be done via dynamic
|
||||||
|
// DOM manipulation without heavy compromises. So replace the static char array with
|
||||||
|
// a modified buffer accordingly.
|
||||||
|
std::string tmp;
|
||||||
|
tmp.resize(INDEX_HTML_UNCOMPRESSED_SIZE);
|
||||||
|
// inflate
|
||||||
|
inflateInit2(&strm, MAX_WBITS|GZIP_FORMAT);
|
||||||
|
strm.next_in = (unsigned char*) _binary_index_html_z_start;
|
||||||
|
strm.avail_in = _binary_index_html_z_end - _binary_index_html_z_start;
|
||||||
|
strm.next_out = (unsigned char*) tmp.data();
|
||||||
|
strm.avail_out = INDEX_HTML_UNCOMPRESSED_SIZE;
|
||||||
|
if(inflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||||
|
LLOG(FATAL, "Failed to uncompress index_html");
|
||||||
|
}
|
||||||
|
// replace
|
||||||
|
// There's no validation on the replacement string, so you can completely mangle
|
||||||
|
// the html if you like. This isn't really an issue because if you can modify laminar's
|
||||||
|
// environment you already have elevated permissions
|
||||||
|
if(auto it = tmp.find("base href=\"/"))
|
||||||
|
tmp.replace(it+11, 1, baseUrl);
|
||||||
|
// deflate
|
||||||
|
index_html.resize(tmp.size());
|
||||||
|
deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
|
||||||
|
strm.next_in = (unsigned char*) tmp.data();
|
||||||
|
strm.avail_in = tmp.size();
|
||||||
|
strm.next_out = (unsigned char*) index_html.data();
|
||||||
|
strm.avail_out = tmp.size();
|
||||||
|
if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
|
||||||
|
LLOG(FATAL, "Failed to compress index.html");
|
||||||
|
}
|
||||||
|
index_html.resize(strm.total_out);
|
||||||
|
} else {
|
||||||
|
index_html = std::string(_binary_index_html_z_start, _binary_index_html_z_end);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// update resource map
|
||||||
|
resources["/"] = Resource{index_html.data(), index_html.data() + index_html.size(), CONTENT_TYPE_HTML};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool beginsWith(std::string haystack, const char* needle) {
|
inline bool beginsWith(std::string haystack, const char* needle) {
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public:
|
|||||||
// type. Function returns false if no resource for the given path exists
|
// type. Function returns false if no resource for the given path exists
|
||||||
bool handleRequest(std::string path, const char** start, const char** end, const char** content_type);
|
bool handleRequest(std::string path, const char** start, const char** end, const char** content_type);
|
||||||
|
|
||||||
|
// Allows providing a custom HTML template. Pass an empty string to use the default.
|
||||||
|
void setHtmlTemplate(std::string templ = std::string());
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Resource {
|
struct Resource {
|
||||||
const char* start;
|
const char* start;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@@ -8,333 +8,201 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<link rel="apple-touch-icon-precomposed" href="favicon-152.png">
|
<link rel="apple-touch-icon-precomposed" href="favicon-152.png">
|
||||||
<link rel="icon" href="favicon.ico">
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<title>Laminar</title>
|
<title>Laminar</title>
|
||||||
<script src="js/vue.min.js"></script>
|
<script src="js/vue.min.js"></script>
|
||||||
<script src="js/vue-router.min.js"></script>
|
<script src="js/vue-router.min.js"></script>
|
||||||
<script src="js/ansi_up.js"></script>
|
<script src="js/ansi_up.js"></script>
|
||||||
<script src="js/Chart.min.js"></script>
|
<script src="js/Chart.min.js"></script>
|
||||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="custom/style.css" rel="stylesheet">
|
|
||||||
<script src="js/app.js" defer></script>
|
<script src="js/app.js" defer></script>
|
||||||
<style>
|
|
||||||
body, html { height: 100%; }
|
|
||||||
.navbar { margin-bottom: 0; border-radius: 0; }
|
|
||||||
.navbar-brand { margin: 0 -15px; padding: 7px 15px }
|
|
||||||
.navbar-brand>img { display: inline; }
|
|
||||||
a.navbar-btn { color: #9d9d9d; }
|
|
||||||
a.navbar-btn.active { color: #fff; }
|
|
||||||
a.navbar-btn:hover { color: #fff; text-decoration: none; }
|
|
||||||
a.navbar-btn:focus { color: #fff; }
|
|
||||||
.bell { margin: 8px 15px; color: #9d9d9d; }
|
|
||||||
.bell:hover { text-decoration: none; color: #9d9d9d; cursor: pointer; }
|
|
||||||
.bell.active { color: #333; }
|
|
||||||
dt,dd { line-height: 2; }
|
|
||||||
canvas {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 800px;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
.progress {
|
|
||||||
height: 10px;
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
table#joblist tr:first-child td { border-top: 0; }
|
|
||||||
#popup-connecting {
|
|
||||||
position: fixed;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
/* status icons */
|
|
||||||
span.status {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
text-align: center;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
span.success { color: forestgreen; }
|
|
||||||
span.failed { color: firebrick; }
|
|
||||||
span.aborted { color: indigo; }
|
|
||||||
span.spin {
|
|
||||||
color: steelblue;
|
|
||||||
animation: 2s linear infinite spin;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
/* chart overlay */
|
|
||||||
li.chart-overlay {
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid lightgray;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
/* sort indicators */
|
|
||||||
a.sort {
|
|
||||||
position: relative;
|
|
||||||
margin-left: 7px;
|
|
||||||
}
|
|
||||||
a.sort:before, a.sort:after {
|
|
||||||
border: 4px solid transparent;
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
a.sort:before {
|
|
||||||
border-bottom-color: #ccc;
|
|
||||||
margin-top: -9px;
|
|
||||||
}
|
|
||||||
a.sort:after {
|
|
||||||
border-top-color: #ccc;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
a.sort.dsc:after { border-top-color: #000; }
|
|
||||||
a.sort.asc:before { border-bottom-color: #000; }
|
|
||||||
a.sort:hover { text-decoration: none; cursor:pointer; }
|
|
||||||
a.sort:not(.asc):hover:before { border-bottom-color: #777; }
|
|
||||||
a.sort:not(.dsc):hover:after { border-top-color: #777; }
|
|
||||||
|
|
||||||
</style>
|
<link href="style.css" rel="stylesheet">
|
||||||
|
<link href="custom/style.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<template id="home"><div>
|
<template id="home"><div id="page-home-main">
|
||||||
<ol class="breadcrumb"><li class="active">Home</li></ol>
|
<nav>
|
||||||
<div class="container-fluid"><div class="row">
|
<table class="table striped">
|
||||||
<div class="col-sm-5 col-md-4 col-lg-3 dash">
|
<tr v-for="job in jobsQueued">
|
||||||
<table class="table table-bordered">
|
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <i>queued</i></td>
|
||||||
<tr v-for="job in jobsQueued">
|
</tr>
|
||||||
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <i>queued</i></td>
|
<tr v-for="job in jobsRunning">
|
||||||
</tr>
|
<td>
|
||||||
<tr v-for="job in jobsRunning">
|
<span v-html="runIcon(job.result)"></span>
|
||||||
<td><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link>
|
<router-link :to="'/jobs/'+job.name">{{job.name}}</router-link>
|
||||||
<small class="pull-right">{{formatDuration(job.started, job.completed)}}</small>
|
<router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link>
|
||||||
<div class="progress">
|
<small style="float:right;">{{formatDuration(job.started, job.completed)}}</small>
|
||||||
<div class="progress-bar progress-bar-striped" :class="'progress-bar-'+(job.overtime?'warning':'info')" :class="job.etc?'':'active'" :style="'width:'+(!job.etc?'100':job.progress)+'%'"></div>
|
<div class="progress" style="margin-top: 5px;">
|
||||||
</div>
|
<div class="progress-bar" :class="{overtime:job.overtime,indeterminate:!job.etc}" :style="job.etc && {width:job.progress+'%'}"></div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
<tr v-for="job in jobsRecent">
|
</tr>
|
||||||
<td><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+job.name">{{job.name}}</router-link> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link><br><small>Took {{formatDuration(job.started, job.completed)}} at {{formatDate(job.started)}}</small></td>
|
<tr v-for="job in jobsRecent">
|
||||||
</tr>
|
<td>
|
||||||
</table>
|
<span v-html="runIcon(job.result)"></span>
|
||||||
|
<router-link :to="'/jobs/'+job.name">{{job.name}}</router-link>
|
||||||
|
<router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link><br>
|
||||||
|
<small>Took {{formatDuration(job.started, job.completed)}} at {{formatDate(job.started)}}</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</nav>
|
||||||
|
<section style="border-left: 1px solid #d0d0d0;">
|
||||||
|
<div id="page-home-stats">
|
||||||
|
<div>
|
||||||
|
<h3>Recent regressions</h3>
|
||||||
|
<table>
|
||||||
|
<tr v-for="job in resultChanged" v-if="job.lastFailure>job.lastSuccess"><td><router-link :to="'/jobs/'+job.name+'/'+job.lastFailure">{{job.name}} #{{job.lastFailure}}</router-link> since <router-link :to="'/jobs/'+job.name+'/'+job.lastSuccess">#{{job.lastSuccess}}</router-link></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Low pass rates</h3>
|
||||||
|
<table>
|
||||||
|
<tr v-for="job in lowPassRates"><td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link></td><td>{{Math.round(job.passRate*100)}} %</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Utilization</h3>
|
||||||
|
<div><canvas id="chartUtil"></canvas></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-7 col-md-8 col-lg-9"><div class="row">
|
<div>
|
||||||
<div class="col-md-6">
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 5px; padding: 5px;">
|
||||||
<div class="panel panel-default">
|
<div><canvas id="chartBpd"></canvas></div>
|
||||||
<div class="panel-heading">Total runs per day this week</div>
|
<div><canvas id="chartBpj"></canvas></div>
|
||||||
<div class="panel-body">
|
<div><canvas id="chartTpj"></canvas></div>
|
||||||
<canvas id="chartBpd"></canvas>
|
<div><canvas id="chartBuildTimeChanges"></canvas></div>
|
||||||
</div>
|
<div><canvas id="chartBuildTimeDist"></canvas></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<div class="panel panel-default">
|
</section>
|
||||||
<div class="panel-heading">Most runs per job in the last 24 hours</div>
|
|
||||||
<div class="panel-body" id="chartStatus">
|
|
||||||
<canvas id="chartBpj"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div></div><div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Longest average run time per job this week</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="chartTpj"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Current executor utilization</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="chartUtil"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div></div><div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Regressions and recoveries</div>
|
|
||||||
<div class="panel-body"><div style="position:relative">
|
|
||||||
<canvas id="chartResultChanges"></canvas>
|
|
||||||
<ul v-for="job in resultChanged">
|
|
||||||
<li v-if="job.lastFailure>job.lastSuccess" :id="'rcd_'+job.name" class="chart-overlay"><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link>: <span v-html="runIcon('failed')"></span> <router-link :to="'/jobs/'+job.name+'/'+job.lastFailure">#{{job.lastFailure}}</router-link> since <span v-html="runIcon('success')"></span> <router-link :to="'/jobs/'+job.name+'/'+job.lastSuccess">#{{job.lastSuccess}}</router-link></li>
|
|
||||||
<li v-if="job.lastFailure<job.lastSuccess" :id="'rcd_'+job.name" class="chart-overlay"><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link>: <span v-html="runIcon('success')"></span> <router-link :to="'/jobs/'+job.name+'/'+job.lastSuccess">#{{job.lastSuccess}}</router-link> since <span v-html="runIcon('failed')"></span> <router-link :to="'/jobs/'+job.name+'/'+job.lastFailure">#{{job.lastFailure}}</router-link></li>
|
|
||||||
</ul>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Low pass rates</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="chartPassRates"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div></div><div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Run time changes</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="chartBuildTimeChanges"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Average run time distribution</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<canvas id="chartBuildTimeDist"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
</div></div>
|
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<template id="jobs"><div>
|
<template id="jobs"><div>
|
||||||
<ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li class="active">Jobs</li></ol>
|
<nav style="display: grid; grid-auto-flow: column; justify-content: space-between; align-items: end; padding: 10px 15px;">
|
||||||
<div class="container-fluid"><div class="row">
|
<div style="display:grid; grid-auto-flow: column; grid-gap: 15px; padding: 5px 0;">
|
||||||
<div class="col-xs-12">
|
<a v-show="ungrouped.length" :class="{'active':group==null}" href v-on:click.prevent="group = null">Ungrouped Jobs</a>
|
||||||
<div class="pull-right">
|
<a v-for="g in Object.keys(groups)" :class="{'active':g==group}" href v-on:click.prevent="group = g">{{g}}</a>
|
||||||
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
|
|
||||||
</div>
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li :class="{'active':tag==null}"><a href v-on:click.prevent="tag = null">All Jobs</a></li>
|
|
||||||
<li v-for="t in tags" :class="{'active':t==tag}"><a href v-on:click.prevent="tag = t">{{t}}</a></li>
|
|
||||||
</ul>
|
|
||||||
<table class="table table-striped" id="joblist">
|
|
||||||
<tr v-for="job in filteredJobs()">
|
|
||||||
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link></td>
|
|
||||||
<td class="text-center"><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link></td>
|
|
||||||
<td class="text-center">{{formatDate(job.started)}}</td>
|
|
||||||
<td class="text-center">{{formatDuration(job.started,job.completed)}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div></div>
|
<input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
|
||||||
|
</nav>
|
||||||
|
<table class="striped" id="job-list">
|
||||||
|
<tr v-for="job in filteredJobs()">
|
||||||
|
<td><router-link :to="'/jobs/'+job.name">{{job.name}}</router-link></td>
|
||||||
|
<td style="white-space: nowrap;"><span v-html="runIcon(job.result)"></span> <router-link :to="'/jobs/'+job.name+'/'+job.number">#{{job.number}}</router-link></td>
|
||||||
|
<td>{{formatDate(job.started)}}</td>
|
||||||
|
<td>{{formatDuration(job.started,job.completed)}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<template id="job"><div>
|
<template id="job"><div id="page-job-main">
|
||||||
<ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li><router-link to="/jobs">Jobs</router-link></li><li class="active">{{$route.params.name}}</li></ol></ol>
|
<div style="padding: 15px;">
|
||||||
<div class="container-fluid">
|
<h2>{{$route.params.name}}</h2>
|
||||||
<div class="row">
|
<div v-html="description"></div>
|
||||||
<div class="col-sm-5 col-md-6 col-lg-7">
|
<dl>
|
||||||
<h3>{{$route.params.name}}</h3>
|
<dt>Last Successful Run</dt>
|
||||||
<dl class="dl-horizontal">
|
<dd><router-link v-if="lastSuccess" :to="'/jobs/'+$route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd>
|
||||||
<dt>Last Successful Run</dt>
|
<dt>Last Failed Run</dt>
|
||||||
<dd><router-link v-if="lastSuccess" :to="'/jobs/'+$route.params.name+'/'+lastSuccess.number">#{{lastSuccess.number}}</router-link> {{lastSuccess?' - at '+formatDate(lastSuccess.started):'never'}}</dd>
|
<dd><router-link v-if="lastFailed" :to="'/jobs/'+$route.params.name+'/'+lastFailed.number">#{{lastFailed.number}}</router-link> {{lastFailed?' - at '+formatDate(lastFailed.started):'never'}}</dd>
|
||||||
<dt>Last Failed Run</dt>
|
</dl>
|
||||||
<dd><router-link v-if="lastFailed" :to="'/jobs/'+$route.params.name+'/'+lastFailed.number">#{{lastFailed.number}}</router-link> {{lastFailed?' - at '+formatDate(lastFailed.started):'never'}}</dd>
|
</div>
|
||||||
</dl>
|
<div style="display: grid; justify-content: center; padding: 15px;">
|
||||||
</div>
|
<canvas id="chartBt"></canvas>
|
||||||
<div class="col-sm-7 col-md-6 col-lg-5">
|
</div>
|
||||||
<div class="panel panel-default">
|
<div style="grid-column: 1/-1">
|
||||||
<div class="panel-heading">Build time</div>
|
<table class="striped">
|
||||||
<div class="panel-body">
|
<thead><tr>
|
||||||
<canvas id="chartBt"></canvas>
|
<th><a class="sort" :class="(sort.field=='result'?sort.order:'')" v-on:click="do_sort('result')"> </a></th>
|
||||||
</div>
|
<th>Run <a class="sort" :class="(sort.field=='number'?sort.order:'')" v-on:click="do_sort('number')"> </a></th>
|
||||||
</div>
|
<th class="text-center">Started <a class="sort" :class="(sort.field=='started'?sort.order:'')" v-on:click="do_sort('started')"> </a></th>
|
||||||
</div>
|
<th class="text-center">Duration <a class="sort" :class="(sort.field=='duration'?sort.order:'')" v-on:click="do_sort('duration')"> </a></th>
|
||||||
|
<th class="text-center vp-sm-hide">Reason <a class="sort" :class="(sort.field=='reason'?sort.order:'')" v-on:click="do_sort('reason')"> </a></th>
|
||||||
|
</tr></thead>
|
||||||
|
<tr v-show="nQueued">
|
||||||
|
<td colspan="5"><i>{{nQueued}} run(s) queued</i></td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="job in jobsRunning.concat(jobsRecent)" track-by="$index">
|
||||||
|
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
|
||||||
|
<td><router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></td>
|
||||||
|
<td class="text-center">{{formatDate(job.started)}}</td>
|
||||||
|
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
||||||
|
<td class="text-center vp-sm-hide">{{job.reason}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="float: right; margin: 15px; display: inline-grid; grid-auto-flow: column; gap: 10px; align-items: center">
|
||||||
|
<button v-on:click="page_prev" :disabled="sort.page==0">«</button>
|
||||||
|
<span>Page {{sort.page+1}} of {{pages}}</span>
|
||||||
|
<button class="btn" v-on:click="page_next" :disabled="sort.page==pages-1">»</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row"><div class="col-xs-12">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead><tr>
|
|
||||||
<th><a class="sort" :class="(sort.field=='result'?sort.order:'')" v-on:click="do_sort('result')"> </a></th>
|
|
||||||
<th>Run <a class="sort" :class="(sort.field=='number'?sort.order:'')" v-on:click="do_sort('number')"> </a></th>
|
|
||||||
<th class="text-center">Started <a class="sort" :class="(sort.field=='started'?sort.order:'')" v-on:click="do_sort('started')"> </a></th>
|
|
||||||
<th class="text-center">Duration <a class="sort" :class="(sort.field=='duration'?sort.order:'')" v-on:click="do_sort('duration')"> </a></th>
|
|
||||||
<th class="text-center hidden-xs">Reason <a class="sort" :class="(sort.field=='reason'?sort.order:'')" v-on:click="do_sort('reason')"> </a></th>
|
|
||||||
</tr></thead>
|
|
||||||
<tr v-show="nQueued">
|
|
||||||
<td colspan="5"><i>{{nQueued}} run(s) queued</i></td>
|
|
||||||
</tr>
|
|
||||||
<tr v-for="job in jobsRunning" track-by="$index">
|
|
||||||
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
|
|
||||||
<td><router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></td>
|
|
||||||
<td class="text-center">{{formatDate(job.started)}}</td>
|
|
||||||
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
|
||||||
<td class="text-center hidden-xs">{{job.reason}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-for="job in jobsRecent" track-by="$index">
|
|
||||||
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
|
|
||||||
<td><router-link :to="'/jobs/'+$route.params.name+'/'+job.number">#{{job.number}}</router-link></td>
|
|
||||||
<td class="text-center">{{formatDate(job.started)}}</td>
|
|
||||||
<td class="text-center">{{formatDuration(job.started, job.completed)}}</td>
|
|
||||||
<td class="text-center hidden-xs">{{job.reason}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<ul class="pagination pull-right">
|
|
||||||
<li><button class="btn btn-default" v-on:click="page_prev" :disabled="sort.page==0">«</button></li>
|
|
||||||
<li>Page {{sort.page+1}} of {{pages}}</li>
|
|
||||||
<li><button class="btn btn-default" v-on:click="page_next" :disabled="sort.page==pages-1">»</button></li>
|
|
||||||
</ul>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<template id="run"><div>
|
<template id="run"><div style="display: grid; grid-template-rows: auto 1fr">
|
||||||
<ol class="breadcrumb"><li><router-link to="/">Home</router-link></li><li><router-link to="/jobs">Jobs</router-link></li><li><router-link :to="'/jobs/'+$route.params.name">{{$route.params.name}}</router-link></li><li class="active">#{{$route.params.number}}</li></ol></ol>
|
<div style="padding: 15px">
|
||||||
<div class="container-fluid">
|
<div style="display: grid; grid-template-columns: auto 1fr 400px auto auto; gap: 5px; align-items: center">
|
||||||
<div class="row">
|
<h2 style="white-space: nowrap"><span v-html="runIcon(job.result)"></span> {{$route.params.name}} #{{$route.params.number}}</h2>
|
||||||
<div class="col-sm-5 col-md-6 col-lg-7">
|
<span></span>
|
||||||
<h3 style="float:left"><span v-html="runIcon(job.result)"></span> {{$route.params.name}} #{{$route.params.number}}</h3>
|
<div><!-- extra div to preserve grid columns when v-show hides the progress bar -->
|
||||||
<nav class="pull-left">
|
|
||||||
<ul class="pagination" style="margin:15px 20px">
|
|
||||||
<li v-show="$route.params.number > 1"><router-link :to="'/jobs/'+$route.params.name+'/'+($route.params.number-1)">«</router-link></li>
|
|
||||||
<li v-show="latestNum > $route.params.number"><router-link :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)">»</router-link></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div style="clear:both;"></div>
|
|
||||||
<dl class="dl-horizontal">
|
|
||||||
<dt>Reason</dt><dd>{{job.reason}}</dd>
|
|
||||||
<dt v-show="job.upstream.num > 0">Upstream</dt><dd v-show="job.upstream.num > 0"><router-link :to="'/jobs/'+job.upstream.name">{{job.upstream.name}}</router-link> <router-link :to="'/jobs/'+job.upstream.name+'/'+job.upstream.num">#{{job.upstream.num}}</router-link></li></dd>
|
|
||||||
<dt>Queued for</dt><dd>{{job.queued}}s</dd>
|
|
||||||
<dt>Started</dt><dd>{{formatDate(job.started)}}</dd>
|
|
||||||
<dt v-show="runComplete(job)">Completed</dt><dd v-show="job.completed">{{formatDate(job.completed)}}</dd>
|
|
||||||
<dt>Duration</dt><dd>{{formatDuration(job.started, job.completed)}}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-7 col-md-6 col-lg-5">
|
|
||||||
<div class="progress" v-show="job.result == 'running'">
|
<div class="progress" v-show="job.result == 'running'">
|
||||||
<div class="progress-bar progress-bar-striped" :class="'progress-bar-'+(job.overtime?'warning':'info')" :class="job.etc?'':'active'" :style="{width:!job.etc?100:job.progress + '%'}"></div>
|
<div class="progress-bar" :class="{overtime:job.overtime,indeterminate:!job.etc}" :style="job.etc && {width:job.progress+'%'}"></div>
|
||||||
</div>
|
|
||||||
<div class="panel panel-default" v-show="job.artifacts.length">
|
|
||||||
<div class="panel-heading">Artifacts</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<ul class="list-unstyled" style="margin-bottom: 0">
|
|
||||||
<li v-for="art in job.artifacts"><a :href="art.url" target="_self">{{art.filename}}</a> [{{ art.size | iecFileSize }}]</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<router-link :disabled="$route.params.number == 1" :to="'/jobs/'+$route.params.name+'/'+($route.params.number-1)" tag="button">«</router-link>
|
||||||
|
<router-link :disabled="$route.params.number == latestNum" :to="'/jobs/'+$route.params.name+'/'+(parseInt($route.params.number)+1)" tag="button">»</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="row"><div class="col-xs-12">
|
<div id="page-run-detail">
|
||||||
<button type="button" class="btn btn-default btn-xs pull-right" :class="{'active':autoscroll}" v-on:click="autoscroll = !autoscroll" style="margin-top:10px">Autoscroll</button>
|
<dl>
|
||||||
<h4>Console output</h4>
|
<dt>Reason</dt><dd>{{job.reason}}</dd>
|
||||||
<pre v-html="log"></pre>
|
<dt v-show="job.upstream.num > 0">Upstream</dt><dd v-show="job.upstream.num > 0"><router-link :to="'/jobs/'+job.upstream.name">{{job.upstream.name}}</router-link> <router-link :to="'/jobs/'+job.upstream.name+'/'+job.upstream.num">#{{job.upstream.num}}</router-link></li></dd>
|
||||||
</div></div>
|
<dt>Queued for</dt><dd>{{job.queued}}s</dd>
|
||||||
|
<dt>Started</dt><dd>{{formatDate(job.started)}}</dd>
|
||||||
|
<dt v-show="runComplete(job)">Completed</dt><dd v-show="job.completed">{{formatDate(job.completed)}}</dd>
|
||||||
|
<dt>Duration</dt><dd>{{formatDuration(job.started, job.completed)}}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl v-show="job.artifacts.length">
|
||||||
|
<dt>Artifacts</dt>
|
||||||
|
<dd>
|
||||||
|
<ul style="margin-bottom: 0">
|
||||||
|
<li v-for="art in job.artifacts"><a :href="art.url" target="_self">{{art.filename}}</a> [{{ art.size | iecFileSize }}]</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="console-log">
|
||||||
|
<code v-html="log"></code>
|
||||||
|
<span v-show="job.result == 'running'" v-html="runIcon('running')" style="display: block;"></span>
|
||||||
</div>
|
</div>
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|
||||||
<div id="app">
|
<main id="app" style="display: grid; grid-template-rows: auto 1fr auto; height: 100%;">
|
||||||
<nav class="navbar navbar-inverse">
|
<nav id="nav-top" style="display: grid; grid-template-columns: auto auto 1fr auto auto; grid-gap: 15px;">
|
||||||
<div>
|
<router-link to="/" style="display: grid; grid-auto-flow: column; align-items: center; margin: 5px; font-size: 20px;">
|
||||||
<router-link to="/" class="navbar-brand"><img src="icon.png">{{title}}</router-link>
|
<img src="icon.png"> {{title}}
|
||||||
<router-link to="/jobs" class="btn navbar-btn pull-right">Jobs</router-link>
|
</router-link>
|
||||||
|
<div id="nav-top-links" style="display: grid; grid-auto-flow: column; justify-content: start; gap: 15px; padding: 0 15px; align-items: center; font-size: 16px;">
|
||||||
|
<router-link to="/jobs">Jobs</router-link>
|
||||||
|
<router-link v-for="(crumb,i) in _route.path.slice(1).split('/').slice(1,-1)" :to="_route.path.split('/').slice(0,i+3).join('/')">{{crumb}}</router-link>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<span class="version">{{version}}</span>
|
||||||
|
<div style="display: grid; align-items: center; padding: 0 15px">
|
||||||
|
<a v-on:click="toggleNotifications(!notify)" class="nav-icon" :class="{active:notify}" v-show="supportsNotifications" :title="(notify?'Disable':'Enable')+' notifications'">
|
||||||
|
<svg width="18" viewBox="0 0 12 12">
|
||||||
|
<g stroke-width="0.5">
|
||||||
|
<path d="m 6,9 c -1,0 -1,0 -1,1 0,1 2,1 2,0 0,-1 0,-1 -1,-1 z" />
|
||||||
|
<path d="m 1,10 c 3,-3 1,-9 5,-9 4,0 2,6 5,9 1,1 -3,-1 -5,-1 -2,0 -6,2 -5,1 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<a v-on:click="toggleNotifications(!notify)" v-show="supportsNotifications" class="bell pull-right" :class="{'active':notify}" :title="(notify?'Disable':'Enable')+' notifications'">🔔</a>
|
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
<div v-show="!connected" id="popup-connecting"><span class="status spin">⚙︎</span> Connecting...</div>
|
<div id="connecting-overlay" :class="{shown:!connected}">
|
||||||
</div>
|
<div><span v-html="runIcon('running')"></span> Connecting...</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ const timeScale = function(max){
|
|||||||
: { scale:function(v){return v;}, label:'Seconds' };
|
: { scale:function(v){return v;}, label:'Seconds' };
|
||||||
}
|
}
|
||||||
const ServerEventHandler = function() {
|
const ServerEventHandler = function() {
|
||||||
function setupEventSource(path, query, next) {
|
function setupEventSource(path, query, next, comp) {
|
||||||
const es = new EventSource(document.head.baseURI + path.substr(1) + query);
|
const es = new EventSource(document.head.baseURI + path.substr(1) + query);
|
||||||
|
es.comp = comp;
|
||||||
es.path = path; // save for later in case we need to add query params
|
es.path = path; // save for later in case we need to add query params
|
||||||
es.onmessage = function(msg) {
|
es.onmessage = function(msg) {
|
||||||
msg = JSON.parse(msg.data);
|
msg = JSON.parse(msg.data);
|
||||||
@@ -46,8 +47,10 @@ const ServerEventHandler = function() {
|
|||||||
this.comp = comp;
|
this.comp = comp;
|
||||||
// 2. needed to close the ws on navigation away
|
// 2. needed to close the ws on navigation away
|
||||||
comp.es = this;
|
comp.es = this;
|
||||||
|
comp.esReconnectInterval = 500;
|
||||||
// Update html and nav titles
|
// Update html and nav titles
|
||||||
document.title = comp.$root.title = msg.title;
|
document.title = comp.$root.title = msg.title;
|
||||||
|
comp.$root.version = msg.version;
|
||||||
// Calculate clock offset (used by ProgressUpdater)
|
// Calculate clock offset (used by ProgressUpdater)
|
||||||
comp.$root.clockSkew = msg.time - Math.floor((new Date()).getTime()/1000);
|
comp.$root.clockSkew = msg.time - Math.floor((new Date()).getTime()/1000);
|
||||||
comp.$root.connected = true;
|
comp.$root.connected = true;
|
||||||
@@ -66,8 +69,14 @@ const ServerEventHandler = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
es.onerror = function() {
|
es.onerror = function(e) {
|
||||||
this.comp.$root.connected = false;
|
this.comp.$root.connected = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.comp.es = setupEventSource(path, query, null, this.comp);
|
||||||
|
}, this.comp.esReconnectInterval);
|
||||||
|
if(this.comp.esReconnectInterval < 7500)
|
||||||
|
this.comp.esReconnectInterval *= 1.5;
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
return es;
|
return es;
|
||||||
}
|
}
|
||||||
@@ -95,15 +104,24 @@ const ServerEventHandler = function() {
|
|||||||
const Utils = {
|
const Utils = {
|
||||||
methods: {
|
methods: {
|
||||||
runIcon(result) {
|
runIcon(result) {
|
||||||
var marker = '⚙';
|
return (result == 'success') ? /* checkmark */
|
||||||
var classname = result;
|
`<svg class="status success" viewBox="0 0 100 100">
|
||||||
if (result === 'success')
|
<path d="m 23,46 c -6,0 -17,3 -17,11 0,8 9,30 12,32 3,2 14,5 20,-2 6,-6 24,-36
|
||||||
marker = '✔';
|
56,-71 5,-3 -9,-8 -23,-2 -13,6 -33,42 -41,47 -6,-3 -5,-12 -8,-15 z" />
|
||||||
else if (result === 'failed' || result === 'aborted')
|
</svg>`
|
||||||
marker = '✘';
|
: (result == 'failed' || result == 'aborted') ? /* cross */
|
||||||
else
|
`<svg class="status failed" viewBox="0 0 100 100">
|
||||||
classname = 'spin';
|
<path d="m 19,20 c 2,8 12,29 15,32 -5,5 -18,21 -21,26 2,3 8,15 11,18 4,-6 17,-21
|
||||||
return '<span title="' + result + '" class="status ' + classname + '">' + marker + '︎</span>';
|
21,-26 5,5 11,15 15,20 8,-2 15,-9 20,-15 -3,-3 -17,-18 -20,-24 3,-5 23,-26 30,-33 -3,-5 -8,-9
|
||||||
|
-12,-12 -6,5 -26,26 -29,30 -6,-8 -11,-15 -15,-23 -3,0 -12,5 -15,7 z" />
|
||||||
|
</svg>`
|
||||||
|
: /* spinner */
|
||||||
|
`<svg class="status running" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="40" stroke-width="15" fill="none" stroke-dasharray="175">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="2s" values="0 50 50;360 50 50"></animateTransform>
|
||||||
|
</circle>
|
||||||
|
</svg>`
|
||||||
|
;
|
||||||
},
|
},
|
||||||
formatDate: function(unix) {
|
formatDate: function(unix) {
|
||||||
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
// TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
|
||||||
@@ -137,7 +155,8 @@ const ProgressUpdater = {
|
|||||||
var p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
var p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
|
||||||
if (p > 1.2) {
|
if (p > 1.2) {
|
||||||
o.overtime = true;
|
o.overtime = true;
|
||||||
} else if (p >= 1) {
|
}
|
||||||
|
if (p >= 1) {
|
||||||
o.progress = 99;
|
o.progress = 99;
|
||||||
} else {
|
} else {
|
||||||
o.progress = 100 * p;
|
o.progress = 100 * p;
|
||||||
@@ -170,7 +189,8 @@ const Home = function() {
|
|||||||
var state = {
|
var state = {
|
||||||
jobsQueued: [],
|
jobsQueued: [],
|
||||||
jobsRecent: [],
|
jobsRecent: [],
|
||||||
resultChanged: []
|
resultChanged: [],
|
||||||
|
lowPassRates: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
|
||||||
@@ -193,6 +213,7 @@ const Home = function() {
|
|||||||
state.jobsRunning = msg.running;
|
state.jobsRunning = msg.running;
|
||||||
state.jobsRecent = msg.recent;
|
state.jobsRecent = msg.recent;
|
||||||
state.resultChanged = msg.resultChanged;
|
state.resultChanged = msg.resultChanged;
|
||||||
|
state.lowPassRates = msg.lowPassRates;
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
// setup charts
|
// setup charts
|
||||||
@@ -235,6 +256,7 @@ const Home = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Builds per day' },
|
||||||
tooltips:{callbacks:{title: function(tip, data) {
|
tooltips:{callbacks:{title: function(tip, data) {
|
||||||
return buildsPerDayDates[tip[0].index].long;
|
return buildsPerDayDates[tip[0].index].long;
|
||||||
}}},
|
}}},
|
||||||
@@ -255,6 +277,7 @@ const Home = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Builds per job' },
|
||||||
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=>{
|
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=>{
|
||||||
if(Number.isInteger(label))
|
if(Number.isInteger(label))
|
||||||
return label;
|
return label;
|
||||||
@@ -273,6 +296,7 @@ const Home = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Mean run time this week' },
|
||||||
scales:{xAxes:[{
|
scales:{xAxes:[{
|
||||||
ticks:{userCallback: tpjScale.scale},
|
ticks:{userCallback: tpjScale.scale},
|
||||||
scaleLabel: {
|
scaleLabel: {
|
||||||
@@ -285,52 +309,6 @@ const Home = function() {
|
|||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var chtResultChanges = new Chart(document.getElementById("chartResultChanges"), {
|
|
||||||
type: 'horizontalBar',
|
|
||||||
data: {
|
|
||||||
labels: msg.resultChanged.map((e)=>{ return e.name; }),
|
|
||||||
datasets: [{
|
|
||||||
//label: '% Passed',
|
|
||||||
backgroundColor: msg.resultChanged.map((e)=>{return e.lastFailure > e.lastSuccess ? 'firebrick' : 'forestgreen';}),
|
|
||||||
data: msg.resultChanged.map((e)=>{ return e.lastSuccess - e.lastFailure; }),
|
|
||||||
itemid: msg.resultChanged.map((e)=> { return 'rcd_' + e.name; })
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options:{
|
|
||||||
scales:{
|
|
||||||
xAxes:[{ticks:{display: false}}],
|
|
||||||
yAxes:[{ticks:{display: false}}]
|
|
||||||
},
|
|
||||||
tooltips:{
|
|
||||||
enabled:false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var chtPassRates = new Chart(document.getElementById("chartPassRates"), {
|
|
||||||
type: 'horizontalBar',
|
|
||||||
data: {
|
|
||||||
labels: msg.lowPassRates.map((e)=>{ return e.name }),
|
|
||||||
datasets: [{
|
|
||||||
stack: 'passrate',
|
|
||||||
label: '% Passed',
|
|
||||||
backgroundColor: "forestgreen",
|
|
||||||
data: msg.lowPassRates.map((e)=>{ return e.passRate*100; })
|
|
||||||
},{
|
|
||||||
stack:'passrate',
|
|
||||||
label: '% Failed',
|
|
||||||
backgroundColor: "firebrick",
|
|
||||||
data: msg.lowPassRates.map((e)=>{ return (1-e.passRate)*100; })
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options:{
|
|
||||||
scales:{xAxes:[{ticks:{callback:(val,idx,values)=>{
|
|
||||||
return val + '%';
|
|
||||||
}}}]},
|
|
||||||
tooltips:{
|
|
||||||
enabled:false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var btcScale = timeScale(Math.max(msg.buildTimeChanges.map((e)=>{return Math.max(e.durations)})));
|
var btcScale = timeScale(Math.max(msg.buildTimeChanges.map((e)=>{return Math.max(e.durations)})));
|
||||||
var chtBuildTimeChanges = new Chart(document.getElementById("chartBuildTimeChanges"), {
|
var chtBuildTimeChanges = new Chart(document.getElementById("chartBuildTimeChanges"), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -344,6 +322,7 @@ const Home = function() {
|
|||||||
}})
|
}})
|
||||||
},
|
},
|
||||||
options:{
|
options:{
|
||||||
|
title: { display: true, text: 'Build time changes' },
|
||||||
legend:{display:true},
|
legend:{display:true},
|
||||||
scales:{
|
scales:{
|
||||||
xAxes:[{ticks:{display: false}}],
|
xAxes:[{ticks:{display: false}}],
|
||||||
@@ -369,6 +348,9 @@ const Home = function() {
|
|||||||
data: msg.buildTimeDist,
|
data: msg.buildTimeDist,
|
||||||
backgroundColor: "steelblue",
|
backgroundColor: "steelblue",
|
||||||
}]
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: { display: true, text: 'Build time distribution' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -415,8 +397,9 @@ const Jobs = function() {
|
|||||||
var state = {
|
var state = {
|
||||||
jobs: [],
|
jobs: [],
|
||||||
search: '',
|
search: '',
|
||||||
tags: [],
|
groups: {},
|
||||||
tag: null
|
group: null,
|
||||||
|
ungrouped: []
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
template: '#jobs',
|
template: '#jobs',
|
||||||
@@ -437,13 +420,10 @@ const Jobs = function() {
|
|||||||
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var tags = {};
|
state.groups = {};
|
||||||
for (var i in state.jobs) {
|
Object.keys(msg.groups).map(k => state.groups[k] = new RegExp(msg.groups[k]));
|
||||||
for (var j in state.jobs[i].tags) {
|
state.ungrouped = state.jobs.filter(j => !Object.values(state.groups).some(r => r.test(j.name))).map(j => j.name);
|
||||||
tags[state.jobs[i].tags[j]] = true;
|
state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0];
|
||||||
}
|
|
||||||
}
|
|
||||||
state.tags = Object.keys(tags);
|
|
||||||
},
|
},
|
||||||
job_started: function(data) {
|
job_started: function(data) {
|
||||||
var updAt = null;
|
var updAt = null;
|
||||||
@@ -470,6 +450,8 @@ const Jobs = function() {
|
|||||||
// first execution of new job. TODO insert without resort
|
// first execution of new job. TODO insert without resort
|
||||||
state.jobs.unshift(data);
|
state.jobs.unshift(data);
|
||||||
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
|
||||||
|
if(!Object.values(state.groups).some(r => r.test(data.name)))
|
||||||
|
state.ungrouped.push(data.name);
|
||||||
} else {
|
} else {
|
||||||
state.jobs[updAt] = data;
|
state.jobs[updAt] = data;
|
||||||
}
|
}
|
||||||
@@ -492,19 +474,13 @@ const Jobs = function() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
filteredJobs: function() {
|
filteredJobs: function() {
|
||||||
var ret = state.jobs;
|
let ret = [];
|
||||||
var tag = state.tag;
|
if (state.group)
|
||||||
if (tag) {
|
ret = state.jobs.filter(job => state.groups[state.group].test(job.name));
|
||||||
ret = ret.filter(function(job) {
|
else
|
||||||
return job.tags.indexOf(tag) >= 0;
|
ret = state.jobs.filter(job => state.ungrouped.includes(job.name));
|
||||||
});
|
if (this.search)
|
||||||
}
|
ret = ret.filter(job => job.name.indexOf(this.search) > -1);
|
||||||
var search = this.search;
|
|
||||||
if (search) {
|
|
||||||
ret = ret.filter(function(job) {
|
|
||||||
return job.name.indexOf(search) > -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -513,6 +489,7 @@ const Jobs = function() {
|
|||||||
|
|
||||||
var Job = function() {
|
var Job = function() {
|
||||||
var state = {
|
var state = {
|
||||||
|
description: '',
|
||||||
jobsRunning: [],
|
jobsRunning: [],
|
||||||
jobsRecent: [],
|
jobsRecent: [],
|
||||||
lastSuccess: null,
|
lastSuccess: null,
|
||||||
@@ -530,6 +507,7 @@ var Job = function() {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
status: function(msg) {
|
status: function(msg) {
|
||||||
|
state.description = msg.description;
|
||||||
state.jobsRunning = msg.running;
|
state.jobsRunning = msg.running;
|
||||||
state.jobsRecent = msg.recent;
|
state.jobsRecent = msg.recent;
|
||||||
state.lastSuccess = msg.lastSuccess;
|
state.lastSuccess = msg.lastSuccess;
|
||||||
@@ -569,6 +547,7 @@ var Job = function() {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
title: { display: true, text: 'Build time' },
|
||||||
scales:{
|
scales:{
|
||||||
xAxes:[{},{
|
xAxes:[{},{
|
||||||
id: 'avg',
|
id: 'avg',
|
||||||
@@ -647,9 +626,7 @@ const Run = function() {
|
|||||||
job: { artifacts: [], upstream: {} },
|
job: { artifacts: [], upstream: {} },
|
||||||
latestNum: null,
|
latestNum: null,
|
||||||
log: '',
|
log: '',
|
||||||
autoscroll: false
|
|
||||||
};
|
};
|
||||||
var firstLog = false;
|
|
||||||
const logFetcher = (vm, name, num) => {
|
const logFetcher = (vm, name, num) => {
|
||||||
const abort = new AbortController();
|
const abort = new AbortController();
|
||||||
fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
||||||
@@ -664,11 +641,6 @@ const Run = function() {
|
|||||||
return;
|
return;
|
||||||
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
||||||
vm.$forceUpdate();
|
vm.$forceUpdate();
|
||||||
if (!firstLog) {
|
|
||||||
firstLog = true;
|
|
||||||
} else if (state.autoscroll) {
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
}
|
|
||||||
return pump();
|
return pump();
|
||||||
});
|
});
|
||||||
}();
|
}();
|
||||||
@@ -755,6 +727,7 @@ new Vue({
|
|||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
title: '', // populated by status ws message
|
title: '', // populated by status ws message
|
||||||
|
version: '',
|
||||||
clockSkew: 0,
|
clockSkew: 0,
|
||||||
connected: false,
|
connected: false,
|
||||||
notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1
|
notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1
|
||||||
@@ -776,7 +749,8 @@ new Vue({
|
|||||||
new Notification('Job ' + data.result, {
|
new Notification('Job ' + data.result, {
|
||||||
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
runIcon: Utils.methods.runIcon
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
notify(e) { localStorage.setItem('showNotifications', e ? 1 : 0); }
|
notify(e) { localStorage.setItem('showNotifications', e ? 1 : 0); }
|
||||||
|
|||||||
21
src/resources/manifest.webmanifest
Normal file
21
src/resources/manifest.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Laminar",
|
||||||
|
"name": "Laminar",
|
||||||
|
"description": "Lightweight Continuous Integration",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "36x36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon-152.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "152x152"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"background_color": "#2F3340",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": "/"
|
||||||
|
}
|
||||||
242
src/resources/style.css
Normal file
242
src/resources/style.css
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/* colour scheme */
|
||||||
|
:root {
|
||||||
|
--main-bg: #fff;
|
||||||
|
--main-fg: #333;
|
||||||
|
--nav-bg: #2F3340;
|
||||||
|
--nav-bg-darker: #292b33;
|
||||||
|
--nav-fg: #d0d0d0;
|
||||||
|
--nav-fg-light: #fafafa;
|
||||||
|
--icon-enabled: #d8cb83;
|
||||||
|
--success: #74af77;
|
||||||
|
--failure: #883d3d;
|
||||||
|
--running: #4786ab;
|
||||||
|
--warning: #de9a34;
|
||||||
|
--link-fg: #2f4579;
|
||||||
|
--console-bg: #313235;
|
||||||
|
--console-fg: #fff;
|
||||||
|
--alt-row-bg: #fafafa;
|
||||||
|
--border-grey: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* basic resets */
|
||||||
|
html { box-sizing: border-box; }
|
||||||
|
*, *:before, *:after { box-sizing: inherit; }
|
||||||
|
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
ol, ul { list-style: none; }
|
||||||
|
body, html { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--main-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* main header bar */
|
||||||
|
#nav-top { background-color: var(--nav-bg); }
|
||||||
|
#nav-top-links { background-color: var(--nav-bg-darker); }
|
||||||
|
#nav-top a { color: var(--nav-fg); }
|
||||||
|
#nav-top a:hover { color: white; text-decoration: none; }
|
||||||
|
|
||||||
|
.version {
|
||||||
|
align-self: center;
|
||||||
|
font-size: x-small;
|
||||||
|
color: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* navbar svg icons (enable notifications) */
|
||||||
|
.nav-icon { display: inherit; }
|
||||||
|
.nav-icon svg { fill: var(--nav-fg); stroke: #000; }
|
||||||
|
.nav-icon:hover { cursor: pointer; }
|
||||||
|
.nav-icon:hover svg { fill: var(--nav-fg-light); }
|
||||||
|
.nav-icon.active svg { fill: var(--icon-enabled); }
|
||||||
|
|
||||||
|
/* anchors */
|
||||||
|
a { color: var(--link-fg); text-decoration: none; }
|
||||||
|
a:visited { color: var(--link-fg); }
|
||||||
|
a:active { color: var(--link-fg); }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* charts */
|
||||||
|
canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 800px;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popup-connecting {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* status icons */
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -2px; /* pixel-pushing */
|
||||||
|
}
|
||||||
|
svg.success path { fill: var(--success); }
|
||||||
|
svg.failed path { fill: var(--failure); }
|
||||||
|
svg.running circle { stroke: var(--running); }
|
||||||
|
|
||||||
|
/* sort indicators */
|
||||||
|
a.sort {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
a.sort:before, a.sort:after {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
a.sort:before {
|
||||||
|
border-bottom-color: var(--border-grey);
|
||||||
|
margin-top: -9px;
|
||||||
|
}
|
||||||
|
a.sort:after {
|
||||||
|
border-top-color: var(--border-grey);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
a.sort.dsc:after { border-top-color: var(--main-fg); }
|
||||||
|
a.sort.asc:before { border-bottom-color: var(--main-fg); }
|
||||||
|
a.sort:hover { text-decoration: none; cursor:pointer; }
|
||||||
|
a.sort:not(.asc):hover:before { border-bottom-color: var(--main-fg); }
|
||||||
|
a.sort:not(.dsc):hover:after { border-top-color: var(--main-fg); }
|
||||||
|
|
||||||
|
/* job group tabs */
|
||||||
|
a.active { color: var(--main-fg); }
|
||||||
|
a.active:hover { text-decoration: none; }
|
||||||
|
|
||||||
|
/* run console output */
|
||||||
|
.console-log { padding: 15px; background-color: var(--console-bg); }
|
||||||
|
.console-log code { white-space: pre-wrap; color: var(--console-fg); }
|
||||||
|
.console-log a { color: var(--console-fg); }
|
||||||
|
|
||||||
|
/* text input (job filtering) */
|
||||||
|
input { padding: 5px 8px; }
|
||||||
|
|
||||||
|
/* description list (run detail) */
|
||||||
|
dl { display: grid; grid-template-columns: auto 1fr; }
|
||||||
|
dt { text-align: right; font-weight: bold; min-width: 85px; }
|
||||||
|
dt,dd { line-height: 2; }
|
||||||
|
|
||||||
|
/* tables */
|
||||||
|
table { border-spacing: 0; width: 100%; }
|
||||||
|
th { text-align: left; border-bottom: 1px solid var(--border-grey); }
|
||||||
|
td, th { padding: 8px; }
|
||||||
|
table.striped td { border-top: 1px solid var(--border-grey); }
|
||||||
|
table.striped tr:nth-child(even) { background-color: var(--alt-row-bg); }
|
||||||
|
td:first-child, th:first-child { padding-left: 15px; }
|
||||||
|
td:last-child, th:last-child { padding-right: 15px; }
|
||||||
|
|
||||||
|
/* next/prev navigation buttons */
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--border-grey);
|
||||||
|
background-color: var(--alt-row-bg);
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 29px;
|
||||||
|
}
|
||||||
|
button[disabled] { cursor: not-allowed; color: var(--border-grey); }
|
||||||
|
button:not([disabled]) { cursor: pointer; color: var(--main-fg); }
|
||||||
|
|
||||||
|
/* progress bar */
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: var(--border-grey);
|
||||||
|
background-color: var(--alt-row-bg);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--running);
|
||||||
|
background-image: linear-gradient(45deg, transparent 35%, rgba(255,255,255,0.18) 35% 65%, transparent 65%);
|
||||||
|
background-size: 1rem;
|
||||||
|
transition: width .6s linear;
|
||||||
|
}
|
||||||
|
.progress-bar.overtime { background-color: var(--warning); }
|
||||||
|
.progress-bar.indeterminate {
|
||||||
|
animation: animate-stripes 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes animate-stripes {
|
||||||
|
from { background-position: 1rem 0; } to { background-position: 0 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* connecting overlay */
|
||||||
|
#connecting-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: end; justify-content: end;
|
||||||
|
color: var(--nav-fg-light);
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 30px;
|
||||||
|
visibility: hidden;
|
||||||
|
background-color: rgba(0,0,0,0.75);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease, visibility 0s 0.5s;
|
||||||
|
}
|
||||||
|
#connecting-overlay.shown {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s ease 2s;
|
||||||
|
}
|
||||||
|
#connecting-overlay > div { opacity: 1; }
|
||||||
|
|
||||||
|
/* responsive layout */
|
||||||
|
#page-home-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
@media (max-width: 865px) {
|
||||||
|
#page-home-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.vp-sm-hide { display: none; }
|
||||||
|
}
|
||||||
|
#page-home-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
padding: 15px;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
#page-home-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#page-job-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template: auto 1fr / minmax(550px, 1fr) 1fr;
|
||||||
|
}
|
||||||
|
@media (max-width: 965px) {
|
||||||
|
#page-job-main {
|
||||||
|
grid-template: auto auto 1fr / 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#page-run-detail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(400px, auto) 1fr;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
@media (max-width: 780px) {
|
||||||
|
#page-run-detail {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
|||||||
249
src/run.cpp
249
src/run.cpp
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2018 Oliver Giles
|
/// Copyright 2015-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -17,7 +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 "run.h"
|
#include "run.h"
|
||||||
#include "node.h"
|
#include "context.h"
|
||||||
#include "conf.h"
|
#include "conf.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
@@ -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,119 +77,53 @@ Run::~Run() {
|
|||||||
LLOG(INFO, "Run destroyed");
|
LLOG(INFO, "Run destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Run::configure(uint buildNum, std::shared_ptr<Node> 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());
|
|
||||||
// per-node before-run script
|
|
||||||
if(fsHome.exists(cfgDir/"nodes"/(nd->name+".before")))
|
|
||||||
addScript(cfgDir/"nodes"/(nd->name+".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);
|
|
||||||
// per-node after-run script
|
|
||||||
if(fsHome.exists(cfgDir/"nodes"/(nd->name+".after")))
|
|
||||||
addScript(cfgDir/"nodes"/(nd->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/"nodes"/(nd->name+".env")))
|
|
||||||
addEnv(cfgDir/"nodes"/(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;
|
|
||||||
node = 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]);
|
||||||
|
|
||||||
std::string Run::reason() const {
|
// All initial/fixed env vars can be set here. Dynamic ones, including
|
||||||
return reasonMsg;
|
// "RESULT" and any set by `laminarc set` have to be handled in the subprocess.
|
||||||
}
|
|
||||||
|
|
||||||
bool Run::step() {
|
// add environment files
|
||||||
if(!scripts.size())
|
if(fsHome.exists(cfgDir/"env"))
|
||||||
return true;
|
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"));
|
||||||
|
|
||||||
Script currentScript = kj::mv(scripts.front());
|
// parameterized vars
|
||||||
scripts.pop();
|
for(auto& pair : params) {
|
||||||
|
setenv(pair.first.c_str(), pair.second.c_str(), false);
|
||||||
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")) {
|
||||||
@@ -195,73 +131,62 @@ bool Run::step() {
|
|||||||
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);
|
||||||
if(!node->name.empty())
|
setenv("CONTEXT", ctx->name.c_str(), true);
|
||||||
setenv("NODE", node->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;
|
||||||
|
if(asprintf(&procName, "{laminar} %s:%d", name.data(), buildNum) > 0)
|
||||||
|
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));
|
|
||||||
}
|
|
||||||
|
|||||||
33
src/run.h
33
src/run.h
@@ -43,7 +43,7 @@ enum class RunState {
|
|||||||
|
|
||||||
std::string to_string(const RunState& rs);
|
std::string to_string(const RunState& rs);
|
||||||
|
|
||||||
class Node;
|
class Context;
|
||||||
|
|
||||||
typedef std::unordered_map<std::string, std::string> ParamMap;
|
typedef std::unordered_map<std::string, std::string> ParamMap;
|
||||||
|
|
||||||
@@ -57,26 +57,17 @@ 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 node
|
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<Node> node, 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<Node> node;
|
std::shared_ptr<Context> context;
|
||||||
RunState result;
|
RunState result;
|
||||||
RunState lastResult;
|
RunState lastResult;
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <capnp/message.h>
|
#include <capnp/message.h>
|
||||||
#include <capnp/capability.h>
|
#include <capnp/capability.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
struct Laminar;
|
struct Laminar;
|
||||||
struct Http;
|
struct Http;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015 Oliver Giles
|
/// Copyright 2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -16,28 +16,10 @@
|
|||||||
/// You should have received a copy of the GNU General Public License
|
/// You should have received a copy of the GNU General Public License
|
||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#ifndef LAMINAR_NODE_H_
|
|
||||||
#define LAMINAR_NODE_H_
|
|
||||||
|
|
||||||
#include <string>
|
#define str(x) #x
|
||||||
#include <set>
|
#define xstr(x) str(x)
|
||||||
|
|
||||||
class Run;
|
const char* laminar_version() {
|
||||||
|
return xstr(LAMINAR_VERSION);
|
||||||
// Represents a group of executors. Currently almost unnecessary POD
|
}
|
||||||
// abstraction, but may be enhanced in the future to support e.g. tags
|
|
||||||
class Node {
|
|
||||||
public:
|
|
||||||
Node() {}
|
|
||||||
|
|
||||||
std::string name;
|
|
||||||
int numExecutors;
|
|
||||||
int busyExecutors = 0;
|
|
||||||
std::set<std::string> tags;
|
|
||||||
|
|
||||||
// Attempts to queue the given run to this node. Returns true if succeeded.
|
|
||||||
bool queue(const Run& run);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif // LAMINAR_NODE_H_
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2019 Oliver Giles
|
/// Copyright 2019-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -19,13 +19,16 @@
|
|||||||
#ifndef LAMINAR_FIXTURE_H_
|
#ifndef LAMINAR_FIXTURE_H_
|
||||||
#define LAMINAR_FIXTURE_H_
|
#define LAMINAR_FIXTURE_H_
|
||||||
|
|
||||||
#include <capnp/rpc-twoparty.h>
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
#include "laminar.capnp.h"
|
#include "laminar.capnp.h"
|
||||||
#include "eventsource.h"
|
#include "eventsource.h"
|
||||||
#include "tempdir.h"
|
#include "tempdir.h"
|
||||||
#include "laminar.h"
|
#include "laminar.h"
|
||||||
|
#include "log.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
#include "conf.h"
|
||||||
|
|
||||||
|
#include <capnp/rpc-twoparty.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
class LaminarFixture : public ::testing::Test {
|
class LaminarFixture : public ::testing::Test {
|
||||||
public:
|
public:
|
||||||
@@ -38,7 +41,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,19 +50,76 @@ 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, const char* configContent = nullptr) {
|
||||||
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", std::string(name) + ".run"},
|
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", std::string(name) + ".run"},
|
||||||
kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT | kj::WriteMode::EXECUTABLE)) {
|
kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT | kj::WriteMode::EXECUTABLE)) {
|
||||||
(*f)->writeAll(std::string("#!/bin/sh\n") + scriptContent + "\n");
|
(*f)->writeAll(std::string("#!/bin/sh\n") + scriptContent + "\n");
|
||||||
}
|
}
|
||||||
|
if(configContent) {
|
||||||
|
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "jobs", std::string(name) + ".conf"}, kj::WriteMode::CREATE)) {
|
||||||
|
(*f)->writeAll(configContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
LSYSCALL(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 +136,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,74 @@ 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LaminarFixture, JobDescription) {
|
||||||
|
defineJob("foo", "true", "DESCRIPTION=bar");
|
||||||
|
auto es = eventSource("/jobs/foo");
|
||||||
|
ioContext->waitScope.poll();
|
||||||
|
ASSERT_EQ(1, es->messages().size());
|
||||||
|
auto json = es->messages().front().GetObject();
|
||||||
|
ASSERT_TRUE(json.HasMember("data"));
|
||||||
|
auto data = json["data"].GetObject();
|
||||||
|
ASSERT_TRUE(data.HasMember("description"));
|
||||||
|
EXPECT_STREQ("bar", data["description"].GetString());
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2018 Oliver Giles
|
/// Copyright 2018-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
#ifndef LAMINAR_TEMPDIR_H_
|
#ifndef LAMINAR_TEMPDIR_H_
|
||||||
#define LAMINAR_TEMPDIR_H_
|
#define LAMINAR_TEMPDIR_H_
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
#include <kj/filesystem.h>
|
#include <kj/filesystem.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
static kj::Path mkdtemp() {
|
static kj::Path mkdtemp() {
|
||||||
char dir[] = "/tmp/laminar-test-XXXXXX";
|
char dir[] = "/tmp/laminar-test-XXXXXX";
|
||||||
::mkdtemp(dir);
|
LASSERT(::mkdtemp(dir) != nullptr, "mkdtemp failed");
|
||||||
return kj::Path::parse(&dir[1]);
|
return kj::Path::parse(&dir[1]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2018 Oliver Giles
|
/// Copyright 2018-2020 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@@ -16,8 +16,9 @@
|
|||||||
/// You should have received a copy of the GNU General Public License
|
/// You should have received a copy of the GNU General Public License
|
||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#include <gtest/gtest.h>
|
|
||||||
#include "conf.h"
|
#include "conf.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
class ConfTest : public ::testing::Test {
|
class ConfTest : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
@@ -30,7 +31,7 @@ protected:
|
|||||||
}
|
}
|
||||||
void parseConf(std::string conf) {
|
void parseConf(std::string conf) {
|
||||||
lseek(fd, SEEK_SET, 0);
|
lseek(fd, SEEK_SET, 0);
|
||||||
write(fd, conf.data(), conf.size());
|
LSYSCALL(write(fd, conf.data(), conf.size()));
|
||||||
cfg = parseConfFile(tmpFile);
|
cfg = parseConfFile(tmpFile);
|
||||||
}
|
}
|
||||||
StringMap cfg;
|
StringMap cfg;
|
||||||
|
|||||||
@@ -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 "node.h"
|
|
||||||
#include "conf.h"
|
|
||||||
#include "tempdir.h"
|
|
||||||
|
|
||||||
class RunTest : public testing::Test {
|
|
||||||
protected:
|
|
||||||
RunTest() :
|
|
||||||
testing::Test(),
|
|
||||||
node(std::make_shared<Node>()),
|
|
||||||
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<Node> node;
|
|
||||||
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, node, *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, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
EXPECT_EQ(RunState::SUCCESS, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, FailedStatus) {
|
|
||||||
setRunLink("false");
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
EXPECT_EQ(RunState::FAILED, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, Environment) {
|
|
||||||
setRunLink("env");
|
|
||||||
run.configure(1234, node, *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, node, *tmp.fs);
|
|
||||||
runAll();
|
|
||||||
StringMap map = parseFromString(readAllOutput());
|
|
||||||
EXPECT_EQ("bar", map["foo"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RunTest, Abort) {
|
|
||||||
setRunLink("yes");
|
|
||||||
run.configure(1, node, *tmp.fs);
|
|
||||||
run.step();
|
|
||||||
usleep(200); // TODO fix
|
|
||||||
run.abort(false);
|
|
||||||
wait();
|
|
||||||
EXPECT_EQ(RunState::ABORTED, run.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user