From 1e5b4927e5ae1d542d51158eb995d4f4b5a8b1c5 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Fri, 18 Sep 2026 10:35:11 +0200 Subject: deploy: --dev hot-reloads the daemon via docker cp + SIGHUP - bokfd re-execs its own binary on SIGHUP, closing listeners/db first - scripts/deploy.sh --dev cross-builds and copies binaries into the running container, then verifies the reported version --- docs/DEPLOY.md | 17 ++++++++ docs/PROTOCOL.md | 5 ++- docs/STATE.md | 4 +- scripts/deploy.sh | 116 +++++++++++++++++++++++++++++++++++++++++------------- src/bokfd.c | 20 +++++++++- 5 files changed, 130 insertions(+), 32 deletions(-) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 8862fab..2964a19 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -50,6 +50,23 @@ to touch an already initialized database. Subsequent `scripts/deploy.sh` runs update the image, tag and compose file, restart the daemon and wait for the healthcheck. +## Fast iteration (`--dev`) + +Normal deploys build an image and recreate the container. While developing, +`scripts/deploy.sh --dev` skips the image entirely: + +- the gate (`make` + `make test`) still runs locally, +- on an architecture mismatch the binaries are cross-compiled here + (`deploy/Dockerfile.cross`, ~20 s on a PC), +- the binaries are copied into the running container with `docker cp`, +- the daemon is reloaded with `SIGHUP`, which re-execs the binary in place + (in-memory sessions are lost, clients reconnect), +- the version the daemon reports is verified against the tag. + +No image is built and the container is not recreated; a later normal deploy +replaces the copied binaries. Use a descriptive tag, e.g. +`scripts/deploy.sh --dev v0.2.0-rc1`. + ## Deploying upgrades ```sh diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index f282691..1baf607 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -488,4 +488,7 @@ beyond the session and calls nothing but public commands. | `allow_org_create` | `true` | any user may create an org | Container deployments mount the socket directory, database directory, backup -and export directories as volumes; the daemon is otherwise stateless. +and export directories as volumes; the daemon is otherwise stateless. On +`SIGHUP` the daemon closes its listeners and database and re-executes its own +binary in place (used by `scripts/deploy.sh --dev`); in-memory sessions are +reset and clients reconnect. diff --git a/docs/STATE.md b/docs/STATE.md index 3cbe448..458833e 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -65,7 +65,9 @@ server/protocol/ledger only. come from a lego sidecar using INWX DNS-01 (`compose.yaml`). Externals get accounts/roles/tokens, never VPN access. `scripts/deploy.sh` builds locally and ships over SSH, or builds on the host when architectures - differ. + differ. `scripts/deploy.sh --dev` cross-compiles the binaries here and + hot-reloads the daemon (SIGHUP re-exec via `docker cp`), skipping the + image build and container recreate. ## Pending decisions diff --git a/scripts/deploy.sh b/scripts/deploy.sh index c345a21..79d731f 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,7 +1,12 @@ #!/usr/bin/env bash # Deploy a locally built bokf image to BOKF_HOST over SSH. See docs/DEPLOY.md. # -# scripts/deploy.sh [tag] +# scripts/deploy.sh [--dev] [tag] +# +# Normal mode builds an image (cross-compiled on this machine when the target +# architecture differs) and recreates the container. --dev skips the image: +# it copies the fresh binaries into the running container and reloads the +# daemon with SIGHUP, which re-execs them in place. # # BOKF_HOST (user@host) and BOKF_REMOTE_DIR are read from the environment or # from .env in the repository root. The host needs only Docker and Compose. @@ -23,6 +28,12 @@ fi BOKF_REMOTE_DIR="${BOKF_REMOTE_DIR:-/srv/bokf}" SSH=(ssh "$BOKF_HOST") +DEV=0 +if [ "${1:-}" = "--dev" ]; then + DEV=1 + shift +fi + TAG="${1:-}" if [ -z "$TAG" ]; then TAG="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" @@ -36,11 +47,41 @@ esac remote() { "${SSH[@]}" "$1"; } +wait_healthy() { + for _ in $(seq 1 30); do + cid="$(remote "cd '$BOKF_REMOTE_DIR' && docker compose ps -q bokfd" 2>/dev/null || true)" + if [ -n "$cid" ]; then + st="$(remote "docker inspect --format '{{.State.Health.Status}}' '$cid'" 2>/dev/null || true)" + if [ "$st" = "healthy" ]; then + return 0 + fi + fi + sleep 2 + done + return 1 +} + +cross_build() { + echo "deploy: cross-compiling $host_arch binaries on this machine" + rm -rf .prebuilt + mkdir -p .prebuilt + docker build -q -f deploy/Dockerfile.cross -t bokf-cross deploy/ >/dev/null + docker run --rm -e "VERSION=$TAG" -v "$PWD":/src:ro \ + -v "$PWD/.prebuilt":/out bokf-cross + if command -v file >/dev/null 2>&1 && + ! file .prebuilt/bokfd | grep -q aarch64; then + echo "deploy: cross-build produced no arm64 binary" >&2 + rm -rf .prebuilt + exit 1 + fi +} + echo "deploy: gate: build + tests" make -j"$(nproc)" make test BOKF_BUILD="${BOKF_BUILD:-auto}" +host_arch="$(uname -m)" case "$BOKF_BUILD" in auto) host_arch="$(remote 'uname -m' 2>/dev/null || echo unknown)" @@ -51,31 +92,64 @@ case "$BOKF_BUILD" in BOKF_BUILD=remote fi ;; - local | remote) ;; + local | remote) + host_arch="$(remote 'uname -m' 2>/dev/null || echo unknown)" + ;; *) echo "deploy: BOKF_BUILD must be auto, local or remote" >&2 exit 1 ;; esac +if [ "$DEV" = 1 ]; then + if ! remote "test -f '$BOKF_REMOTE_DIR/var/db/bokfd.db'"; then + echo "deploy: no database on $BOKF_HOST; run a normal deploy first" >&2 + exit 1 + fi + if [ "$BOKF_BUILD" = local ]; then + prebin="build" + echo "deploy: dev: using the locally built binaries" + else + cross_build + prebin=".prebuilt" + fi + echo "deploy: dev: copying binaries into the running container" + remote "mkdir -p /tmp/bokf-dev" + scp -q "$prebin/bokfd" "$prebin/bokfctl" "$prebin/bokftui" \ + "$BOKF_HOST:/tmp/bokf-dev/" + remote "cd '$BOKF_REMOTE_DIR' + cid=\$(docker compose ps -q bokfd) + test -n \"\$cid\" || { echo 'deploy: bokfd is not running' >&2; exit 1; } + docker cp /tmp/bokf-dev/bokfd \"\$cid:/usr/local/bin/bokfd\" + docker cp /tmp/bokf-dev/bokfctl \"\$cid:/usr/local/bin/bokfctl\" + docker cp /tmp/bokf-dev/bokftui \"\$cid:/usr/local/bin/bokftui\" + docker kill --signal=HUP \"\$cid\"" + if [ "$prebin" = ".prebuilt" ]; then + rm -rf .prebuilt + fi + ver="" + for _ in $(seq 1 15); do + ver="$(remote "cd '$BOKF_REMOTE_DIR' && docker exec \$(docker compose ps -q bokfd) bokfctl meta 2>/dev/null" | + sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' || true)" + [ "$ver" = "$TAG" ] && break + sleep 1 + done + if [ "$ver" = "$TAG" ]; then + echo "deploy: dev reloaded, daemon reports $ver" + exit 0 + fi + echo "deploy: dev reload did not come back with $TAG (got '${ver:-nothing}')" >&2 + remote "cd '$BOKF_REMOTE_DIR' && docker compose logs --tail=30 bokfd" >&2 || true + exit 1 +fi + if [ "$BOKF_BUILD" = local ]; then echo "deploy: building image bokf:$TAG locally" docker build --build-arg "VERSION=$TAG" -t "bokf:$TAG" . echo "deploy: shipping image to $BOKF_HOST" docker save "bokf:$TAG" | gzip | "${SSH[@]}" 'gunzip | docker load' else - echo "deploy: cross-compiling $host_arch binaries on this machine" - rm -rf .prebuilt - mkdir -p .prebuilt - docker build -q -f deploy/Dockerfile.cross -t bokf-cross deploy/ >/dev/null - docker run --rm -e "VERSION=$TAG" -v "$PWD":/src:ro \ - -v "$PWD/.prebuilt":/out bokf-cross - if command -v file >/dev/null 2>&1 && - ! file .prebuilt/bokfd | grep -q aarch64; then - echo "deploy: cross-build produced no arm64 binary" >&2 - rm -rf .prebuilt - exit 1 - fi + cross_build echo "deploy: assembling image bokf:$TAG on $BOKF_HOST" tar -cf - \ --exclude=./.git --exclude=./build --exclude=./var --exclude=./.env \ @@ -106,20 +180,6 @@ fi echo "deploy: starting containers" remote "cd '$BOKF_REMOTE_DIR' && docker compose up -d --no-build" -wait_healthy() { - for _ in $(seq 1 30); do - cid="$(remote "cd '$BOKF_REMOTE_DIR' && docker compose ps -q bokfd" 2>/dev/null || true)" - if [ -n "$cid" ]; then - st="$(remote "docker inspect --format '{{.State.Health.Status}}' '$cid'" 2>/dev/null || true)" - if [ "$st" = "healthy" ]; then - return 0 - fi - fi - sleep 2 - done - return 1 -} - if wait_healthy; then echo "deploy: bokf:$TAG is healthy on $BOKF_HOST" exit 0 diff --git a/src/bokfd.c b/src/bokfd.c index 944e910..933b274 100644 --- a/src/bokfd.c +++ b/src/bokfd.c @@ -38,6 +38,7 @@ struct conn { }; static volatile sig_atomic_t g_stop = 0; +static volatile sig_atomic_t g_reload = 0; static size_t g_line_limit = 1024 * 1024; static SSL_CTX *g_tls_ctx = NULL; static time_t g_tls_cert_mtime = 0; @@ -50,6 +51,12 @@ static void on_signal(int sig) g_stop = 1; } +static void on_reload(int sig) +{ + (void)sig; + g_reload = 1; +} + static int mkdir_p(const char *path, mode_t mode) { char tmp[4096]; @@ -582,6 +589,7 @@ int main(int argc, char **argv) signal(SIGINT, on_signal); signal(SIGTERM, on_signal); + signal(SIGHUP, on_reload); signal(SIGPIPE, SIG_IGN); sessions_init(g_cfg.session_ttl); @@ -599,7 +607,7 @@ int main(int argc, char **argv) int lfds[3]; int ltls[3]; - while (!g_stop) { + while (!g_stop && !g_reload) { int n = 0; int nlisteners = 0; lfds[nlisteners] = lfd; @@ -670,7 +678,10 @@ int main(int argc, char **argv) nconns = keep; } - log_info("shutting down"); + if (g_reload) + log_info("reloading on SIGHUP"); + else + log_info("shutting down"); for (size_t i = 0; i < nconns; i++) { if (conns[i].ssl) SSL_free(conns[i].ssl); @@ -691,5 +702,10 @@ int main(int argc, char **argv) sessions_free_all(); sqlite3_close(db); config_free(); + if (g_reload) { + execv(argv[0], argv); + log_error("re-exec %s failed: %s", argv[0], strerror(errno)); + return 1; + } return 0; } -- cgit v1.3