mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cfa995f8b9 | ||
|
0a340f9b0b | ||
|
d259dff604 | ||
|
44aea1fc06 | ||
|
27d2a760fd | ||
|
fc6343bd19 | ||
|
736c95ff57 | ||
|
5ea394c610 | ||
|
8c3d7f62a9 | ||
|
a1a95c8e7f | ||
|
277a59f1cb | ||
|
97b9f6b1ae | ||
|
d2c58f0bcd | ||
|
dab620b01e | ||
|
1e7e9319c3 | ||
|
af4b51b3e9 | ||
|
458ec26943 | ||
|
6a20291dc4 | ||
|
3cc01bc45d | ||
|
e25b58944d | ||
|
1be755e323 | ||
|
e9fc547a72 | ||
|
01183a3c25 | ||
|
e7defa9f15 | ||
|
99e2e62906 | ||
|
261c08d2fe | ||
|
48c0e9340e | ||
|
7303e4d592 | ||
|
23cb30fc0c | ||
|
7eb19ce8c4 | ||
|
7c4e1108ae | ||
|
5607a93cc1 | ||
|
41ddd8fe4f | ||
|
e581a0cf5d | ||
|
4a6f99a203 | ||
|
efafda16ff | ||
|
bb087b72ee | ||
|
37bbf6ade4 | ||
|
e1686d454b | ||
|
549f49052a | ||
|
d913d04c4a | ||
|
78ceeec3e8 |
@ -1,5 +1,5 @@
|
||||
###
|
||||
### Copyright 2015-2021 Oliver Giles
|
||||
### Copyright 2015-2024 Oliver Giles
|
||||
###
|
||||
### This file is part of Laminar
|
||||
###
|
||||
@ -16,8 +16,45 @@
|
||||
### You should have received a copy of the GNU General Public License
|
||||
### along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||
###
|
||||
project(laminar)
|
||||
cmake_minimum_required(VERSION 3.6)
|
||||
project(laminar)
|
||||
|
||||
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
||||
# ld.lld is a default option on FreeBSD
|
||||
set(LLVM_LINKER_IS_LLD ON)
|
||||
endif()
|
||||
|
||||
# ld.lld specific options. There is no sane way in cmake
|
||||
# to detect if toolchain is actually using ld.lld
|
||||
if (LLVM_LINKER_IS_LLD)
|
||||
if (NOT DEFINED LINKER_EMULATION_FLAGS)
|
||||
if (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "amd64")
|
||||
set(LINKER_EMULATION_FLAGS "-melf_x86_64")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "x86_64")
|
||||
set(LINKER_EMULATION_FLAGS "-melf_x86_64")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "aarch64")
|
||||
set(LINKER_EMULATION_FLAGS "-maarch64elf")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "powerpc64le")
|
||||
set(LINKER_EMULATION_FLAGS "-melf64lppc")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "powerpc64")
|
||||
set(LINKER_EMULATION_FLAGS "-melf64ppc")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "riscv64")
|
||||
# llvm17 & riscv64 requires extra step, it is necessary to
|
||||
# patch 'Elf64.e_flags' (48-th byte) in binary-blob object files
|
||||
# with value 0x5 - to change soft_float ABI to hard_float ABI
|
||||
# so they can link with rest of the object files.
|
||||
set(LINKER_EMULATION_FLAGS "-melf64lriscv")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm")
|
||||
set(LINKER_EMULATION_FLAGS "-marmelf")
|
||||
elseif (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "armv7")
|
||||
set(LINKER_EMULATION_FLAGS "-marmelf")
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"Unsupported '${CMAKE_SYSTEM_PROCESSOR}' translation to emulation flag. "
|
||||
"Please set it explicitly 'cmake -DLINKER_EMULATION_FLAGS=\"-melf_your_arch\" ...'")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
@ -56,7 +93,7 @@ macro(generate_compressed_bins BASEDIR)
|
||||
DEPENDS ${BASEDIR}/${FILE}
|
||||
)
|
||||
add_custom_command(OUTPUT ${OUTPUT_FILE}
|
||||
COMMAND ${CMAKE_LINKER} -r -b binary -o ${OUTPUT_FILE} ${COMPRESSED_FILE}
|
||||
COMMAND ${CMAKE_LINKER} ${LINKER_EMULATION_FLAGS} -r -b binary -o ${OUTPUT_FILE} ${COMPRESSED_FILE}
|
||||
COMMAND ${CMAKE_OBJCOPY}
|
||||
--rename-section .data=.rodata.alloc,load,readonly,data,contents
|
||||
--add-section .note.GNU-stack=/dev/null
|
||||
@ -84,11 +121,11 @@ add_custom_command(OUTPUT index_html_size.h
|
||||
|
||||
# Download 3rd-party frontend JS libs...
|
||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js
|
||||
js/vue.min.js EXPECTED_MD5 fb192338844efe86ec759a40152fcb8e)
|
||||
${CMAKE_BINARY_DIR}/js/vue.min.js EXPECTED_MD5 fb192338844efe86ec759a40152fcb8e)
|
||||
file(DOWNLOAD https://raw.githubusercontent.com/drudru/ansi_up/v4.0.4/ansi_up.js
|
||||
js/ansi_up.js EXPECTED_MD5 b31968e1a8fed0fa82305e978161f7f5)
|
||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js
|
||||
js/Chart.min.js EXPECTED_MD5 f6c8efa65711e0cbbc99ba72997ecd0e)
|
||||
${CMAKE_BINARY_DIR}/js/ansi_up.js EXPECTED_MD5 b31968e1a8fed0fa82305e978161f7f5)
|
||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js
|
||||
${CMAKE_BINARY_DIR}/js/Chart.min.js EXPECTED_MD5 7dd5ea7d2cf22a1c42b43c40093d2669)
|
||||
# ...and compile them
|
||||
generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue.min.js
|
||||
js/ansi_up.js js/Chart.min.js)
|
||||
@ -109,13 +146,31 @@ set(LAMINARD_CORE_SOURCES
|
||||
index_html_size.h
|
||||
)
|
||||
|
||||
find_package(CapnProto REQUIRED)
|
||||
include_directories(${CAPNP_INCLUDE_DIRS})
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
include_directories(${SQLite3_INCLUDE_DIRS})
|
||||
|
||||
find_package(ZLIB REQUIRED)
|
||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
include_directories(${Threads_INCLUDE_DIRS})
|
||||
|
||||
## Server
|
||||
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 CapnProto::capnp-rpc CapnProto::capnp CapnProto::kj-http CapnProto::kj-async
|
||||
CapnProto::kj Threads::Threads SQLite::SQLite3 ZLIB::ZLIB)
|
||||
|
||||
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
||||
pkg_check_modules(INOTIFY REQUIRED libinotify)
|
||||
target_link_libraries(laminard ${INOTIFY_LINK_LIBRARIES})
|
||||
endif()
|
||||
|
||||
## Client
|
||||
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 CapnProto::capnp-rpc CapnProto::capnp CapnProto::kj-async CapnProto::kj Threads::Threads)
|
||||
|
||||
## Manpages
|
||||
macro(gzip SOURCE)
|
||||
@ -139,7 +194,6 @@ if(BUILD_TESTS)
|
||||
target_link_libraries(laminar-tests ${GTEST_LIBRARIES} capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
||||
endif()
|
||||
|
||||
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(ZSH_COMPLETIONS_DIR /usr/share/zsh/site-functions CACHE PATH "Path to zsh completions directory")
|
||||
install(TARGETS laminard RUNTIME DESTINATION sbin)
|
||||
@ -148,5 +202,8 @@ install(FILES etc/laminar.conf DESTINATION /etc)
|
||||
install(FILES etc/laminarc-completion.bash DESTINATION ${BASH_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})
|
||||
if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(SYSTEMD_UNITDIR /lib/systemd/system CACHE PATH "Path to systemd unit files")
|
||||
configure_file(etc/laminar.service.in laminar.service @ONLY)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/laminar.service DESTINATION ${SYSTEMD_UNITDIR})
|
||||
endif()
|
||||
|
@ -12,11 +12,11 @@ See [the website](https://laminar.ohwg.net) and the [documentation](https://lami
|
||||
|
||||
First install development packages for `capnproto (version 0.7.0 or newer)`, `rapidjson`, `sqlite` and `boost` (for the header-only `multi_index_container` library) from your distribution's repository or other source.
|
||||
|
||||
On Debian Bullseye, this can be done with:
|
||||
On Debian Bookworm, this can be done with:
|
||||
|
||||
```bash
|
||||
sudo apt install \
|
||||
capnproto cmake g++ libboost-dev libcapnp-dev libsqlite3-dev rapidjson-dev zlib1g-dev
|
||||
sudo apt install capnproto cmake g++ libboost-dev libcapnp-dev libsqlite3-dev \
|
||||
make rapidjson-dev zlib1g-dev pkg-config
|
||||
```
|
||||
|
||||
Then compile and install laminar with:
|
||||
|
@ -19,7 +19,7 @@ Throughout this document, the fixed base path `/var/lib/laminar` is used. This i
|
||||
|
||||
Since Debian Bullseye, Laminar is available in [the official repositories](https://packages.debian.org/search?searchon=sourcenames&keywords=laminar).
|
||||
|
||||
Alternatively, pre-built upstream packages are available for Debian 10 (Bullseye) on x86_64 and armhf, and for Rocky/CentOS/RHEL 7 and 8 on x86_64.
|
||||
Alternatively, pre-built upstream packages are available for Debian 10 (Buster) on x86_64 and armhf, and for Rocky/CentOS/RHEL 7 and 8 on x86_64.
|
||||
|
||||
Finally, Laminar may be built from source for any Linux distribution.
|
||||
|
||||
@ -222,13 +222,13 @@ Then, point `laminarc` to the new location using an environment variable:
|
||||
LAMINAR_HOST=192.168.1.1:9997 laminarc queue example
|
||||
```
|
||||
|
||||
If you need more flexibility, consider running the communication channel as a regular unix socket and applying user and group permissions to the file. To achieve this, set
|
||||
If you need more flexibility, consider running the communication channel as a regular unix socket. Setting
|
||||
|
||||
```
|
||||
LAMINAR_BIND_RPC=unix:/var/run/laminar.sock
|
||||
```
|
||||
|
||||
or similar path in `/etc/laminar.conf`.
|
||||
or similar path in `/etc/laminar.conf` will result in a socket with group read/write permissions (`660`), so any user in the `laminar` group can queue a job.
|
||||
|
||||
This can be securely and flexibly combined with remote triggering using `ssh`. There is no need to allow the client full shell access to the server machine, the ssh server can restrict certain users to certain commands (in this case `laminarc`). See [the authorized_keys section of the sshd man page](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) for further information.
|
||||
|
||||
@ -534,7 +534,7 @@ If `CONTEXTS` is empty or absent (or if `JOB.conf` doesn't exist), laminar will
|
||||
|
||||
## Adding environment to a context
|
||||
|
||||
Append desired environment variables to `/var/lib/laminar/cfg/contexts/CONTEXT_NAME.conf`:
|
||||
Append desired environment variables to `/var/lib/laminar/cfg/contexts/CONTEXT_NAME.env`:
|
||||
|
||||
```
|
||||
DUT_IP=192.168.3.2
|
||||
@ -713,6 +713,7 @@ Finally, variables supplied on the command-line call to `laminarc queue`, `lamin
|
||||
- `queue [JOB [PARAMS...]]...` adds one or more jobs to the queue with optional parameters, returning immediately.
|
||||
- `start [JOB [PARAMS...]]...` starts one or more jobs with optional parameters, returning when the jobs begin execution.
|
||||
- `run [JOB [PARAMS...]]...` triggers one or more jobs with optional parameters and waits for the completion of all jobs.
|
||||
- `--next` may be passed before `JOB` in order to place the job at the front of the queue instead of at the end.
|
||||
- `set [VARIABLE=VALUE]...` sets one or more variables to be exported in subsequent scripts for the run identified by the `$JOB` and `$RUN` environment variables
|
||||
- `show-jobs` shows the known jobs on the server (`$LAMINAR_HOME/cfg/jobs/*.run`).
|
||||
- `show-running` shows the currently running jobs with their numbers.
|
||||
|
@ -7,7 +7,7 @@ Documentation=https://laminar.ohwg.net/docs.html
|
||||
[Service]
|
||||
User=laminar
|
||||
EnvironmentFile=-/etc/laminar.conf
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/sbin/laminard
|
||||
ExecStart=@CMAKE_INSTALL_PREFIX@/sbin/laminard -v
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@ -6,8 +6,8 @@
|
||||
Laminar CI client application
|
||||
.Sh SYNOPSIS
|
||||
.Nm laminarc Li queue \fIJOB\fR [\fIPARAM=VALUE...\fR] ...
|
||||
.Nm laminarc Li queue \fIJOB\fR [\fIPARAM=VALUE...\fR] ...
|
||||
.Nm laminarc Li queue \fIJOB\fR [\fIPARAM=VALUE...\fR] ...
|
||||
.Nm laminarc Li start \fIJOB\fR [\fIPARAM=VALUE...\fR] ...
|
||||
.Nm laminarc Li run \fIJOB\fR [\fIPARAM=VALUE...\fR] ...
|
||||
.Nm laminarc Li set \fIPARAM=VALUE...\fR
|
||||
.Nm laminarc Li show-jobs
|
||||
.Nm laminarc Li show-running
|
||||
@ -27,6 +27,9 @@ begin execution.
|
||||
adds job(s) (with optional parameters) to the queue and returns when the jobs
|
||||
complete execution. The exit code will be non-zero if any of the runs does
|
||||
not complete successfully.
|
||||
.It \t
|
||||
\fB--next\fR may be passed to \fBqueue\fR, \fBstart\fR or \fBrun\fR in order
|
||||
to place the job at the front of the queue instead of at the end.
|
||||
.It Sy set
|
||||
sets one or more parameters to be exported as environment variables in subsequent
|
||||
scripts for the run identified by the $JOB and $RUN environment variables.
|
||||
|
@ -9,7 +9,7 @@ set -x
|
||||
|
||||
# Simple way of getting the docker build tag:
|
||||
tag=$(docker build -q - <<\EOF
|
||||
FROM debian:bullseye
|
||||
FROM debian:bookworm
|
||||
RUN apt-get update && apt-get install -y build-essential
|
||||
EOF
|
||||
)
|
||||
@ -19,7 +19,7 @@ EOF
|
||||
|
||||
exec {pfd}<><(:) # get a new pipe
|
||||
docker build - <<\EOF |
|
||||
FROM debian:bullseye
|
||||
FROM debian:bookworm
|
||||
RUN apt-get update && apt-get install -y build-essential
|
||||
EOF
|
||||
tee >(awk '/Successfully built/{print $3}' >&$pfd) # parse output to pipe
|
||||
|
@ -38,7 +38,7 @@ server {
|
||||
# fine-grained control of permissions.
|
||||
# see http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
|
||||
# and https://laminar.ohwg.net/docs.html#Running-on-a-different-HTTP-port-or-Unix-socket
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
|
||||
# required to allow laminar's SSE stream to pass correctly
|
||||
proxy_http_version 1.1;
|
||||
|
@ -4,10 +4,10 @@ OUTPUT_DIR=$PWD
|
||||
|
||||
SOURCE_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]})/..)
|
||||
|
||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian10
|
||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian11
|
||||
|
||||
DOCKER_TAG=$(docker build -q - <<EOS
|
||||
FROM debian:10-slim
|
||||
FROM debian:11-slim
|
||||
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev
|
||||
EOS
|
||||
)
|
@ -4,10 +4,10 @@ OUTPUT_DIR=$PWD
|
||||
|
||||
SOURCE_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]})/..)
|
||||
|
||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian10
|
||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian11
|
||||
|
||||
DOCKER_TAG=$(docker build -q - <<EOS
|
||||
FROM debian:10-slim
|
||||
FROM debian:11-slim
|
||||
RUN dpkg --add-architecture armhf && apt-get update && apt-get install -y wget cmake crossbuild-essential-armhf capnproto libcapnp-dev:armhf rapidjson-dev libsqlite3-dev:armhf libboost-dev:armhf zlib1g-dev:armhf
|
||||
EOS
|
||||
)
|
50
pkg/debian12-amd64.sh
Executable file
50
pkg/debian12-amd64.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -ex
|
||||
|
||||
OUTPUT_DIR=$PWD
|
||||
|
||||
SOURCE_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]})/..)
|
||||
|
||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian12
|
||||
|
||||
DOCKER_TAG=$(docker build -q - <<EOS
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev pkg-config
|
||||
EOS
|
||||
)
|
||||
|
||||
docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG bash -xe <<EOS
|
||||
|
||||
mkdir /build
|
||||
cd /build
|
||||
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -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: libcapnp-1.0.1, 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,contexts,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
|
50
pkg/debian13-amd64.sh
Executable file
50
pkg/debian13-amd64.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -ex
|
||||
|
||||
OUTPUT_DIR=$PWD
|
||||
|
||||
SOURCE_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]})/..)
|
||||
|
||||
VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream-debian13
|
||||
|
||||
DOCKER_TAG=$(docker build -q - <<EOS
|
||||
FROM debian:trixie-slim
|
||||
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev pkg-config
|
||||
EOS
|
||||
)
|
||||
|
||||
docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG bash -xe <<EOS
|
||||
|
||||
mkdir /build
|
||||
cd /build
|
||||
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -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: libcapnp-1.0.1, 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,contexts,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
|
49
pkg/ubuntu2204-amd64.sh
Executable file
49
pkg/ubuntu2204-amd64.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/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-ubuntu2204
|
||||
|
||||
DOCKER_TAG=$(docker build -q - <<EOS
|
||||
FROM ubuntu:22.04
|
||||
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev pkg-config
|
||||
EOS
|
||||
)
|
||||
|
||||
docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG bash -xe <<EOS
|
||||
|
||||
mkdir /build
|
||||
cd /build
|
||||
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -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: libcapnp-0.8.0, 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,contexts,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
|
||||
|
49
pkg/ubuntu2404-amd64.sh
Executable file
49
pkg/ubuntu2404-amd64.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/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-ubuntu2404
|
||||
|
||||
DOCKER_TAG=$(docker build -q - <<EOS
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev pkg-config
|
||||
EOS
|
||||
)
|
||||
|
||||
docker run --rm -i -v $SOURCE_DIR:/laminar:ro -v $OUTPUT_DIR:/output $DOCKER_TAG bash -xe <<EOS
|
||||
|
||||
mkdir /build
|
||||
cd /build
|
||||
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DLAMINAR_VERSION=$VERSION -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: libcapnp-1.0.1, 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,contexts,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,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2015-2020 Oliver Giles
|
||||
/// Copyright 2015-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -87,12 +87,14 @@ static void printTriggerLink(const char* job, uint run) {
|
||||
|
||||
static void usage(std::ostream& out) {
|
||||
out << "laminarc version " << laminar_version() << "\n";
|
||||
out << "Usage: laminarc [-h|--help] COMMAND [PARAMETERS...]]\n";
|
||||
out << "Usage: laminarc [-h|--help] COMMAND\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 << " JOB_LIST may be prepended with --next, in this case the job will\n";
|
||||
out << " be pushed to the front of the queue instead of the end.\n";
|
||||
out << " set PARAMETER_LIST... sets the given parameters as environment variables in the currently\n";
|
||||
out << " running job. Fails if run outside of a job context.\n";
|
||||
out << " abort NAME NUMBER aborts the run identified by NAME and NUMBER.\n";
|
||||
@ -132,16 +134,25 @@ int main(int argc, char** argv) {
|
||||
|
||||
auto& waitScope = client.getWaitScope();
|
||||
|
||||
if(strcmp(argv[1], "queue") == 0) {
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "Usage %s queue <jobName>\n", argv[0]);
|
||||
int jobNameIndex = 2;
|
||||
bool frontOfQueue = false;
|
||||
|
||||
if(strcmp(argv[1], "queue") == 0 || strcmp(argv[1], "start") == 0 || strcmp(argv[1], "run") == 0) {
|
||||
if(argc < 3 || (strcmp(argv[2], "--next") == 0 && argc < 4)) {
|
||||
fprintf(stderr, "Usage %s %s JOB_LIST...\n", argv[0], argv[1]);
|
||||
return EXIT_BAD_ARGUMENT;
|
||||
}
|
||||
int jobNameIndex = 2;
|
||||
// make a request for each job specified on the commandline
|
||||
if(strcmp(argv[2], "--next") == 0) {
|
||||
frontOfQueue = true;
|
||||
jobNameIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if(strcmp(argv[1], "queue") == 0) {
|
||||
do {
|
||||
auto req = laminar.queueRequest();
|
||||
req.setJobName(argv[jobNameIndex]);
|
||||
req.setFrontOfQueue(frontOfQueue);
|
||||
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
||||
ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::QueueResults> resp){
|
||||
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) {
|
||||
@ -153,16 +164,10 @@ int main(int argc, char** argv) {
|
||||
jobNameIndex += n + 1;
|
||||
} while(jobNameIndex < argc);
|
||||
} else if(strcmp(argv[1], "start") == 0) {
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "Usage %s queue <jobName>\n", argv[0]);
|
||||
return EXIT_BAD_ARGUMENT;
|
||||
}
|
||||
kj::Vector<capnp::RemotePromise<LaminarCi::StartResults>> promises;
|
||||
int jobNameIndex = 2;
|
||||
// make a request for each job specified on the commandline
|
||||
do {
|
||||
auto req = laminar.startRequest();
|
||||
req.setJobName(argv[jobNameIndex]);
|
||||
req.setFrontOfQueue(frontOfQueue);
|
||||
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
||||
ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::StartResults> resp){
|
||||
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) {
|
||||
@ -174,21 +179,16 @@ int main(int argc, char** argv) {
|
||||
jobNameIndex += n + 1;
|
||||
} while(jobNameIndex < argc);
|
||||
} else if(strcmp(argv[1], "run") == 0) {
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "Usage %s run <jobName>\n", argv[0]);
|
||||
return EXIT_BAD_ARGUMENT;
|
||||
}
|
||||
int jobNameIndex = 2;
|
||||
// make a request for each job specified on the commandline
|
||||
do {
|
||||
auto req = laminar.runRequest();
|
||||
req.setJobName(argv[jobNameIndex]);
|
||||
req.setFrontOfQueue(frontOfQueue);
|
||||
int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
|
||||
ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::RunResults> resp){
|
||||
if(resp.getResult() == LaminarCi::JobResult::UNKNOWN)
|
||||
fprintf(stderr, "Failed to start job '%s'\n", argv[2]);
|
||||
else
|
||||
printTriggerLink(argv[jobNameIndex], resp.getBuildNum());
|
||||
else
|
||||
printTriggerLink(argv[jobNameIndex], resp.getBuildNum());
|
||||
if(resp.getResult() != LaminarCi::JobResult::SUCCESS)
|
||||
ret = EXIT_RUN_FAILED;
|
||||
}));
|
||||
@ -233,7 +233,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
auto queued = laminar.listQueuedRequest().send().wait(waitScope);
|
||||
for(auto it : queued.getResult()) {
|
||||
printf("%s\n", it.cStr());
|
||||
printf("%s:%d\n", it.getJob().cStr(), it.getBuildNum());
|
||||
}
|
||||
} else if(strcmp(argv[1], "show-running") == 0) {
|
||||
if(argc != 2) {
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include <sqlite3.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <cstdint>
|
||||
|
||||
struct StdevCtx {
|
||||
double mean;
|
||||
|
13
src/http.cpp
13
src/http.cpp
@ -131,9 +131,16 @@ kj::Promise<void> Http::cleanupPeers(kj::Timer& timer)
|
||||
{
|
||||
return timer.afterDelay(15 * kj::SECONDS).then([&]{
|
||||
for(EventPeer* p : eventPeers) {
|
||||
// an empty SSE message is a colon followed by two newlines
|
||||
p->pendingOutput.push_back(":\n\n");
|
||||
p->fulfiller->fulfill();
|
||||
// Even single threaded, if load causes this timeout to be serviced
|
||||
// before writeEvents has created a fulfiller, or if an exception
|
||||
// caused the destruction of the promise but attach(peer) hasn't yet
|
||||
// removed it from the eventPeers list, we will see a null fulfiller
|
||||
// here
|
||||
if(p->fulfiller) {
|
||||
// an empty SSE message is a colon followed by two newlines
|
||||
p->pendingOutput.push_back(":\n\n");
|
||||
p->fulfiller->fulfill();
|
||||
}
|
||||
}
|
||||
return cleanupPeers(timer);
|
||||
}).eagerlyEvaluate(nullptr);
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
interface LaminarCi {
|
||||
|
||||
queue @0 (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);
|
||||
listQueued @3 () -> (result :List(Text));
|
||||
queue @0 (jobName :Text, params :List(JobParam), frontOfQueue :Bool) -> (result :MethodResult, buildNum :UInt32);
|
||||
start @1 (jobName :Text, params :List(JobParam), frontOfQueue :Bool) -> (result :MethodResult, buildNum :UInt32);
|
||||
run @2 (jobName :Text, params :List(JobParam), frontOfQueue :Bool) -> (result :JobResult, buildNum :UInt32);
|
||||
listQueued @3 () -> (result :List(Run));
|
||||
listRunning @4 () -> (result :List(Run));
|
||||
listKnown @5 () -> (result :List(Text));
|
||||
abort @6 (run :Run) -> (result :MethodResult);
|
||||
|
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2015-2020 Oliver Giles
|
||||
/// Copyright 2015-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -95,11 +95,31 @@ Laminar::Laminar(Server &server, Settings settings) :
|
||||
db = new Database((homePath/"laminar.sqlite").toString(true).cStr());
|
||||
// Prepare database for first use
|
||||
// TODO: error handling
|
||||
db->exec("CREATE TABLE IF NOT EXISTS builds("
|
||||
"name TEXT, number INT UNSIGNED, node TEXT, queuedAt INT, "
|
||||
"startedAt INT, completedAt INT, result INT, output TEXT, "
|
||||
"outputLen INT, parentJob TEXT, parentBuild INT, reason TEXT, "
|
||||
"PRIMARY KEY (name, number))");
|
||||
const char *create_table_stmt =
|
||||
"CREATE TABLE IF NOT EXISTS builds("
|
||||
"name TEXT, number INT UNSIGNED, node TEXT, queuedAt INT, "
|
||||
"startedAt INT, completedAt INT, result INT, output TEXT, "
|
||||
"outputLen INT, parentJob TEXT, parentBuild INT, reason TEXT, "
|
||||
"PRIMARY KEY (name, number DESC))";
|
||||
db->exec(create_table_stmt);
|
||||
|
||||
// Migrate from (name, number) primary key to (name, number DESC).
|
||||
// SQLite does not allow to alter primary key of existing table, so
|
||||
// we have to create a new table.
|
||||
db->stmt("SELECT sql LIKE '%, PRIMARY KEY (name, number))' "
|
||||
"FROM sqlite_master WHERE type = 'table' AND name = 'builds'")
|
||||
.fetch<int>([&](int has_old_index) {
|
||||
if (has_old_index) {
|
||||
LLOG(INFO, "Migrating table to the new primary key");
|
||||
db->exec("BEGIN TRANSACTION");
|
||||
db->exec("ALTER TABLE builds RENAME TO builds_old");
|
||||
db->exec(create_table_stmt);
|
||||
db->exec("INSERT INTO builds SELECT * FROM builds_old");
|
||||
db->exec("DROP TABLE builds_old");
|
||||
db->exec("COMMIT");
|
||||
}
|
||||
});
|
||||
|
||||
db->exec("CREATE INDEX IF NOT EXISTS idx_completion_time ON builds("
|
||||
"completedAt DESC)");
|
||||
|
||||
@ -306,13 +326,17 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
||||
j.EndObject();
|
||||
}
|
||||
j.EndArray();
|
||||
int nQueued = 0;
|
||||
j.startArray("queued");
|
||||
for(const auto& run : queuedJobs) {
|
||||
if (run->name == scope.job) {
|
||||
nQueued++;
|
||||
j.StartObject();
|
||||
j.set("number", run->build);
|
||||
j.set("result", to_string(RunState::QUEUED));
|
||||
j.set("reason", run->reason());
|
||||
j.EndObject();
|
||||
}
|
||||
}
|
||||
j.set("nQueued", nQueued);
|
||||
j.EndArray();
|
||||
db->stmt("SELECT number,startedAt FROM builds WHERE name = ? AND result = ? "
|
||||
"ORDER BY completedAt DESC LIMIT 1")
|
||||
.bind(scope.job, int(RunState::SUCCESS))
|
||||
@ -334,9 +358,8 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
||||
j.set("description", desc == jobDescriptions.end() ? "" : desc->second);
|
||||
} else if(scope.type == MonitorScope::ALL) {
|
||||
j.startArray("jobs");
|
||||
db->stmt("SELECT name,number,startedAt,completedAt,result,reason FROM builds b "
|
||||
"JOIN (SELECT name n,MAX(number) latest FROM builds WHERE result IS NOT NULL GROUP BY n) q "
|
||||
"ON b.name = q.n AND b.number = latest")
|
||||
db->stmt("SELECT name, number, startedAt, completedAt, result, reason "
|
||||
"FROM builds GROUP BY name HAVING number = MAX(number)")
|
||||
.fetch<str,uint,time_t,time_t,int,str>([&](str name,uint number, time_t started, time_t completed, int result, str reason){
|
||||
j.StartObject();
|
||||
j.set("name", name);
|
||||
@ -399,6 +422,8 @@ std::string Laminar::getStatus(MonitorScope scope) {
|
||||
for(const auto& run : queuedJobs) {
|
||||
j.StartObject();
|
||||
j.set("name", run->name);
|
||||
j.set("number", run->build);
|
||||
j.set("result", to_string(RunState::QUEUED));
|
||||
j.EndObject();
|
||||
}
|
||||
j.EndArray();
|
||||
@ -579,7 +604,7 @@ bool Laminar::loadConfiguration() {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
||||
std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params, bool frontOfQueue) {
|
||||
if(!fsHome->exists(kj::Path{"cfg","jobs",name+".run"})) {
|
||||
LLOG(ERROR, "Non-existent job", name);
|
||||
return nullptr;
|
||||
@ -590,7 +615,10 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
||||
jobContexts.at(name).insert("default");
|
||||
|
||||
std::shared_ptr<Run> run = std::make_shared<Run>(name, ++buildNums[name], kj::mv(params), homePath.clone());
|
||||
queuedJobs.push_back(run);
|
||||
if(frontOfQueue)
|
||||
queuedJobs.push_front(run);
|
||||
else
|
||||
queuedJobs.push_back(run);
|
||||
|
||||
db->stmt("INSERT INTO builds(name,number,queuedAt,parentJob,parentBuild,reason) VALUES(?,?,?,?,?,?)")
|
||||
.bind(run->name, run->build, run->queuedAt, run->parentName, run->parentBuild, run->reason())
|
||||
@ -602,6 +630,9 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params) {
|
||||
.startObject("data")
|
||||
.set("name", name)
|
||||
.set("number", run->build)
|
||||
.set("result", to_string(RunState::QUEUED))
|
||||
.set("queueIndex", frontOfQueue ? 0 : (queuedJobs.size() - 1))
|
||||
.set("reason", run->reason())
|
||||
.EndObject();
|
||||
http->notifyEvent(j.str(), name.c_str());
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2015-2020 Oliver Giles
|
||||
/// Copyright 2015-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -52,7 +52,7 @@ public:
|
||||
|
||||
// Queues a job, returns immediately. Return value will be nullptr if
|
||||
// the supplied name is not a known job.
|
||||
std::shared_ptr<Run> queueJob(std::string name, ParamMap params = ParamMap());
|
||||
std::shared_ptr<Run> queueJob(std::string name, ParamMap params = ParamMap(), bool frontOfQueue = false);
|
||||
|
||||
// Return the latest known number of the named job
|
||||
uint latestRun(std::string job);
|
||||
|
@ -21,7 +21,11 @@
|
||||
#include <unistd.h>
|
||||
#include <queue>
|
||||
#include <dirent.h>
|
||||
#if defined(__FreeBSD__)
|
||||
#include <sys/procctl.h>
|
||||
#else
|
||||
#include <sys/prctl.h>
|
||||
#endif
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <kj/async-io.h>
|
||||
@ -317,7 +321,11 @@ int leader_main(void) {
|
||||
// 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.
|
||||
#if defined(__FreeBSD__)
|
||||
procctl(P_PID, 0, PROC_REAP_ACQUIRE, NULL);
|
||||
#else
|
||||
prctl(PR_SET_CHILD_SUBREAPER, 1, NULL, NULL, NULL);
|
||||
#endif
|
||||
|
||||
// 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
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include <kj/async-unix.h>
|
||||
#include <kj/filesystem.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
@ -53,6 +54,13 @@ static void usage(std::ostream& out) {
|
||||
out << " -v enable verbose output\n";
|
||||
}
|
||||
|
||||
static void on_sighup(int)
|
||||
{
|
||||
constexpr const char msg[] = "Laminar received and ignored SIGHUP\n";
|
||||
// write(2) is safe to call inside signal handler.
|
||||
write(STDERR_FILENO, msg, sizeof(msg) - 1);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if(argv[0][0] == '{')
|
||||
return leader_main();
|
||||
@ -92,6 +100,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
signal(SIGINT, &laminar_quit);
|
||||
signal(SIGTERM, &laminar_quit);
|
||||
signal(SIGHUP, &on_sighup);
|
||||
|
||||
printf("laminard version %s started\n", laminar_version());
|
||||
|
||||
|
@ -22,7 +22,12 @@
|
||||
<nav>
|
||||
<table class="table striped">
|
||||
<tr v-for="job in jobsQueued">
|
||||
<td><router-link :to="'jobs/'+job.name">{{job.name}}</router-link> <i>queued</i></td>
|
||||
<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>
|
||||
<i>queued</i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="job in jobsRunning">
|
||||
<td>
|
||||
@ -134,15 +139,11 @@
|
||||
<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 style="width:1px"><span v-html="runIcon('queued')"></span></td>
|
||||
<td colspan="4"><i>{{nQueued}} run(s) queued</i></td>
|
||||
</tr>
|
||||
<tr v-for="job in jobsRunning.concat(jobsRecent)" track-by="$index">
|
||||
<tr v-for="job in jobsQueued.concat(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"><span v-if="job.result!='queued'">{{formatDate(job.started)}}</span></td>
|
||||
<td class="text-center"><span v-if="job.result!='queued'">{{formatDuration(job.started, job.completed)}}</span></td>
|
||||
<td class="text-center vp-sm-hide">{{job.reason}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -157,7 +158,7 @@
|
||||
<template id="run"><div style="display: grid; grid-template-rows: auto 1fr">
|
||||
<div style="padding: 15px">
|
||||
<div style="display: grid; grid-template-columns: auto 25px auto auto 1fr 400px; gap: 5px; align-items: center">
|
||||
<h2 style="white-space: nowrap"><span v-html="runIcon(job.result)"></span> {{route.params.name}} #{{route.params.number}}</h2>
|
||||
<h2 style="white-space: nowrap"><span v-html="runIcon(job.result)"></span> <router-link :to="'jobs/'+route.params.name">{{route.params.name}}</router-link> #{{route.params.number}}</h2>
|
||||
<span></span>
|
||||
<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>
|
||||
@ -186,14 +187,14 @@
|
||||
</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>
|
||||
<code></code>
|
||||
<span v-show="!logComplete" v-html="runIcon('running')" style="display: block;"></span>
|
||||
</div>
|
||||
</div></template>
|
||||
|
||||
<main id="app" style="display: grid; grid-template-rows: auto 1fr auto; height: 100%;">
|
||||
<nav id="nav-top" style="display: grid; grid-template-columns: auto auto 1fr auto auto; grid-gap: 15px;">
|
||||
<router-link to="/" style="display: grid; grid-auto-flow: column; align-items: center; margin: 5px; font-size: 20px;">
|
||||
<router-link to="." style="display: grid; grid-auto-flow: column; align-items: center; margin: 5px; font-size: 20px;">
|
||||
<img src="icon.png"> {{title}}
|
||||
</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;">
|
||||
|
@ -129,7 +129,8 @@ const Charts = (() => {
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
hover: { mode: null }
|
||||
hover: { mode: null },
|
||||
aspectRatio: 2
|
||||
}
|
||||
});
|
||||
c.executorBusyChanged = busy => {
|
||||
@ -159,20 +160,28 @@ const Charts = (() => {
|
||||
datasets: [{
|
||||
label: 'Failed Builds',
|
||||
backgroundColor: "#883d3d",
|
||||
data: data.map(e => e.failed || 0)
|
||||
data: data.map(e => e.failed || 0),
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
},{
|
||||
label: 'Successful Builds',
|
||||
backgroundColor: "#74af77",
|
||||
data: data.map(e => e.success || 0)
|
||||
data: data.map(e => e.success || 0),
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Runs per day' },
|
||||
tooltips:{callbacks:{title: (tip, data) => dayNames[tip[0].index].long}},
|
||||
scales:{yAxes:[{
|
||||
ticks:{userCallback: (label, index, labels) => Number.isInteger(label) ? label: null},
|
||||
stacked: true
|
||||
}]}
|
||||
plugins: {
|
||||
title: { display: true, text: 'Runs per day' },
|
||||
tooltip:{callbacks:{title: (tip) => dayNames[tip[0].dataIndex].long}},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
ticks:{callback: (label, index, labels) => Number.isInteger(label) ? label: null},
|
||||
stacked: true
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
c.jobCompleted = success => {
|
||||
@ -183,7 +192,7 @@ const Charts = (() => {
|
||||
},
|
||||
createRunsPerJobChart: (id, data) => {
|
||||
const c = new Chart(document.getElementById("chartBpj"), {
|
||||
type: 'horizontalBar',
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Object.keys(data),
|
||||
datasets: [{
|
||||
@ -193,9 +202,16 @@ const Charts = (() => {
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Runs per job' },
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
title: { display: true, text: 'Runs per job' },
|
||||
},
|
||||
hover: { mode: null },
|
||||
scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=> Number.isInteger(label) ? label: null}}]}
|
||||
scales: {
|
||||
x: {
|
||||
ticks:{callback: (label, index, labels)=> Number.isInteger(label) ? label: null}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
c.jobCompleted = name => {
|
||||
@ -216,7 +232,7 @@ const Charts = (() => {
|
||||
createTimePerJobChart: (id, data, completedCounts) => {
|
||||
const scale = timeScale(Math.max(...Object.values(data)));
|
||||
const c = new Chart(document.getElementById(id), {
|
||||
type: 'horizontalBar',
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Object.keys(data),
|
||||
datasets: [{
|
||||
@ -226,18 +242,23 @@ const Charts = (() => {
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Mean run time this week' },
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
title: { display: true, text: 'Mean run time this week' },
|
||||
tooltip:{callbacks:{
|
||||
label: (tip) => tip.dataset.label + ': ' + tip.raw.toFixed(2) + ' ' + scale.label.toLowerCase()
|
||||
}}
|
||||
},
|
||||
hover: { mode: null },
|
||||
scales:{xAxes:[{
|
||||
ticks:{userCallback: scale.ticks},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: scale.label
|
||||
scales: {
|
||||
x:{
|
||||
ticks: {callback: scale.ticks},
|
||||
title: {
|
||||
display: true,
|
||||
text: scale.label
|
||||
}
|
||||
}
|
||||
}]},
|
||||
tooltips:{callbacks:{
|
||||
label: (tip, data) => data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel.toFixed(2) + ' ' + scale.label.toLowerCase()
|
||||
}}
|
||||
},
|
||||
}
|
||||
});
|
||||
c.jobCompleted = (name, time) => {
|
||||
@ -261,7 +282,8 @@ const Charts = (() => {
|
||||
label: name,
|
||||
data: durations.map(x => x * scale.factor),
|
||||
borderColor: 'hsl('+(name.hashCode() % 360)+', 27%, 57%)',
|
||||
backgroundColor: 'transparent'
|
||||
backgroundColor: 'transparent',
|
||||
tension: 0.35,
|
||||
});
|
||||
const c = new Chart(document.getElementById(id), {
|
||||
type: 'line',
|
||||
@ -270,21 +292,21 @@ const Charts = (() => {
|
||||
datasets: data.map(e => dataValue(e.name, e.durations))
|
||||
},
|
||||
options:{
|
||||
title: { display: true, text: 'Run time changes' },
|
||||
legend:{ display: true, position: 'bottom' },
|
||||
scales:{
|
||||
xAxes:[{ticks:{display: false}}],
|
||||
yAxes:[{
|
||||
ticks:{userCallback: scale.ticks},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: scale.label
|
||||
}
|
||||
}]
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
title: { display: true, text: 'Run time changes' },
|
||||
tooltip: { enabled: false },
|
||||
},
|
||||
scales:{
|
||||
x: {ticks: {display: false}},
|
||||
y: {
|
||||
ticks: {callback: scale.ticks},
|
||||
title: {
|
||||
display: true,
|
||||
text: scale.label
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltips:{
|
||||
enabled:false
|
||||
}
|
||||
}
|
||||
});
|
||||
c.jobCompleted = (name, time) => {
|
||||
@ -310,62 +332,65 @@ const Charts = (() => {
|
||||
data: {
|
||||
labels: jobs.map(e => '#' + e.number).reverse(),
|
||||
datasets: [{
|
||||
label: 'Average',
|
||||
type: 'line',
|
||||
data: [{x:0, y:avg * scale.factor}, {x:1, y:avg * scale.factor}],
|
||||
borderColor: '#7483af',
|
||||
backgroundColor: 'transparent',
|
||||
xAxisID: 'avg',
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 0,
|
||||
pointHoverRadius: 0,
|
||||
},{
|
||||
label: 'Build time',
|
||||
backgroundColor: jobs.map(e => e.result == 'success' ? '#74af77': '#883d3d').reverse(),
|
||||
barPercentage: 1.0,
|
||||
categoryPercentage: 0.95,
|
||||
data: jobs.map(e => (e.completed - e.started) * scale.factor).reverse()
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
title: { display: true, text: 'Build time' },
|
||||
plugins: {
|
||||
title: { display: true, text: 'Build time' },
|
||||
tooltip: {
|
||||
callbacks:{
|
||||
label: (tip) => scale.ticks(tip.raw) + ' ' + scale.label.toLowerCase()
|
||||
}
|
||||
}
|
||||
},
|
||||
hover: { mode: null },
|
||||
scales:{
|
||||
xAxes:[{
|
||||
categoryPercentage: 0.95,
|
||||
barPercentage: 1.0
|
||||
},{
|
||||
id: 'avg',
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
display: false
|
||||
},
|
||||
gridLines: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false
|
||||
}
|
||||
}],
|
||||
yAxes:[{
|
||||
ticks:{userCallback: scale.ticks},
|
||||
scaleLabel:{display: true, labelString: scale.label}
|
||||
}]
|
||||
},
|
||||
y: {
|
||||
suggestedMax: avg * scale.factor,
|
||||
ticks: {callback: scale.ticks },
|
||||
title: {display: true, text: scale.label}
|
||||
}
|
||||
},
|
||||
tooltips:{callbacks:{
|
||||
label: (tip, data) => scale.ticks(tip.yLabel) + ' ' + scale.label.toLowerCase()
|
||||
}}
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
afterDraw: (chart, args, options) => {
|
||||
const {ctx, avg, chartArea, scales:{y:yaxis}} = chart;
|
||||
const y = chartArea.top + yaxis.height - avg * scale.factor * yaxis.height / yaxis.end;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.translate(chartArea.left, y);
|
||||
ctx.moveTo(0,0);
|
||||
ctx.lineTo(chartArea.width, 0);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = '#7483af';
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}]
|
||||
});
|
||||
c.avg = avg;
|
||||
c.jobCompleted = (num, result, time) => {
|
||||
let avg = c.data.datasets[0].data[0].y / scale.factor;
|
||||
avg = ((avg * (num - 1)) + time) / num;
|
||||
c.data.datasets[0].data[0].y = avg * scale.factor;
|
||||
c.data.datasets[0].data[1].y = avg * scale.factor;
|
||||
if(c.data.datasets[1].data.length == 20) {
|
||||
c.avg = ((c.avg * (num - 1)) + time) / num;
|
||||
c.options.scales.y.suggestedMax = avg * scale.factor;
|
||||
if(c.data.datasets[0].data.length == 20) {
|
||||
c.data.labels.shift();
|
||||
c.data.datasets[1].data.shift();
|
||||
c.data.datasets[1].backgroundColor.shift();
|
||||
c.data.datasets[0].data.shift();
|
||||
c.data.datasets[0].backgroundColor.shift();
|
||||
}
|
||||
c.data.labels.push('#' + num);
|
||||
c.data.datasets[1].data.push(time * scale.factor);
|
||||
c.data.datasets[1].backgroundColor.push(result == 'success' ? '#74af77': '#883d3d');
|
||||
c.data.datasets[0].data.push(time * scale.factor);
|
||||
c.data.datasets[0].backgroundColor.push(result == 'success' ? '#74af77': '#883d3d');
|
||||
c.update();
|
||||
};
|
||||
return c;
|
||||
@ -374,13 +399,11 @@ const Charts = (() => {
|
||||
})();
|
||||
|
||||
// For all charts, set miniumum Y to 0
|
||||
Chart.scaleService.updateScaleDefaults('linear', {
|
||||
ticks: { suggestedMin: 0 }
|
||||
});
|
||||
Chart.defaults.scales.linear.suggestedMin = 0;
|
||||
// Don't display legend by default
|
||||
Chart.defaults.global.legend.display = false;
|
||||
Chart.defaults.plugins.legend.display = false;
|
||||
// Disable tooltip hover animations
|
||||
Chart.defaults.global.hover.animationDuration = 0;
|
||||
Chart.defaults.plugins.tooltip.animation = false;
|
||||
|
||||
// Component for the / endpoint
|
||||
const Home = templateId => {
|
||||
@ -397,8 +420,8 @@ const Home = templateId => {
|
||||
data: () => state,
|
||||
methods: {
|
||||
status: function(msg) {
|
||||
state.jobsQueued = msg.queued;
|
||||
state.jobsRunning = msg.running;
|
||||
state.jobsQueued = msg.queued.reverse();
|
||||
state.jobsRunning = msg.running.reverse();
|
||||
state.jobsRecent = msg.recent;
|
||||
state.resultChanged = msg.resultChanged;
|
||||
state.lowPassRates = msg.lowPassRates;
|
||||
@ -415,7 +438,7 @@ const Home = templateId => {
|
||||
});
|
||||
},
|
||||
job_queued: function(data) {
|
||||
state.jobsQueued.splice(0, 0, data);
|
||||
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex, 0, data);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
job_started: function(data) {
|
||||
@ -482,12 +505,13 @@ const All = templateId => {
|
||||
state.jobsRunning = msg.running;
|
||||
// mix running and completed jobs
|
||||
msg.running.forEach(job => {
|
||||
job.result = 'running';
|
||||
const idx = state.jobs.findIndex(j => j.name === job.name);
|
||||
if (idx > -1)
|
||||
state.jobs[idx] = job;
|
||||
else {
|
||||
// special case: first run of a job.
|
||||
state.jobs.unshift(j);
|
||||
state.jobs.unshift(job);
|
||||
state.jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
||||
}
|
||||
});
|
||||
@ -543,7 +567,7 @@ const All = templateId => {
|
||||
if (expr)
|
||||
ret = state.jobs.filter(job => (new RegExp(expr)).test(job.name));
|
||||
else
|
||||
ret = state.jobs;
|
||||
ret = [...state.jobs];
|
||||
// sort failed before success, newest first
|
||||
ret.sort((a,b) => a.result == b.result ? a.started - b.started : 2*(b.result == 'success')-1);
|
||||
return ret;
|
||||
@ -559,11 +583,11 @@ const All = templateId => {
|
||||
const Job = templateId => {
|
||||
const state = {
|
||||
description: '',
|
||||
jobsQueued: [],
|
||||
jobsRunning: [],
|
||||
jobsRecent: [],
|
||||
lastSuccess: null,
|
||||
lastFailed: null,
|
||||
nQueued: 0,
|
||||
pages: 0,
|
||||
sort: {}
|
||||
};
|
||||
@ -575,11 +599,11 @@ const Job = templateId => {
|
||||
methods: {
|
||||
status: function(msg) {
|
||||
state.description = msg.description;
|
||||
state.jobsRunning = msg.running;
|
||||
state.jobsQueued = msg.queued.reverse();
|
||||
state.jobsRunning = msg.running.reverse();
|
||||
state.jobsRecent = msg.recent;
|
||||
state.lastSuccess = msg.lastSuccess;
|
||||
state.lastFailed = msg.lastFailed;
|
||||
state.nQueued = msg.nQueued;
|
||||
state.pages = msg.pages;
|
||||
state.sort = msg.sort;
|
||||
|
||||
@ -593,11 +617,12 @@ const Job = templateId => {
|
||||
chtBuildTime = Charts.createRunTimeChart("chartBt", msg.recent, msg.averageRuntime);
|
||||
});
|
||||
},
|
||||
job_queued: function() {
|
||||
state.nQueued++;
|
||||
job_queued: function(data) {
|
||||
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex, 0, data);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
job_started: function(data) {
|
||||
state.nQueued--;
|
||||
state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
|
||||
state.jobsRunning.splice(0, 0, data);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
@ -642,7 +667,7 @@ const Run = templateId => {
|
||||
const state = {
|
||||
job: { artifacts: [], upstream: {} },
|
||||
latestNum: null,
|
||||
log: '',
|
||||
logComplete: false,
|
||||
};
|
||||
const logFetcher = (vm, name, num) => {
|
||||
const abort = new AbortController();
|
||||
@ -650,18 +675,62 @@ const Run = templateId => {
|
||||
// ATOW pipeThrough not supported in Firefox
|
||||
//const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
|
||||
const reader = res.body.getReader();
|
||||
const target = document.getElementsByTagName('code')[0];
|
||||
let logToRender = '';
|
||||
let logComplete = false;
|
||||
let tid = null;
|
||||
let lastUiUpdate = 0;
|
||||
|
||||
function updateUI() {
|
||||
// output may contain private ANSI CSI escape sequence to point to
|
||||
// downstream jobs. ansi_up (correctly) discards unknown sequences,
|
||||
// so they must be matched before passing through ansi_up. ansi_up
|
||||
// also (correctly) escapes HTML, so they need to be converted back
|
||||
// to links after going through ansi_up.
|
||||
// A better solution one day would be if ansi_up were to provide
|
||||
// a callback interface for handling unknown sequences.
|
||||
// Also, update the DOM directly rather than using a binding through
|
||||
// Vue, the performance is noticeably better with large logs.
|
||||
target.insertAdjacentHTML('beforeend', ansi_up.ansi_to_html(
|
||||
logToRender.replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m, $1, $2) =>
|
||||
'~~~~LAMINAR_RUN~'+$1+':'+$2+'~'
|
||||
)
|
||||
).replace(/~~~~LAMINAR_RUN~([^:]+):(\d+)~/g, (m, $1, $2) =>
|
||||
'<a href="jobs/'+$1+'" onclick="return LaminarApp.navigate(this.href);">'+$1+'</a>:'+
|
||||
'<a href="jobs/'+$1+'/'+$2+'" onclick="return LaminarApp.navigate(this.href);">#'+$2+'</a>'
|
||||
));
|
||||
logToRender = '';
|
||||
if (logComplete) {
|
||||
// output finished
|
||||
state.logComplete = true;
|
||||
}
|
||||
|
||||
lastUiUpdate = Date.now();
|
||||
tid = null;
|
||||
}
|
||||
|
||||
return function pump() {
|
||||
return reader.read().then(({done, value}) => {
|
||||
value = utf8decoder.decode(value);
|
||||
if (done)
|
||||
if (done) {
|
||||
// do not set state.logComplete directly, because rendering
|
||||
// may take some time, and we don't want the progress indicator
|
||||
// to disappear before rendering is complete. Instead, delay
|
||||
// it until after the entire log has been rendered
|
||||
logComplete = true;
|
||||
// if no render update is pending, schedule one immediately
|
||||
// (do not use the delayed buffering mechanism from below), so
|
||||
// that for the common case of short logs, the loading spinner
|
||||
// disappears immediately as the log is rendered
|
||||
if(tid === null)
|
||||
setTimeout(updateUI, 0);
|
||||
return;
|
||||
state.log += ansi_up.ansi_to_html(
|
||||
value.replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m, $1, $2) =>
|
||||
'<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:'+
|
||||
'<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>'
|
||||
)
|
||||
);
|
||||
vm.$forceUpdate();
|
||||
}
|
||||
// sometimes logs can be very large, and we are calling pump()
|
||||
// furiously to get all the data to the client. To prevent straining
|
||||
// the client renderer, buffer the data and delay the UI updates.
|
||||
logToRender += utf8decoder.decode(value);
|
||||
if(tid === null)
|
||||
tid = setTimeout(updateUI, Math.max(500 - (Date.now() - lastUiUpdate), 0));
|
||||
return pump();
|
||||
});
|
||||
}();
|
||||
@ -684,7 +753,9 @@ const Run = templateId => {
|
||||
state.job = data;
|
||||
state.latestNum = data.latestNum;
|
||||
state.jobsRunning = [data];
|
||||
state.log = '';
|
||||
state.logComplete = false;
|
||||
// DOM is used directly for performance
|
||||
document.getElementsByTagName('code')[0].innerHTML = '';
|
||||
if(this.logstream)
|
||||
this.logstream.abort();
|
||||
if(data.started)
|
||||
@ -825,7 +896,7 @@ Vue.component('RouterView', (() => {
|
||||
};
|
||||
})());
|
||||
|
||||
new Vue({
|
||||
const LaminarApp = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
title: '', // populated by status message
|
||||
@ -851,6 +922,11 @@ new Vue({
|
||||
new Notification('Job ' + data.result, {
|
||||
body: data.name + ' ' + '#' + data.number + ': ' + data.result
|
||||
});
|
||||
},
|
||||
navigate: function(path) {
|
||||
history.pushState(null, null, path);
|
||||
this.$emit('navigate');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -122,7 +122,7 @@ a.active:hover { text-decoration: none; }
|
||||
/* run console ansi colors (based on base16-default-dark and base16-bright) */
|
||||
:root {
|
||||
--ansi-black: #181818;
|
||||
--ansi-red: #f8f8f8;
|
||||
--ansi-red: #ab4642;
|
||||
--ansi-green: #a1b56c;
|
||||
--ansi-yellow: #f7ca88;
|
||||
--ansi-blue: #7cafc2;
|
||||
|
12
src/rpc.cpp
12
src/rpc.cpp
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2015-2019 Oliver Giles
|
||||
/// Copyright 2015-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -53,7 +53,7 @@ public:
|
||||
kj::Promise<void> queue(QueueContext context) override {
|
||||
std::string jobName = context.getParams().getJobName();
|
||||
LLOG(INFO, "RPC queue", 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()), context.getParams().getFrontOfQueue());
|
||||
if(Run* r = run.get()) {
|
||||
context.getResults().setResult(LaminarCi::MethodResult::SUCCESS);
|
||||
context.getResults().setBuildNum(r->build);
|
||||
@ -67,7 +67,7 @@ public:
|
||||
kj::Promise<void> start(StartContext context) override {
|
||||
std::string jobName = context.getParams().getJobName();
|
||||
LLOG(INFO, "RPC start", 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()), context.getParams().getFrontOfQueue());
|
||||
if(Run* r = run.get()) {
|
||||
return r->whenStarted().then([context,r]() mutable {
|
||||
context.getResults().setResult(LaminarCi::MethodResult::SUCCESS);
|
||||
@ -83,7 +83,7 @@ public:
|
||||
kj::Promise<void> run(RunContext context) override {
|
||||
std::string jobName = context.getParams().getJobName();
|
||||
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()), context.getParams().getFrontOfQueue());
|
||||
if(run) {
|
||||
return run->whenFinished().then([context,run](RunState state) mutable {
|
||||
context.getResults().setResult(fromRunState(state));
|
||||
@ -101,7 +101,9 @@ public:
|
||||
auto res = context.getResults().initResult(queue.size());
|
||||
int i = 0;
|
||||
for(auto it : queue) {
|
||||
res.set(i++, it->name);
|
||||
res[i].setJob(it->name);
|
||||
res[i].setBuildNum(it->build);
|
||||
i++;
|
||||
}
|
||||
return kj::READY_NOW;
|
||||
}
|
||||
|
19
src/run.cpp
19
src/run.cpp
@ -21,10 +21,16 @@
|
||||
#include "conf.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <sys/wait.h>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
|
||||
#if defined(__FreeBSD__)
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/limits.h>
|
||||
#endif
|
||||
|
||||
// short syntax helper for kj::Path
|
||||
template<typename T>
|
||||
inline kj::Path operator/(const kj::Path& p, const T& ext) {
|
||||
@ -153,7 +159,20 @@ kj::Promise<RunState> Run::start(RunState lastResult, std::shared_ptr<Context> c
|
||||
// main() by calling leader_main()
|
||||
char* procName;
|
||||
if(asprintf(&procName, "{laminar} %s:%d", name.data(), build) > 0)
|
||||
#if defined(__FreeBSD__)
|
||||
{
|
||||
int sysctl_rq[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
|
||||
size_t self_exe_len = PATH_MAX;
|
||||
char self_exe[PATH_MAX];
|
||||
|
||||
if (sysctl(sysctl_rq, 4, self_exe, &self_exe_len, NULL, 0))
|
||||
_exit(EXIT_FAILURE);
|
||||
|
||||
execl(self_exe, procName, NULL); // does not return
|
||||
}
|
||||
#else
|
||||
execl("/proc/self/exe", procName, NULL); // does not return
|
||||
#endif
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2015-2019 Oliver Giles
|
||||
/// Copyright 2015-2021 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -28,8 +28,8 @@
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/inotify.h>
|
||||
#include <sys/signalfd.h>
|
||||
|
||||
// Size of buffer used to read from file descriptors. Should be
|
||||
// a multiple of sizeof(struct signalfd_siginfo) == 128
|
||||
@ -117,8 +117,11 @@ void Server::listenRpc(Rpc &rpc, kj::StringPtr rpcBindAddress)
|
||||
if(rpcBindAddress.startsWith("unix:"))
|
||||
unlink(rpcBindAddress.slice(strlen("unix:")).cStr());
|
||||
listeners->add(ioContext.provider->getNetwork().parseAddress(rpcBindAddress)
|
||||
.then([this,&rpc](kj::Own<kj::NetworkAddress>&& addr) {
|
||||
return acceptRpcClient(rpc, addr->listen());
|
||||
.then([this,&rpc,rpcBindAddress](kj::Own<kj::NetworkAddress>&& addr) {
|
||||
kj::Own<kj::ConnectionReceiver> listener = addr->listen();
|
||||
if(rpcBindAddress.startsWith("unix:"))
|
||||
chmod(rpcBindAddress.slice(strlen("unix:")).cStr(), 0660);
|
||||
return acceptRpcClient(rpc, kj::mv(listener));
|
||||
}));
|
||||
|
||||
}
|
||||
@ -128,8 +131,19 @@ void Server::listenHttp(Http &http, kj::StringPtr httpBindAddress)
|
||||
if(httpBindAddress.startsWith("unix:"))
|
||||
unlink(httpBindAddress.slice(strlen("unix:")).cStr());
|
||||
listeners->add(ioContext.provider->getNetwork().parseAddress(httpBindAddress)
|
||||
.then([this,&http](kj::Own<kj::NetworkAddress>&& addr) {
|
||||
return http.startServer(ioContext.lowLevelProvider->getTimer(), addr->listen());
|
||||
.then([this,&http,httpBindAddress](kj::Own<kj::NetworkAddress>&& addr) {
|
||||
kj::Own<kj::ConnectionReceiver> listener = addr->listen();
|
||||
if(httpBindAddress.startsWith("unix:"))
|
||||
chmod(httpBindAddress.slice(strlen("unix:")).cStr(), 0660);
|
||||
return http.startServer(ioContext.lowLevelProvider->getTimer(), kj::mv(listener));
|
||||
}).catch_([this,&http,httpBindAddress](kj::Exception&&e) mutable -> kj::Promise<void> {
|
||||
if(e.getType() == kj::Exception::Type::DISCONNECTED) {
|
||||
LLOG(ERROR, "HTTP disconnect, restarting server", e.getDescription());
|
||||
listenHttp(http, httpBindAddress);
|
||||
return kj::READY_NOW;
|
||||
}
|
||||
// otherwise propagate the exception
|
||||
return kj::mv(e);
|
||||
}));
|
||||
}
|
||||
|
||||
@ -157,5 +171,7 @@ kj::Promise<void> Server::handleFdRead(kj::AsyncInputStream* stream, char* buffe
|
||||
|
||||
void Server::taskFailed(kj::Exception &&exception) {
|
||||
//kj::throwFatalException(kj::mv(exception));
|
||||
fprintf(stderr, "taskFailed: %s\n", exception.getDescription().cStr());
|
||||
// prettier
|
||||
fprintf(stderr, "fatal: %s\n", exception.getDescription().cStr());
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2019-2020 Oliver Giles
|
||||
/// Copyright 2019-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -33,20 +33,26 @@
|
||||
class LaminarFixture : public ::testing::Test {
|
||||
public:
|
||||
LaminarFixture() {
|
||||
bind_rpc = std::string("unix:/") + tmp.path.toString(true).cStr() + "/rpc.sock";
|
||||
bind_http = std::string("unix:/") + tmp.path.toString(true).cStr() + "/http.sock";
|
||||
home = tmp.path.toString(true).cStr();
|
||||
tmp.fs->openSubdir(kj::Path{"cfg", "jobs"}, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT);
|
||||
bind_rpc = std::string("unix:/") + home + "/rpc.sock";
|
||||
bind_http = std::string("unix:/") + home + "/http.sock";
|
||||
settings.home = home.c_str();
|
||||
settings.bind_rpc = bind_rpc.c_str();
|
||||
settings.bind_http = bind_http.c_str();
|
||||
settings.archive_url = "/test-archive/";
|
||||
}
|
||||
~LaminarFixture() noexcept(true) {}
|
||||
|
||||
void SetUp() override {
|
||||
tmp.init();
|
||||
server = new Server(*ioContext);
|
||||
laminar = new Laminar(*server, settings);
|
||||
}
|
||||
~LaminarFixture() noexcept(true) {
|
||||
|
||||
void TearDown() override {
|
||||
delete server;
|
||||
delete laminar;
|
||||
tmp.clean();
|
||||
}
|
||||
|
||||
kj::Own<EventSource> eventSource(const char* path) {
|
||||
@ -92,6 +98,14 @@ public:
|
||||
return { res.getResult(), kj::mv(log) };
|
||||
}
|
||||
|
||||
void setNumExecutors(int nexec) {
|
||||
KJ_IF_MAYBE(f, tmp.fs->tryOpenFile(kj::Path{"cfg", "contexts", "default.conf"},
|
||||
kj::WriteMode::CREATE | kj::WriteMode::MODIFY | kj::WriteMode::CREATE_PARENT)) {
|
||||
std::string content = "EXECUTORS=" + std::to_string(nexec);
|
||||
(*f)->writeAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
kj::String stripLaminarLogLines(const kj::String& str) {
|
||||
auto out = kj::heapString(str.size());
|
||||
char *o = out.begin();
|
||||
|
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2019 Oliver Giles
|
||||
/// Copyright 2019-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -162,3 +162,27 @@ TEST_F(LaminarFixture, JobDescription) {
|
||||
ASSERT_TRUE(data.HasMember("description"));
|
||||
EXPECT_STREQ("bar", data["description"].GetString());
|
||||
}
|
||||
|
||||
TEST_F(LaminarFixture, QueueFront) {
|
||||
setNumExecutors(0);
|
||||
defineJob("foo", "true");
|
||||
defineJob("bar", "true");
|
||||
auto es = eventSource("/");
|
||||
auto req1 = client().queueRequest();
|
||||
req1.setJobName("foo");
|
||||
auto res1 = req1.send();
|
||||
auto req2 = client().queueRequest();
|
||||
req2.setFrontOfQueue(true);
|
||||
req2.setJobName("bar");
|
||||
auto res2 = req2.send();
|
||||
ioContext->waitScope.poll();
|
||||
setNumExecutors(2);
|
||||
ioContext->waitScope.poll();
|
||||
ASSERT_GE(es->messages().size(), 5);
|
||||
auto started1 = es->messages().at(3).GetObject();
|
||||
EXPECT_STREQ("job_started", started1["type"].GetString());
|
||||
EXPECT_STREQ("bar", started1["data"]["name"].GetString());
|
||||
auto started2 = es->messages().at(4).GetObject();
|
||||
EXPECT_STREQ("job_started", started2["type"].GetString());
|
||||
EXPECT_STREQ("foo", started2["data"]["name"].GetString());
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
///
|
||||
/// Copyright 2018-2020 Oliver Giles
|
||||
/// Copyright 2018-2022 Oliver Giles
|
||||
///
|
||||
/// This file is part of Laminar
|
||||
///
|
||||
@ -28,12 +28,25 @@ class TempDir {
|
||||
public:
|
||||
TempDir() :
|
||||
path(mkdtemp()),
|
||||
fs(kj::newDiskFilesystem()->getRoot().openSubdir(path, kj::WriteMode::CREATE|kj::WriteMode::MODIFY))
|
||||
fs(kj::newDiskFilesystem()->getRoot().openSubdir(path, kj::WriteMode::MODIFY))
|
||||
{
|
||||
}
|
||||
~TempDir() noexcept {
|
||||
kj::newDiskFilesystem()->getRoot().remove(path);
|
||||
}
|
||||
void init() {
|
||||
// set up empty directory structure
|
||||
fs->openSubdir(kj::Path{"cfg"}, kj::WriteMode::CREATE);
|
||||
fs->openSubdir(kj::Path{"cfg", "jobs"}, kj::WriteMode::CREATE);
|
||||
fs->openSubdir(kj::Path{"cfg", "contexts"}, kj::WriteMode::CREATE);
|
||||
}
|
||||
void clean() {
|
||||
// rm -rf in config folder
|
||||
for(kj::StringPtr name : fs->listNames()) {
|
||||
fs->remove(kj::Path{name});
|
||||
}
|
||||
}
|
||||
|
||||
kj::Path path;
|
||||
kj::Own<const kj::Directory> fs;
|
||||
private:
|
||||
|
Loading…
Reference in New Issue
Block a user