From b36746f55bd1395c83c8a752fa0d4aae8e16b8c1 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 1 Apr 2026 12:39:44 +0200 Subject: [PATCH] Validate Shepherd services on FreeBSD --- docs/PROGRESS.md | 83 ++++ .../phase4-freebsd-shepherd-service.md | 171 +++++++++ tests/shepherd/build-local-guile-fibers.sh | 174 +++++++++ tests/shepherd/build-local-shepherd.sh | 220 +++++++++++ .../run-freebsd-shepherd-service-prototype.sh | 355 ++++++++++++++++++ 5 files changed, 1003 insertions(+) create mode 100644 docs/reports/phase4-freebsd-shepherd-service.md create mode 100755 tests/shepherd/build-local-guile-fibers.sh create mode 100755 tests/shepherd/build-local-shepherd.sh create mode 100755 tests/shepherd/run-freebsd-shepherd-service-prototype.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index ff446e4..725413b 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1266,3 +1266,86 @@ Next recommended step: 2. carry forward the separate real-checkout runtime blocker for later integration work: - investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version` 3. continue using `/frx/store` rather than `/gnu/store` for future FreeBSD integration experiments when a persistent store root is required + +## 2026-04-01 — Phase 4.1 completed: Shepherd built and validated as a regular FreeBSD service manager + +Completed work: + +- added a reproducible local Guile Fibers build harness: + - `tests/shepherd/build-local-guile-fibers.sh` +- added a reproducible local Shepherd build harness: + - `tests/shepherd/build-local-shepherd.sh` +- added a runnable multi-service Shepherd validation harness for FreeBSD: + - `tests/shepherd/run-freebsd-shepherd-service-prototype.sh` +- wrote the Phase 4.1 report: + - `docs/reports/phase4-freebsd-shepherd-service.md` +- ran the service-management prototype successfully and captured metadata under: + - `/tmp/freebsd-shepherd-service-metadata.txt` + +Important findings: + +- the current FreeBSD path now has a working local Shepherd build based on: + - local fixed Guile + - locally installed Guile Fibers `1.4.2` + - Shepherd `1.0.9` +- Shepherd build/install required one concrete FreeBSD-specific toolchain adaptation: + - `SED=/usr/local/bin/gsed` + because the install phase edits wrapper scripts using GNU `sed -i` syntax that base FreeBSD `sed` does not accept +- at runtime, Shepherd reports: + - `System lacks support for 'signalfd'; using fallback mechanism.` + but the fallback path works correctly for supervision on this host +- the prototype successfully validated all requested regular-service capabilities: + - start/stop via `herd` + - dependency handling + - status monitoring + - crash/respawn behavior + - privilege-aware execution +- the concrete service set used for validation included: + - an unprivileged heartbeat logger + - a loopback HTTP service + - a dependent file-monitor service + - a crash-once respawn test service +- observed metadata confirmed: + - `logger_running=yes` + - `web_running=yes` + - `monitor_running=yes` + - `crashy_running=yes` + - `logger_uid=65534` (`nobody`) + - `http_response=shepherd-freebsd-ok` + - `monitor_detected=detected` + - `crashy_counter=2` + +Current assessment: + +- Phase 4.1 is now satisfied on the current FreeBSD prototype track +- Shepherd is no longer just a theoretical later step; it now builds and supervises multiple services correctly on the host when paired with the fixed local Guile stack +- the next question is no longer “can Shepherd run on FreeBSD at all?” but “what is the best FreeBSD init-integration strategy for it on this prototype path?” + +Recent commits: + +- `e380e88` — `Add FreeBSD Guile verification harness` +- `cd721b1` — `Update progress after Guile verification` +- `27916cb` — `Diagnose Guile subprocess crash on FreeBSD` +- `02f7a7f` — `Validate local Guile fix on FreeBSD` +- `4aebea4` — `Add native GNU Hello FreeBSD build harness` +- `c944cdb` — `Validate Guix builder phases on FreeBSD` +- `0a2e48e` — `Validate GNU which builder phases on FreeBSD` +- `245a47d` — `Document gaps to real Guix FreeBSD builds` +- `d62e9b0` — `Investigate Guix derivation generation on FreeBSD` +- `c0a85ed` — `Build local Guile-GnuTLS on FreeBSD` +- `15b9037` — `Build local Guile-Git on FreeBSD` +- `47d31e8` — `Build local Guile-JSON on FreeBSD` +- `d82195b` — `Advance Guix checkout on FreeBSD` +- `9bf3d30` — `Document FreeBSD syscall mapping` +- `7621798` — `Prototype FreeBSD jail build isolation` +- `d65b2af` — `Prototype FreeBSD build user isolation` +- `e404e2e` — `Prototype FreeBSD store management` +- `eb0d77c` — `Adapt GNU build phases for FreeBSD` +- `d47dc9b` — `Prototype FreeBSD package definitions` + +Next recommended step: + +1. complete Phase 4.2 by prototyping how Shepherd should be launched and stopped through FreeBSD init conventions while validating boot/shutdown dependency ordering for essential services +2. after that, bridge Shepherd to key FreeBSD service concepts such as rc.d management, loopback/network configuration, filesystem setup, and temporary user/group administration +3. continue carrying the separate real-checkout runtime blocker for later integration work: + - investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version` diff --git a/docs/reports/phase4-freebsd-shepherd-service.md b/docs/reports/phase4-freebsd-shepherd-service.md new file mode 100644 index 0000000..09e9068 --- /dev/null +++ b/docs/reports/phase4-freebsd-shepherd-service.md @@ -0,0 +1,171 @@ +# Phase 4.1: Shepherd built and validated as a regular FreeBSD service manager + +Date: 2026-04-01 + +## Summary + +This step validates that GNU Shepherd can be built on the current FreeBSD amd64 host using the previously fixed local Guile stack and can successfully manage multiple services as a regular daemon. + +Added files: + +- `tests/shepherd/build-local-guile-fibers.sh` +- `tests/shepherd/build-local-shepherd.sh` +- `tests/shepherd/run-freebsd-shepherd-service-prototype.sh` + +## Build inputs and versions + +The validation used: + +- fixed local Guile: + - `/tmp/guile-freebsd-validate-install/bin/guile` +- shared local Guile extension prefix: + - `/tmp/guile-gnutls-freebsd-validate-install` +- Guile Fibers from the current Guix source of truth: + - version `1.4.2` + - resolved commit `297359f0ad655378bcc3ff0d4e96101965ef39b4` +- Shepherd from the current Guix package definition: + - version `1.0.9` + - nix-base32 `1mh080060lnycys8yq6kkiy363wif8dsip3nyklgd3a1r22wb274` + - verified SHA256 `e488c585c8418df6e8f476dca81b72910f337c9cd3608fb467de5260004000d6` + +## FreeBSD-specific build findings + +### Guile Fibers + +- building from the Guix-matching Git tag required autotools regeneration: + - `autoreconf -vfi` +- the resulting installation validated successfully with: + - `(use-modules (fibers))` + +### Shepherd + +- Shepherd `1.0.9` configured and built successfully against the fixed local Guile plus the locally installed Fibers module +- one FreeBSD-specific build adaptation was required during install: + - configure must use `SED=/usr/local/bin/gsed` +- reason: + - the Shepherd install phase edits installed wrapper scripts with GNU `sed -i` syntax + - base FreeBSD `/usr/bin/sed` rejects that invocation +- after using GNU `sed`, install completed successfully + +## Runtime findings on FreeBSD + +The installed Shepherd and herd commands run successfully on FreeBSD with the local Guile environment. + +Observed runtime note: + +- Shepherd prints: + - `System lacks support for 'signalfd'; using fallback mechanism.` + +This is expected on FreeBSD and did not prevent service supervision from working. + +## Service-management prototype + +Run command: + +```sh +METADATA_OUT=/tmp/freebsd-shepherd-service-metadata.txt \ +./tests/shepherd/run-freebsd-shepherd-service-prototype.sh +``` + +The prototype starts a root-launched Shepherd instance and validates four services: + +1. `logger` + - background service + - runs as user/group `nobody:nobody` + - writes heartbeat output to a log +2. `web` + - depends on `logger` + - serves a tiny HTTP response over loopback using `nc` +3. `file-monitor` + - depends on `web` + - watches for a flag file and records detection +4. `crashy` + - fails on first start + - then respawns and stays running + +## Verified behaviors + +### Start/stop through Shepherd command interface + +The prototype successfully used `herd` to: + +- start services +- inspect service status +- stop the entire service graph through `stop root` + +### Dependency handling + +Starting `file-monitor` automatically started its dependencies: + +- `logger` +- `web` + +All three were then reported as running by `herd status`. + +### Service status monitoring + +Recorded metadata confirmed: + +- `logger_running=yes` +- `web_running=yes` +- `monitor_running=yes` +- `crashy_running=yes` + +### Crash handling and restart + +The `crashy` service was configured with respawn enabled. + +Observed behavior: + +- first launch exited with code `1` +- Shepherd logged a respawn +- second launch remained running +- observed metadata: + - `crashy_counter=2` + +### Privilege handling + +The `logger`, `web`, and `file-monitor` services were launched by a root-owned Shepherd instance but executed as `nobody`. + +Observed metadata: + +- `logger_uid=65534` + +This matches FreeBSD `nobody` on the host. + +### Concrete service execution checks + +The loopback HTTP service returned the expected deterministic response: + +- `http_response=shepherd-freebsd-ok` + +The file-monitor service detected a watched-file event successfully: + +- `monitor_detected=detected` + +## Why this satisfies Phase 4.1 + +Phase 4.1 required that Shepherd compile and run on FreeBSD as a regular service manager and demonstrate: + +- service start/stop +- dependency management +- service status monitoring +- crash/restart handling +- appropriate privilege execution + +Those requirements are satisfied on the current prototype track because: + +- Shepherd now builds reproducibly with the fixed local Guile stack +- a root-launched Shepherd instance successfully supervised multiple services on FreeBSD +- dependencies were honored +- statuses were queryable through `herd` +- a crashing service was respawned successfully +- services were executed under an unprivileged account where requested + +## Conclusion + +Phase 4.1 is satisfied on the current FreeBSD prototype track: + +- Shepherd builds on FreeBSD with a small GNU `sed` install-time adjustment +- the lack of `signalfd` is handled by Shepherd's fallback path +- regular-daemon service supervision works correctly for multiple dependent services on the host diff --git a/tests/shepherd/build-local-guile-fibers.sh b/tests/shepherd/build-local-guile-fibers.sh new file mode 100755 index 0000000..38333fa --- /dev/null +++ b/tests/shepherd/build-local-guile-fibers.sh @@ -0,0 +1,174 @@ +#!/bin/sh +set -eu + +fibers_repo=${GUILE_FIBERS_REPO:-"https://codeberg.org/guile/fibers"} +fibers_version=${GUILE_FIBERS_VERSION:-1.4.2} +install_prefix=${INSTALL_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install} +guile_bin=${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile} +make_bin=${MAKE_BIN:-gmake} + +if [ ! -x "$guile_bin" ]; then + echo "Guile binary is not executable: $guile_bin" >&2 + exit 1 +fi +for tool in git autoreconf pkg-config "$make_bin"; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Required tool not found: $tool" >&2 + exit 1 + fi +done + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-guile-fibers.XXXXXX) + cleanup=1 +fi + +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +guile_bindir=$(CDPATH= cd -- "$(dirname "$guile_bin")" && pwd) +guile_prefix=$(CDPATH= cd -- "$guile_bindir/.." && pwd) +guile_lib_dir=$guile_prefix/lib +guile_version=$(LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" -c '(display (effective-version))') +site_dir=$install_prefix/share/guile/site/$guile_version +site_ccache_dir=$install_prefix/lib/guile/$guile_version/site-ccache +extensions_dir=$install_prefix/lib/guile/$guile_version/extensions + +tool_bindir=$workdir/guile-tools-bin +mkdir -p "$tool_bindir" +ln -sf "$guile_bin" "$tool_bindir/guile-3.0" +ln -sf "$guile_bin" "$tool_bindir/guile" +ln -sf "$guile_bindir/guild" "$tool_bindir/guild-3.0" +ln -sf "$guile_bindir/guild" "$tool_bindir/guild" +ln -sf "$guile_bindir/guile-config" "$tool_bindir/guile-config-3.0" +ln -sf "$guile_bindir/guile-config" "$tool_bindir/guile-config" +ln -sf "$guile_bindir/guile-snarf" "$tool_bindir/guile-snarf" +export PATH="$tool_bindir:$guile_bindir:/usr/local/bin:$PATH" +export ACLOCAL_PATH=/usr/local/share/aclocal${ACLOCAL_PATH:+:$ACLOCAL_PATH} +export PKG_CONFIG_PATH=$install_prefix/lib/pkgconfig:$install_prefix/libdata/pkgconfig:/usr/local/libdata/pkgconfig:/usr/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} +export CPPFLAGS="-I$guile_prefix/include -I$install_prefix/include -I/usr/local/include" +export LDFLAGS="-L$guile_lib_dir -L$install_prefix/lib -L/usr/local/lib -Wl,-rpath,$guile_lib_dir -Wl,-rpath,$install_prefix/lib -Wl,-rpath,/usr/local/lib" +if [ -n "${LD_LIBRARY_PATH:-}" ]; then + export LD_LIBRARY_PATH="$install_prefix/lib:$guile_lib_dir:/usr/local/lib:$LD_LIBRARY_PATH" +else + export LD_LIBRARY_PATH="$install_prefix/lib:$guile_lib_dir:/usr/local/lib" +fi +if [ -d "$site_dir" ]; then + export GUILE_LOAD_PATH="$site_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" +fi +if [ -d "$site_ccache_dir" ]; then + export GUILE_LOAD_COMPILED_PATH="$site_ccache_dir${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" +fi +if [ -d "$extensions_dir" ]; then + export GUILE_EXTENSIONS_PATH="$extensions_dir${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" +fi + +src_dir=$workdir/guile-fibers +build_dir=$workdir/build +metadata_file=$workdir/guile-fibers-build-metadata.txt +env_file=$workdir/guile-fibers-env.sh +clone_log=$workdir/clone.log +autoreconf_log=$workdir/autoreconf.log +configure_log=$workdir/configure.log +build_log=$workdir/build.log +install_log=$workdir/install.log + +printf 'Using Guile: %s\n' "$guile_bin" +printf 'Installing into existing prefix: %s\n' "$install_prefix" +printf 'Working directory: %s\n' "$workdir" + +rm -rf \ + "$site_dir/fibers" \ + "$site_dir/fibers.scm" \ + "$site_ccache_dir/fibers" \ + "$site_ccache_dir/fibers.go" +mkdir -p "$install_prefix" + +git clone --depth 1 --branch "v$fibers_version" "$fibers_repo" "$src_dir" >"$clone_log" 2>&1 +fibers_commit=$(git -C "$src_dir" rev-parse HEAD) +( + cd "$src_dir" + autoreconf -vfi +) >"$autoreconf_log" 2>&1 +mkdir -p "$build_dir" +( + cd "$build_dir" + "$src_dir/configure" --prefix="$install_prefix" +) >"$configure_log" 2>&1 +( + cd "$build_dir" + "$make_bin" GUILE_AUTO_COMPILE=0 -j"${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 1)}" +) >"$build_log" 2>&1 +( + cd "$build_dir" + "$make_bin" GUILE_AUTO_COMPILE=0 install +) >"$install_log" 2>&1 + +export GUILE_LOAD_PATH="$site_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" +export GUILE_LOAD_COMPILED_PATH="$site_ccache_dir${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" +module_check=$($guile_bin -c '(use-modules (fibers)) (display "ok") (newline)') +if [ "$module_check" != "ok" ]; then + echo "(fibers) module validation failed" >&2 + exit 1 +fi + +cat >"$env_file" <"$metadata_file" <&2 + exit 1 +fi +if [ ! -d "$guix_source_dir/guix" ]; then + echo "Guix source tree not found at $guix_source_dir" >&2 + exit 1 +fi +for tool in fetch sha256 bsdtar "$make_bin" "$gsed_bin"; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Required tool not found: $tool" >&2 + exit 1 + fi +done + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-shepherd-build.XXXXXX) + cleanup=1 +fi + +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +guile_bindir=$(CDPATH= cd -- "$(dirname "$guile_bin")" && pwd) +guile_prefix=$(CDPATH= cd -- "$guile_bindir/.." && pwd) +guile_lib_dir=$guile_prefix/lib +guile_version=$(LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" -c '(display (effective-version))') +guile_extra_site_dir=$guile_extra_prefix/share/guile/site/$guile_version +guile_extra_site_ccache_dir=$guile_extra_prefix/lib/guile/$guile_version/site-ccache +guile_extra_extensions_dir=$guile_extra_prefix/lib/guile/$guile_version/extensions + +tool_bindir=$workdir/guile-tools-bin +mkdir -p "$tool_bindir" +ln -sf "$guile_bin" "$tool_bindir/guile-3.0" +ln -sf "$guile_bin" "$tool_bindir/guile" +ln -sf "$guile_bindir/guild" "$tool_bindir/guild-3.0" +ln -sf "$guile_bindir/guild" "$tool_bindir/guild" +ln -sf "$guile_bindir/guile-config" "$tool_bindir/guile-config-3.0" +ln -sf "$guile_bindir/guile-config" "$tool_bindir/guile-config" +ln -sf "$guile_bindir/guile-snarf" "$tool_bindir/guile-snarf" +export PATH="$tool_bindir:$guile_bindir:/usr/local/bin:$PATH" +export ACLOCAL_PATH=/usr/local/share/aclocal${ACLOCAL_PATH:+:$ACLOCAL_PATH} +export PKG_CONFIG_PATH=$guile_extra_prefix/lib/pkgconfig:$guile_extra_prefix/libdata/pkgconfig:$guile_prefix/lib/pkgconfig:/usr/local/libdata/pkgconfig:/usr/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} +export CPPFLAGS="-I$guile_prefix/include -I$guile_extra_prefix/include -I/usr/local/include" +export LDFLAGS="-L$guile_lib_dir -L$guile_extra_prefix/lib -L/usr/local/lib -Wl,-rpath,$guile_lib_dir -Wl,-rpath,$guile_extra_prefix/lib -Wl,-rpath,/usr/local/lib" +if [ -n "${LD_LIBRARY_PATH:-}" ]; then + export LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib:$LD_LIBRARY_PATH" +else + export LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib" +fi +if [ -d "$guile_extra_site_dir" ]; then + export GUILE_LOAD_PATH="$guile_extra_site_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" +fi +if [ -d "$guile_extra_site_ccache_dir" ]; then + export GUILE_LOAD_COMPILED_PATH="$guile_extra_site_ccache_dir${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" +fi +if [ -d "$guile_extra_extensions_dir" ]; then + export GUILE_EXTENSIONS_PATH="$guile_extra_extensions_dir${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" +fi + +metadata_file=$workdir/shepherd-build-metadata.txt +env_file=$workdir/shepherd-env.sh +src_tarball=$workdir/shepherd-$version.tar.gz +src_dir=$workdir/shepherd-$version +build_dir=$workdir/build +configure_log=$workdir/configure.log +build_log=$workdir/build.log +install_log=$workdir/install.log +version_log=$workdir/version.log + +expected_sha256_hex=$(GUILE_AUTO_COMPILE=0 \ + GUILE_LOAD_PATH="$guix_source_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \ + "$guile_bin" -c "(use-modules (guix base32) (rnrs bytevectors) (ice-9 format)) (for-each (lambda (b) (format #t \"~2,'0x\" b)) (bytevector->u8-list (nix-base32-string->bytevector (cadr (command-line))))) (newline)" \ + "$expected_nix_base32") + +printf 'Using Guile: %s\n' "$guile_bin" +printf 'Installing to: %s\n' "$install_prefix" +printf 'Working directory: %s\n' "$workdir" +printf 'Fetching: %s\n' "$source_url" +fetch -o "$src_tarball" "$source_url" + +actual_sha256_hex=$(sha256 -q "$src_tarball") +if [ "$actual_sha256_hex" != "$expected_sha256_hex" ]; then + echo "sha256 mismatch for $src_tarball" >&2 + echo "expected: $expected_sha256_hex" >&2 + echo "actual: $actual_sha256_hex" >&2 + exit 1 +fi + +rm -rf "$src_dir" "$build_dir" "$install_prefix" +mkdir -p "$src_dir" "$build_dir" +bsdtar -xf "$src_tarball" -C "$src_dir" --strip-components=1 + +( + cd "$build_dir" + "$src_dir/configure" \ + --prefix="$install_prefix" \ + --localstatedir="$install_prefix/var" \ + SED="$gsed_bin" \ + GUILE="$guile_bin" \ + GUILD="$guile_bindir/guild" \ + GUILE_SNARF="$guile_bindir/guile-snarf" +) >"$configure_log" 2>&1 + +( + cd "$build_dir" + "$make_bin" GUILE_AUTO_COMPILE=0 -j"${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 1)}" +) >"$build_log" 2>&1 + +( + cd "$build_dir" + "$make_bin" GUILE_AUTO_COMPILE=0 install +) >"$install_log" 2>&1 + +shepherd_version_output=$(GUILE_LOAD_PATH="$guile_extra_site_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \ + GUILE_LOAD_COMPILED_PATH="$guile_extra_site_ccache_dir${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" \ + GUILE_EXTENSIONS_PATH="$guile_extra_extensions_dir${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" \ + LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + "$install_prefix/bin/shepherd" --version 2>&1 | tee "$version_log") +herd_version_output=$(GUILE_LOAD_PATH="$guile_extra_site_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \ + GUILE_LOAD_COMPILED_PATH="$guile_extra_site_ccache_dir${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" \ + GUILE_EXTENSIONS_PATH="$guile_extra_extensions_dir${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" \ + LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + "$install_prefix/bin/herd" --version 2>&1 | tail -n 1) + +case "$shepherd_version_output" in + *"shepherd (GNU Shepherd) $version"*) : ;; + *) + echo "shepherd version validation failed" >&2 + exit 1 + ;; +esac + +case "$shepherd_version_output" in + *"System lacks support for 'signalfd'; using fallback mechanism."*) + signalfd_fallback=yes + ;; + *) + signalfd_fallback=no + ;; +esac + +cat >"$env_file" <"$metadata_file" <&2 + exit 1 +fi + +ensure_built() { + if [ ! -d "$guile_extra_prefix/share/guile/site" ] || \ + ! GUILE_LOAD_PATH="$guile_extra_prefix/share/guile/site/3.0${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" \ + GUILE_LOAD_COMPILED_PATH="$guile_extra_prefix/lib/guile/3.0/site-ccache${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" \ + GUILE_EXTENSIONS_PATH="$guile_extra_prefix/lib/guile/3.0/extensions${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" \ + LD_LIBRARY_PATH="$guile_extra_prefix/lib:/tmp/guile-freebsd-validate-install/lib:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + "$guile_bin" -c '(catch #t (lambda () (use-modules (fibers)) (display "ok") (newline)) (lambda _ (display "missing") (newline)))' | grep -qx ok; then + METADATA_OUT= ENV_OUT= "$repo_root/tests/shepherd/build-local-guile-fibers.sh" + fi + + if [ ! -x "$shepherd_bin" ] || [ ! -x "$herd_bin" ]; then + METADATA_OUT= ENV_OUT= GUILE_EXTRA_PREFIX="$guile_extra_prefix" "$repo_root/tests/shepherd/build-local-shepherd.sh" + fi +} + +ensure_built + +guile_bindir=$(CDPATH= cd -- "$(dirname "$guile_bin")" && pwd) +guile_prefix=$(CDPATH= cd -- "$guile_bindir/.." && pwd) +guile_lib_dir=$guile_prefix/lib +guile_version=$(LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" -c '(display (effective-version))') +extra_site_dir=$guile_extra_prefix/share/guile/site/$guile_version +extra_site_ccache_dir=$guile_extra_prefix/lib/guile/$guile_version/site-ccache +extra_extensions_dir=$guile_extra_prefix/lib/guile/$guile_version/extensions + +run_root_env() { + if [ "$(id -u)" -eq 0 ]; then + env \ + LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib" \ + GUILE_LOAD_PATH="$extra_site_dir" \ + GUILE_LOAD_COMPILED_PATH="$extra_site_ccache_dir" \ + GUILE_EXTENSIONS_PATH="$extra_extensions_dir" \ + "$@" + else + sudo env \ + LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib" \ + GUILE_LOAD_PATH="$extra_site_dir" \ + GUILE_LOAD_COMPILED_PATH="$extra_site_ccache_dir" \ + GUILE_EXTENSIONS_PATH="$extra_extensions_dir" \ + "$@" + fi +} + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/freebsd-shepherd-service.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +port=$((19000 + ($$ % 1000))) +state_dir=$workdir/state +config_file=$workdir/service-config.scm +socket_file=$workdir/shepherd.sock +pid_file=$workdir/shepherd.pid +shepherd_log=$workdir/shepherd.log +shepherd_stdout=$workdir/shepherd.out +metadata_file=$workdir/freebsd-shepherd-service-metadata.txt +logger_status=$workdir/logger.status +web_status=$workdir/web.status +monitor_status=$workdir/file-monitor.status +crashy_status=$workdir/crashy.status +service_command_log=$state_dir/service-events.log +http_response_file=$workdir/http-response.txt + +cleanup_workdir() { + run_root_env "$herd_bin" -s "$socket_file" stop root >/dev/null 2>&1 || true + if [ -f "$pid_file" ]; then + if [ "$(id -u)" -eq 0 ]; then + kill "$(cat "$pid_file")" >/dev/null 2>&1 || true + else + sudo sh -c 'kill "$(cat "$1")" >/dev/null 2>&1 || true' sh "$pid_file" + fi + fi + if [ "$cleanup" -eq 1 ]; then + if [ "$(id -u)" -eq 0 ]; then + rm -rf "$workdir" + else + sudo rm -rf "$workdir" + fi + fi +} +trap cleanup_workdir EXIT INT TERM + +mkdir -p "$state_dir" +chmod 0755 "$workdir" +chmod 0777 "$state_dir" + +cat >"$workdir/logger.sh" <<'EOF' +#!/bin/sh +log=$1 +ready=$2 +uidfile=$3 +printf 'logger-start uid=%s gid=%s\n' "$(id -u)" "$(id -g)" >> "$log" +id -u > "$uidfile" +touch "$ready" +trap 'echo logger-stop >> "$log"; exit 0' TERM INT +while :; do + echo heartbeat >> "$log" + sleep 1 + done +EOF +chmod +x "$workdir/logger.sh" + +cat >"$workdir/web.sh" <<'EOF' +#!/bin/sh +port=$1 +ready=$2 +log=$3 +echo web-start >> "$log" +touch "$ready" +trap 'echo web-stop >> "$log"; exit 0' TERM INT +while :; do + printf 'HTTP/1.0 200 OK\r\nContent-Length: 20\r\n\r\nshepherd-freebsd-ok\n' | nc -l 127.0.0.1 "$port" +done +EOF +chmod +x "$workdir/web.sh" + +cat >"$workdir/monitor.sh" <<'EOF' +#!/bin/sh +watch=$1 +event=$2 +ready=$3 +log=$4 +echo monitor-start >> "$log" +touch "$ready" +trap 'echo monitor-stop >> "$log"; exit 0' TERM INT +while :; do + if [ -f "$watch" ] && [ ! -f "$event" ]; then + echo detected > "$event" + echo monitor-detected >> "$log" + fi + sleep 1 + done +EOF +chmod +x "$workdir/monitor.sh" + +cat >"$workdir/crashy.sh" <<'EOF' +#!/bin/sh +counter=$1 +ready=$2 +log=$3 +n=0 +if [ -f "$counter" ]; then + n=$(cat "$counter") +fi +n=$((n + 1)) +echo "$n" > "$counter" +echo "crashy-attempt-$n" >> "$log" +if [ "$n" -eq 1 ]; then + exit 1 +fi +touch "$ready" +trap 'echo crashy-stop >> "$log"; exit 0' TERM INT +while :; do + sleep 10 + done +EOF +chmod +x "$workdir/crashy.sh" + +cat >"$config_file" <"$shepherd_stdout" 2>&1 & + +for _ in 1 2 3 4 5 6 7 8 9 10; do + if [ -f "$pid_file" ] && [ -S "$socket_file" ]; then + break + fi + sleep 1 +done +if [ ! -f "$pid_file" ] || [ ! -S "$socket_file" ]; then + echo "Shepherd did not become ready" >&2 + exit 1 +fi + +run_root_env "$herd_bin" -s "$socket_file" start file-monitor >/dev/null +for ready in "$state_dir/logger.ready" "$state_dir/web.ready" "$state_dir/monitor.ready"; do + for _ in 1 2 3 4 5 6 7 8 9 10; do + [ -f "$ready" ] && break + sleep 1 + done + [ -f "$ready" ] || { + echo "Expected readiness file missing: $ready" >&2 + exit 1 + } +done + +run_root_env "$herd_bin" -s "$socket_file" status logger >"$logger_status" +run_root_env "$herd_bin" -s "$socket_file" status web >"$web_status" +run_root_env "$herd_bin" -s "$socket_file" status file-monitor >"$monitor_status" + +fetch -T 5 -qo "$http_response_file" "http://127.0.0.1:$port/" +http_response=$(tr -d '\r' <"$http_response_file") +if [ "$http_response" != "shepherd-freebsd-ok" ]; then + echo "Unexpected HTTP response: $http_response" >&2 + exit 1 +fi + +touch "$state_dir/watch.flag" +for _ in 1 2 3 4 5 6 7 8 9 10; do + [ -f "$state_dir/event.flag" ] && break + sleep 1 +done +[ -f "$state_dir/event.flag" ] || { + echo "Monitor did not detect watched file" >&2 + exit 1 +} + +run_root_env "$herd_bin" -s "$socket_file" start crashy >/dev/null || true +for _ in 1 2 3 4 5 6 7 8 9 10; do + [ -f "$state_dir/crashy.ready" ] && break + sleep 1 +done +[ -f "$state_dir/crashy.ready" ] || { + echo "Respawn test service did not reach steady state" >&2 + exit 1 +} +run_root_env "$herd_bin" -s "$socket_file" status crashy >"$crashy_status" + +logger_uid=$(cat "$state_dir/logger.uid") +crashy_counter=$(cat "$state_dir/crashy.counter") +if [ "$logger_uid" != "65534" ]; then + echo "Expected logger to run as nobody (65534), got $logger_uid" >&2 + exit 1 +fi +if [ "$crashy_counter" -lt 2 ]; then + echo "Expected respawn counter >= 2, got $crashy_counter" >&2 + exit 1 +fi + +run_root_env "$herd_bin" -s "$socket_file" stop root >/dev/null || true +for _ in 1 2 3 4 5 6 7 8 9 10; do + [ ! -f "$pid_file" ] && break + sleep 1 +done + +case $(cat "$logger_status") in + *"It is running"*) logger_running=yes ;; + *) logger_running=no ;; +esac +case $(cat "$web_status") in + *"It is running"*) web_running=yes ;; + *) web_running=no ;; +esac +case $(cat "$monitor_status") in + *"It is running"*) monitor_running=yes ;; + *) monitor_running=no ;; +esac +case $(cat "$crashy_status") in + *"It is running"*) crashy_running=yes ;; + *) crashy_running=no ;; +esac +case $(cat "$shepherd_stdout") in + *"System lacks support for 'signalfd'; using fallback mechanism."*) signalfd_fallback=yes ;; + *) signalfd_fallback=no ;; +esac + +cat >"$metadata_file" <