#!/usr/bin/env bash # yams/scripts/local-ci/package-validate-checks.sh # # In-container half of scripts/local-ci/package-validate.sh (the entry point; # run that, not this). It runs as root inside a distro container booted with # systemd as PID 1, and prints one line per check: # # PASS FAIL INFO # # The packages ship a systemd *user* unit; the checks drive it through a # normal user with lingering enabled (so a user manager runs without a login). # # Usage: package-validate-checks.sh [args] # install install like a user would (no env vars) # layout installed file list + ELF + permission audit # service user unit + preset installed, globally enabled, # no system unit # user a user's unit is enabled and active, socket in # $XDG_RUNTIME_DIR, CLI works with no env vars, # yams daemon stop/start/restart go via systemd # onnx after `yams init` the user daemon loads the # onnx plugin with the bundled ONNX Runtime # persist after a container reboot # seed state of an older release (system service) # upgraded after upgrading over `seed` # remove uninstall (deb: remove + purge) # minimal raw base image, no systemd, nothing # preinstalled: install pulls only declared deps # # is deb, rpm or arch. The exit status is the number of FAIL lines (0 = ok). set -uo pipefail PHASE="${1:?phase}" PM="${2:?package manager}" ARG="${3:-}" UNIT=yams-daemon.service USER_UNIT_DIR=/usr/lib/systemd/user SYSTEM_UNIT_DIR=/usr/lib/systemd/system LEGACY_DATA_DIR=/var/lib/yams LEGACY_MANIFEST=/var/tmp/yams-validate-legacy.sha256 UPGRADE_NOTE=/var/tmp/yams-validate-upgrade-note.txt TUSER=alice # the user whose daemon the checks drive (lingering) OUSER=bob # a second user, for isolation PUSER=carol # probes the bundled ONNX Runtime with its own config TOKEN_FILE=/var/tmp/yams-validate-tokens FAILS=0 pass() { printf 'PASS %s %s\n' "$1" "${2:-}"; } fail() { printf 'FAIL %s %s\n' "$1" "${2:-}" FAILS=$((FAILS + 1)) } info() { printf 'INFO %s %s\n' "$1" "${2:-}"; } check() { # check local id="$1" detail="$2" shift 2 if "$@" >/dev/null 2>&1; then pass "${id}" "${detail}"; else fail "${id}" "${detail}"; fi } token() { printf 'yamsval%s%s' "$1" "$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"; } remember() { printf '%s=%s\n' "$1" "$2" >>"${TOKEN_FILE}"; } recall() { sed -n "s/^$1=//p" "${TOKEN_FILE}" 2>/dev/null | tail -n1; } # Run a command as through a login shell (pam_systemd sets # XDG_RUNTIME_DIR like a real login); no YAMS_* variable leaks in. as_user() { local user="$1" rt shift rt="$(user_runtime "${user}")" su - "${user}" -c "[ -n \"\${XDG_RUNTIME_DIR:-}\" ] || export XDG_RUNTIME_DIR=${rt}; $*" } ensure_user() { id "$1" >/dev/null 2>&1 || useradd -m -s /bin/bash "$1" loginctl enable-linger "$1" >/dev/null 2>&1 || true } user_runtime() { printf '/run/user/%s' "$(id -u "$1")"; } # systemctl --user for , with the runtime dir and bus set explicitly. uctl() { local user="$1" rt shift rt="$(user_runtime "${user}")" local -a busenv=() if [ -S "${rt}/bus" ]; then busenv=("DBUS_SESSION_BUS_ADDRESS=unix:path=${rt}/bus"); fi runuser -u "${user}" -- env XDG_RUNTIME_DIR="${rt}" ${busenv[@]+"${busenv[@]}"} \ systemctl --user "$@" 2>/dev/null || true } wait_user_manager() { local uid uid="$(id -u "$1")" for _ in $(seq 1 120); do [ "$(systemctl is-active "user@${uid}.service" 2>/dev/null)" = active ] && [ -S "/run/user/${uid}/systemd/private" ] && return 0 sleep 0.5 done return 1 } wait_user_active() { local sock sock="$(user_runtime "$1")/yams-daemon.sock" for _ in $(seq 1 120); do [ "$(uctl "$1" is-active "${UNIT}")" = active ] && [ -S "${sock}" ] && return 0 sleep 0.5 done return 1 } user_journal() { uctl "$1" status "${UNIT}" --no-pager -n 15 | sed 's/^/INFO user-unit /' } # Exactly one daemon runs for TUSER, and it is the unit's MainPID (no CLI-spawned copy). check_single_daemon() { local main pids main="$(uctl "${TUSER}" show -p MainPID --value "${UNIT}")" pids="$(pgrep -u "${TUSER}" -f '^/usr/bin/yams-daemon' | paste -sd' ' - || true)" if [ -n "${main}" ] && [ "${main}" != 0 ] && [ "${pids}" = "${main}" ]; then pass "$1" "the only daemon for ${TUSER} is the unit's (pid ${main})" else fail "$1" "unit MainPID=${main:-?}, ${TUSER}'s daemons: ${pids:-none}" fi } wait_plugins() { # wait_plugins for _ in $(seq 1 30); do as_user "$1" 'timeout 30 yams plugin health' 2>/dev/null | grep -qi "$2" && return 0 sleep 2 done return 1 } # Poll a search until it reports (indexing is asynchronous). search_finds() { # search_finds local user="$1" query="$2" needle="$3" out="" for _ in $(seq 1 60); do out="$(as_user "${user}" "timeout 30 yams search '${query}'" 2>&1 || true)" case "${out}" in *"${needle}"*) return 0 ;; esac sleep 1 done printf '%s\n' "${out}" | tail -n 5 | sed 's/^/INFO search-output /' return 1 } pkg_files() { case "${PM}" in deb) dpkg -L yams ;; rpm) rpm -ql yams ;; arch) pacman -Qlq yams ;; esac 2>/dev/null | sed 's#/$##' | while IFS= read -r f; do [ -n "${f}" ] || continue # Files and symlinks only; directories are shared with other packages. if [ -L "${f}" ] || [ ! -d "${f}" ]; then printf '%s\n' "${f}"; fi done | sort -u } # Compare package-manager identity, not the filename or the CLI's base version. package_identity_from_file() { local pkg="$1" name version arch case "${PM}" in deb) name="$(dpkg-deb -f "${pkg}" Package)" || return 1 version="$(dpkg-deb -f "${pkg}" Version)" || return 1 arch="$(dpkg-deb -f "${pkg}" Architecture)" || return 1 [ "${name}" = yams ] && [ -n "${version}" ] && [ -n "${arch}" ] || return 1 printf '%s %s %s\n' "${name}" "${version}" "${arch}" ;; rpm) rpm -qp --qf '%{NAME} %{EPOCHNUM}:%{VERSION}-%{RELEASE} %{ARCH}\n' "${pkg}" ;; arch) pacman -Qp "${pkg}" ;; *) return 1 ;; esac } installed_package_identity() { case "${PM}" in deb) [ "$(dpkg-query -W -f='${Status}' yams 2>/dev/null)" = 'install ok installed' ] || return 1 dpkg-query -W -f='${Package} ${Version} ${Architecture}\n' yams ;; rpm) rpm -q --qf '%{NAME} %{EPOCHNUM}:%{VERSION}-%{RELEASE} %{ARCH}\n' yams ;; arch) pacman -Q yams ;; *) return 1 ;; esac } # --------------------------------------------------------------------------- phase_install() { local pkg="${ARG:?package file}" local log=/var/tmp/yams-validate-install.log local rc=0 expected actual # An earlier successful old-package install must not certify a failed candidate. remember INSTALL_VERIFIED no if ! expected="$(package_identity_from_file "${pkg}")" || [ -z "${expected}" ]; then fail install-identity "cannot read candidate package metadata" return fi case "${expected}" in yams\ *) ;; *) fail install-identity "candidate is not yams"; return ;; esac case "${PM}" in deb) # The Docker base image's policy-rc.d blocks service starts; a real host has none. rm -f /usr/sbin/policy-rc.d apt-get update >/dev/null 2>&1 || true apt-get install -y "${pkg}" >"${log}" 2>&1 || rc=$? ;; rpm) dnf install -y "${pkg}" >"${log}" 2>&1 || rc=$? ;; arch) pacman -U --noconfirm "${pkg}" >"${log}" 2>&1 || rc=$? ;; esac if [ "${rc}" -ne 0 ]; then fail install "$(basename "${pkg}") (rc=${rc})" tail -n 20 "${log}" | sed 's/^/INFO install-log /' return fi if ! actual="$(installed_package_identity)" || [ "${actual}" != "${expected}" ]; then fail install-identity "expected '${expected}', installed '${actual:-unknown}'" return fi remember INSTALL_EXPECTED "${expected}" remember INSTALL_VERIFIED yes pass install "$(basename "${pkg}") (${actual})" } # --------------------------------------------------------------------------- phase_layout() { local files files="$(pkg_files)" if [ -z "${files}" ]; then fail layout-filelist "package manager lists no files for yams" return fi info layout-filelist "$(printf '%s\n' "${files}" | wc -l) files" # Runtime package allowlist. Anything else is either a leak (headers, # static libs, pkg-config, build trees) or a new file someone must review. local allow='^/usr/bin/(yams|yams-cli|yams-daemon|yams-mcp-server)$' allow+='|^/usr/lib(64)?/(yams/)?libyams_[A-Za-z0-9_]+\.so(\.[0-9]+)*$' allow+='|^/usr/lib(64)?/yams/plugins/[A-Za-z0-9_.-]+\.so$' # Private fallback ONNX Runtime used by the ONNX/Glint plugins when no compatible # system copy exists (core library, its soname symlinks, providers_shared). allow+='|^/usr/lib(64)?/yams/onnxruntime/libonnxruntime(_providers_shared)?\.so(\.[0-9]+)*$' allow+='|^/usr/lib/systemd/user/yams-daemon\.service$' allow+='|^/usr/lib/systemd/user-preset/80-yams\.preset$' allow+='|^/usr/share/doc/yams(/[A-Za-z0-9_./-]+)?$' allow+='|^/usr/share/licenses/yams(/[A-Za-z0-9_.-]+)?$' # rpm's debuginfo build-id links (symlinks to the shipped ELF files). allow+='|^/usr/lib/\.build-id(/[0-9a-f]{2}(/[0-9a-f]+)?)?$' allow+='|^/usr/share/yams/[A-Za-z0-9_./-]+$' local unexpected unexpected="$(printf '%s\n' "${files}" | grep -Ev "${allow}" || true)" if [ -z "${unexpected}" ]; then pass layout-allowlist "every installed path is on the runtime allowlist" else fail layout-allowlist "unexpected installed paths: $(printf '%s ' ${unexpected})" fi local devel devel="$(printf '%s\n' "${files}" | grep -E '^/usr/include/|\.a$|\.pc$|\.(h|hpp|hh)$|/cmake/' || true)" if [ -z "${devel}" ]; then pass layout-no-devel "no headers, static libraries, pkg-config or CMake files" else fail layout-no-devel "development files in runtime package: $(printf '%s ' ${devel})" fi for required in /usr/bin/yams /usr/bin/yams-daemon /usr/lib/systemd/user/yams-daemon.service \ /usr/lib/systemd/user-preset/80-yams.preset; do check "layout-has:${required}" "installed" test -e "${required}" done local bad_perm="" local f while IFS= read -r f; do # Skip symlinks and paths the image's dpkg path-exclude dropped (docs). if [ -L "${f}" ] || [ ! -e "${f}" ]; then continue; fi if [ -n "$(find "${f}" -maxdepth 0 \( -perm -0002 -o -perm -4000 -o -perm -2000 \) 2>/dev/null)" ]; then bad_perm+="${f} " fi if [ "$(stat -c %U "${f}")" != root ]; then bad_perm+="${f}(owner=$(stat -c %U "${f}")) " fi done <<<"${files}" if [ -z "${bad_perm}" ]; then pass layout-perms "no world-writable, setuid/setgid or non-root-owned files" else fail layout-perms "${bad_perm}" fi # ELF: dependencies resolve on a clean system and no build-tree rpath leaks. local elf_bad="" rpath_bad="" elf_count=0 while IFS= read -r f; do if [ -L "${f}" ] || [ ! -e "${f}" ]; then continue; fi [ "$(head -c4 "${f}" 2>/dev/null | od -An -c | tr -d ' ')" = "177ELF" ] || continue elf_count=$((elf_count + 1)) local missing missing="$(ldd "${f}" 2>&1 | grep -E 'not found' || true)" [ -n "${missing}" ] && elf_bad+="${f}: $(printf '%s' "${missing}" | tr -s ' \n' ' ') " local rp rp="$(readelf -d "${f}" 2>/dev/null | sed -n 's/.*(R\(UN\)\{0,1\}PATH).*\[\(.*\)\]/\2/p' | tr ':' '\n')" local entry while IFS= read -r entry; do [ -z "${entry}" ] && continue case "${entry}" in '$ORIGIN' | '$ORIGIN/'* | /usr/lib | /usr/lib64 | /usr/lib/yams | /usr/lib/yams/*) ;; *) rpath_bad+="${f}:${entry} " ;; esac done <<<"${rp}" done <<<"${files}" info layout-elf "${elf_count} ELF objects" if [ -z "${elf_bad}" ]; then pass layout-elf-needed "all DT_NEEDED resolve (ldd)"; else fail layout-elf-needed "${elf_bad}"; fi if [ -z "${rpath_bad}" ]; then pass layout-elf-runpath "RPATH/RUNPATH only \$ORIGIN or /usr/lib[/yams]"; else fail layout-elf-runpath "${rpath_bad}"; fi } # --------------------------------------------------------------------------- # Packaging-level state of the user unit (as root, no user session needed). phase_service() { check service-user-unit "user unit installed in ${USER_UNIT_DIR}" test -f "${USER_UNIT_DIR}/${UNIT}" check service-user-preset "user preset installed" test -f /usr/lib/systemd/user-preset/80-yams.preset if [ -e "/usr/lib/systemd/system/${UNIT}" ] || [ -e "/lib/systemd/system/${UNIT}" ]; then fail service-no-system-unit "a system unit is still installed" else pass service-no-system-unit "no system unit (the daemon is per-user)" fi local g g="$(systemctl --global is-enabled "${UNIT}" 2>/dev/null || true)" if [ "${g}" = enabled ]; then pass service-global-enabled "enabled for all users (user preset applied)" else fail service-global-enabled "systemctl --global is-enabled says '${g:-?}'" fi if grep -Eq '^(ProtectHome|PrivateTmp)=' "${USER_UNIT_DIR}/${UNIT}"; then fail unit-reads-home "unit hides the user's files (ProtectHome/PrivateTmp)" else pass unit-reads-home "unit can read the user's home and /tmp" fi } # --------------------------------------------------------------------------- # A normal user with lingering: the user manager starts the unit (globally # enabled), and the CLI uses it with no environment set up by hand. phase_user() { ensure_user "${TUSER}" if ! wait_user_manager "${TUSER}"; then fail user-manager "user@$(id -u "${TUSER}").service did not start" journalctl -u "user@$(id -u "${TUSER}").service" --no-pager -n 8 2>/dev/null | sed 's/^/INFO user-manager /' return fi pass user-manager "user manager running for ${TUSER} (linger)" local verify verify="$(runuser -u "${TUSER}" -- env XDG_RUNTIME_DIR="$(user_runtime "${TUSER}")" \ systemd-analyze --user verify "${USER_UNIT_DIR}/${UNIT}" 2>&1 | grep -viE 'cgroup' || true)" if [ -z "${verify}" ]; then pass unit-verify "systemd-analyze --user verify is clean" else fail unit-verify "$(printf '%s' "${verify}" | tr '\n' ' ')" fi local xdg xdg="$(su - "${TUSER}" -c 'printf %s "${XDG_RUNTIME_DIR:-}"')" if [ "${xdg}" = "$(user_runtime "${TUSER}")" ]; then pass user-login-env "su login has XDG_RUNTIME_DIR=${xdg} (pam_systemd)" else info user-login-env "su login lacks XDG_RUNTIME_DIR (no pam_systemd in su); the checks set it like a real login" fi local en en="$(uctl "${TUSER}" is-enabled "${UNIT}")" if [ "${en}" = enabled ]; then pass user-enabled "${UNIT} enabled for ${TUSER}"; else fail user-enabled "is-enabled: '${en}'"; fi if wait_user_active "${TUSER}"; then pass user-active "${UNIT} active for ${TUSER} without a manual start" else fail user-active "${UNIT} is '$(uctl "${TUSER}" is-active "${UNIT}")'" user_journal "${TUSER}" return fi local sock perms sock="$(user_runtime "${TUSER}")/yams-daemon.sock" perms="$(stat -c '%a %U' "${sock}" 2>/dev/null || true)" case "${perms}" in *" ${TUSER}") pass user-socket "socket ${sock} (${perms})" ;; *) fail user-socket "no socket owned by ${TUSER} at ${sock} (${perms:-missing})" ;; esac local t rc=0 out t="$(token user)" remember USER "${t}" as_user "${TUSER}" "printf 'user note %s\n' '${t}' > ~/user-note.txt; printf 'tmp note %s\n' '${t}x' > /tmp/user-tmp-note.txt" out="$(as_user "${TUSER}" 'timeout 60 yams daemon status' 2>&1)" || rc=$? if [ "${rc}" -eq 0 ] && ! printf '%s' "${out}" | grep -qi 'not running'; then pass cli-status "yams daemon status reaches the unit's daemon" else fail cli-status "rc=${rc}: $(printf '%s' "${out}" | tail -n 2 | tr '\n' ' ')" fi rc=0 out="$(as_user "${TUSER}" 'timeout 120 yams add ~/user-note.txt && timeout 120 yams add /tmp/user-tmp-note.txt' 2>&1)" || rc=$? if [ "${rc}" -eq 0 ]; then pass cli-add "yams add from \$HOME and /tmp"; else fail cli-add "rc=${rc}: $(printf '%s' "${out}" | tail -n 2 | tr '\n' ' ')"; fi if search_finds "${TUSER}" "${t}" user-note; then pass cli-search "home file searchable"; else fail cli-search "home note not found by yams search"; fi if as_user "${TUSER}" 'test -e ~/.local/share/yams/yams.db'; then pass cli-data-dir "corpus in ~${TUSER}/.local/share/yams" else fail cli-data-dir "no ~/.local/share/yams/yams.db for ${TUSER}" fi check_single_daemon cli-single-daemon check user-log "daemon log in ~/.local/state/yams" as_user "${TUSER}" 'test -s ~/.local/state/yams/daemon.log' # Lifecycle commands go through systemd. as_user "${TUSER}" 'timeout 60 yams daemon stop' >/dev/null 2>&1 || true local st st="$(uctl "${TUSER}" is-active "${UNIT}")" if [ "${st}" != active ]; then pass cli-stop "yams daemon stop stopped the unit (${st})"; else fail cli-stop "unit still active after yams daemon stop"; fi as_user "${TUSER}" 'timeout 60 yams daemon start' >/dev/null 2>&1 || true if wait_user_active "${TUSER}"; then pass cli-start-unit "yams daemon start started the unit" else fail cli-start-unit "unit is '$(uctl "${TUSER}" is-active "${UNIT}")' after yams daemon start" fi check_single_daemon cli-start-single local before after before="$(uctl "${TUSER}" show -p MainPID --value "${UNIT}")" as_user "${TUSER}" 'timeout 60 yams daemon restart' >/dev/null 2>&1 || true wait_user_active "${TUSER}" || true after="$(uctl "${TUSER}" show -p MainPID --value "${UNIT}")" if [ -n "${after}" ] && [ "${after}" != 0 ] && [ "${after}" != "${before}" ]; then pass cli-restart "yams daemon restart restarted the unit (pid ${before} -> ${after})" else fail cli-restart "MainPID ${before} -> ${after}" fi check cli-install-user "yams daemon install --user enables the packaged unit" \ as_user "${TUSER}" 'yams daemon install --user | grep -q "packaged" && test ! -e ~/.config/systemd/user/yams-daemon.service' # Users are isolated: another user's runtime dir and socket are private. ensure_user "${OUSER}" if as_user "${OUSER}" "test -r $(user_runtime "${TUSER}")/yams-daemon.sock || test -x $(user_runtime "${TUSER}")"; then fail user-isolation "${OUSER} can reach ${TUSER}'s runtime dir" else pass user-isolation "${OUSER} cannot reach ${TUSER}'s daemon" fi } # --------------------------------------------------------------------------- # `yams init` writes the user's config; with it the unit loads the plugins and # the onnx plugin uses the bundled ONNX Runtime (lane images have no system copy). phase_onnx() { local bundled bundled="$(ls -d /usr/lib/yams/onnxruntime /usr/lib64/yams/onnxruntime 2>/dev/null | head -n 1)" if [ -n "${bundled}" ] && ls "${bundled}"/libonnxruntime.so* >/dev/null 2>&1; then pass onnx-runtime-shipped "bundled runtime present in ${bundled}" else fail onnx-runtime-shipped "no libonnxruntime.so under /usr/lib/yams/onnxruntime" fi check onnx-plugin-shipped "onnx plugin installed" test -e /usr/lib/yams/plugins/libyams_onnx_plugin.so local rc=0 out out="$(as_user "${TUSER}" 'timeout 300 yams init --non-interactive --no-keygen' 2>&1)" || rc=$? if as_user "${TUSER}" "grep -Eq '^[[:space:]]*auto_load_plugins[[:space:]]*=[[:space:]]*true' ~/.config/yams/config.toml"; then pass onnx-init-config "yams init wrote auto_load_plugins = true (rc=${rc})" else fail onnx-init-config "rc=${rc}; config lacks auto_load_plugins = true: $(printf '%s' "${out}" | tail -n 2 | tr '\n' ' ')" as_user "${TUSER}" 'mkdir -p ~/.config/yams && printf "[daemon]\nauto_load_plugins = true\n" >> ~/.config/yams/config.toml' fi uctl "${TUSER}" restart "${UNIT}" >/dev/null if ! wait_user_active "${TUSER}"; then fail onnx-restart "unit did not come back after yams init" user_journal "${TUSER}" return fi if wait_plugins "${TUSER}" glint; then pass onnx-autoload "init's config autoloads plugins (glint loaded)" else fail onnx-autoload "no plugins loaded with init's config" as_user "${TUSER}" 'timeout 60 yams plugin health' 2>&1 | tail -n 4 | sed 's/^/INFO plugin-health /' fi check_single_daemon onnx-single-daemon # The bundled ONNX Runtime, probed with a separate user so TUSER keeps # init's configuration for the later phases: plugin autoload with the # default ("auto") embedding backend adopts the onnx model provider, which # resolves the runtime at load (no model is installed, so nothing embeds). ensure_user "${PUSER}" if ! wait_user_manager "${PUSER}"; then fail onnx-probe-user "no user manager for ${PUSER}" return fi # config_version = 3 keeps the daemon from migrating this file to the full # template (whose default embedding backend is the built-in simeon). as_user "${PUSER}" 'mkdir -p ~/.config/yams && printf "[version]\nconfig_version = 3\n\n[daemon]\nauto_load_plugins = true\n\n[embeddings]\nenable = true\nbackend = \"auto\"\n" > ~/.config/yams/config.toml' local since since="$(date '+%Y-%m-%d %H:%M:%S')" uctl "${PUSER}" restart "${UNIT}" >/dev/null wait_user_active "${PUSER}" || true local line="" for _ in $(seq 1 60); do line="$(journalctl -q --no-pager _UID="$(id -u "${PUSER}")" --since "${since}" 2>/dev/null | grep -E 'Using (bundled|system) ONNX Runtime' | tail -n 1)" [ -n "${line}" ] && break sleep 1 done case "${line}" in *"Using bundled ONNX Runtime"*) pass onnx-runtime-bundled "user daemon: ${line##*] }" ;; *) fail onnx-runtime-bundled "no 'Using bundled ONNX Runtime' from the user daemon (${line:-none})" journalctl -q --no-pager _UID="$(id -u "${PUSER}")" --since "${since}" 2>/dev/null | grep -iE 'onnx|plugin' | tail -n 8 | sed 's/^/INFO onnx-log /' ;; esac if wait_plugins "${PUSER}" onnx; then pass onnx-plugin-loaded "yams plugin health lists the onnx plugin" else fail onnx-plugin-loaded "yams plugin health does not list onnx" fi uctl "${PUSER}" stop "${UNIT}" >/dev/null loginctl disable-linger "${PUSER}" >/dev/null 2>&1 || true } # --------------------------------------------------------------------------- phase_persist() { if ! wait_user_manager "${TUSER}"; then fail reboot-user-manager "no user manager for ${TUSER} after reboot (linger)" return fi if wait_user_active "${TUSER}"; then pass reboot-active "${UNIT} active for ${TUSER} after reboot" else fail reboot-active "${UNIT} is '$(uctl "${TUSER}" is-active "${UNIT}")' after reboot" user_journal "${TUSER}" return fi local t t="$(recall USER)" if [ -n "${t}" ] && search_finds "${TUSER}" "${t}" user-note; then pass reboot-data-persists "${TUSER}'s document survives a reboot" else fail reboot-data-persists "${TUSER}'s document missing after reboot" fi } # --------------------------------------------------------------------------- # Record existing legacy bytes only after the old system daemon has stopped. # Do not manufacture an absent /var/lib/yams to satisfy an upgrade postcondition. snapshot_legacy_corpus() { [ -d "${LEGACY_DATA_DIR}" ] && [ ! -L "${LEGACY_DATA_DIR}" ] || return 1 [ -n "$(find "${LEGACY_DATA_DIR}" -type f -print -quit)" ] || return 1 find "${LEGACY_DATA_DIR}" -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum >"${LEGACY_MANIFEST}" || return 1 [ -s "${LEGACY_MANIFEST}" ] } phase_seed() { : >"${TOKEN_FILE}" local mode t hash out state="" rc=0 if [ -f "${SYSTEM_UNIT_DIR}/${UNIT}" ] || [ -f "/lib/systemd/system/${UNIT}" ]; then mode=legacy # Keep the original startup budget, but only for a real legacy system unit. for _ in $(seq 1 120); do state="$(systemctl is-active "${UNIT}" 2>/dev/null || true)" [ "${state}" = active ] && break sleep 0.5 done if [ "${state}" != active ]; then fail upgrade-seed-service "legacy system daemon is not active" return fi pass upgrade-old-system-unit "old release runs the system ${UNIT}" if ! systemctl stop "${UNIT}" || [ "$(systemctl is-active "${UNIT}" 2>/dev/null)" = active ]; then fail upgrade-seed-legacy "cannot quiesce legacy corpus" return fi elif [ -f "${USER_UNIT_DIR}/${UNIT}" ]; then mode=per-user info upgrade-old-system-unit "per-user release; no legacy system service expected" else fail upgrade-seed-mode "neither a legacy system unit nor a packaged user unit exists" return fi if [ "${mode}" = legacy ] || [ -e "${LEGACY_DATA_DIR}" ]; then if ! snapshot_legacy_corpus; then fail upgrade-seed-legacy "no verifiable existing legacy corpus" return fi remember LEGACY_PRESENT yes pass upgrade-seed-legacy "existing legacy files recorded by SHA256" else remember LEGACY_PRESENT no fi ensure_user "${TUSER}" if ! wait_user_manager "${TUSER}"; then fail upgrade-seed-service "no user manager for ${TUSER}" return fi if [ "${mode}" = per-user ]; then uctl "${TUSER}" start "${UNIT}" >/dev/null if ! wait_user_active "${TUSER}"; then fail upgrade-seed-service "old per-user daemon is not ready" return fi fi # The legacy lane also preserves a user's locally ingested document while # migrating the package from system-service to per-user-service layout. t="$(token seed)" printf 'package upgrade note %s\n' "${t}" >"${UPGRADE_NOTE}" chmod 644 "${UPGRADE_NOTE}" hash="$(sha256sum "${UPGRADE_NOTE}" | awk '{print $1}')" if ! as_user "${TUSER}" "cat '${UPGRADE_NOTE}' > ~/pre-upgrade-note.txt"; then fail upgrade-seed-add "cannot prepare user source document" return fi out="$(as_user "${TUSER}" 'timeout 120 yams add ~/pre-upgrade-note.txt' 2>&1)" || rc=$? if [ "${rc}" -ne 0 ]; then fail upgrade-seed-add "rc=${rc}: $(printf '%s' "${out}" | tail -n 2)" return fi if ! as_user "${TUSER}" "timeout 120 yams cat --hash '${hash}'" >"${UPGRADE_NOTE}.before" || ! cmp -s "${UPGRADE_NOTE}" "${UPGRADE_NOTE}.before"; then fail upgrade-seed-body "pre-transition body does not match original bytes" return fi if ! search_finds "${TUSER}" "${t}" pre-upgrade-note; then fail upgrade-seed-search "pre-transition document is not searchable" return fi # Do not let an old CLI-spawned daemon answer the post-install probes. if ! as_user "${TUSER}" 'timeout 60 yams daemon stop'; then fail upgrade-seed-service "cannot stop old user daemon before package transition" return fi remember UPGRADE_TOKEN "${t}" remember UPGRADE_HASH "${hash}" remember UPGRADE_MODE "${mode}" pass upgrade-seed-body "pre-transition document searchable and SHA256 body hydrated" # The candidate package must be installed next; an old install cannot qualify it. remember INSTALL_VERIFIED no } phase_upgraded() { local mode hash t expected actual legacy_present mode="$(recall UPGRADE_MODE)" legacy_present="$(recall LEGACY_PRESENT)" hash="$(recall UPGRADE_HASH)" t="$(recall UPGRADE_TOKEN)" case "${mode}" in legacy | per-user) ;; *) fail upgrade-prerequisites "missing verified pre-transition corpus"; return ;; esac case "${legacy_present}" in yes | no) ;; *) fail upgrade-prerequisites "legacy corpus precondition not recorded"; return ;; esac if [ "${mode}" = legacy ] && [ "${legacy_present}" != yes ]; then fail upgrade-prerequisites "legacy fixture did not record existing corpus" return fi if [ -z "${t}" ] || ! printf '%s\n' "${hash}" | grep -Eq '^[0-9a-f]{64}$' || [ ! -s "${UPGRADE_NOTE}" ]; then fail upgrade-prerequisites "missing verified pre-transition body/hash" return fi expected="$(recall INSTALL_EXPECTED)" if [ "$(recall INSTALL_VERIFIED)" != yes ] || [ -z "${expected}" ] || ! actual="$(installed_package_identity)" || [ "${actual}" != "${expected}" ]; then fail upgrade-package-identity "candidate install not verified or installed metadata differs" return fi pass upgrade-package-identity "candidate '${actual}' installed and configured" if [ "$(systemctl is-active "${UNIT}" 2>/dev/null)" = active ]; then fail upgrade-system-unit-stopped "old system unit still running" else pass upgrade-system-unit-stopped "old system unit stopped" fi if [ -e "${SYSTEM_UNIT_DIR}/${UNIT}" ] || [ -e "/lib/systemd/system/${UNIT}" ]; then fail upgrade-system-unit-removed "system unit file still installed" else pass upgrade-system-unit-removed "system unit file removed" fi check_no_dangling_links upgrade-no-dangling-links if [ "${legacy_present}" = yes ]; then if [ -s "${LEGACY_MANIFEST}" ] && sha256sum --quiet -c "${LEGACY_MANIFEST}"; then pass upgrade-old-corpus-kept "every recorded legacy file retains its SHA256" else fail upgrade-old-corpus-kept "legacy manifest missing, files removed or bytes changed" fi else info upgrade-old-corpus-kept "no pre-existing legacy corpus; checking seeded user data" fi local g g="$(systemctl --global is-enabled "${UNIT}" 2>/dev/null || true)" if [ "${g}" = enabled ]; then pass upgrade-global-enabled "user unit enabled for all users"; else fail upgrade-global-enabled "'${g:-?}'"; fi # The already-running user manager learns about the unit; it starts at the # next login, or now on request. uctl "${TUSER}" daemon-reload >/dev/null local en en="$(uctl "${TUSER}" is-enabled "${UNIT}")" if [ "${en}" = enabled ]; then pass upgrade-user-enabled "${UNIT} enabled for ${TUSER}"; else fail upgrade-user-enabled "'${en}'"; fi uctl "${TUSER}" start "${UNIT}" >/dev/null if wait_user_active "${TUSER}"; then pass upgrade-user-active "${TUSER}'s user daemon runs after the upgrade" else fail upgrade-user-active "'$(uctl "${TUSER}" is-active "${UNIT}")'" user_journal "${TUSER}" return fi if as_user "${TUSER}" "timeout 120 yams cat --hash '${hash}'" >"${UPGRADE_NOTE}.after" && cmp -s "${UPGRADE_NOTE}" "${UPGRADE_NOTE}.after"; then pass upgrade-seeded-body "pre-transition body retains its SHA256 and exact bytes" else fail upgrade-seeded-body "pre-transition document missing or changed" fi if search_finds "${TUSER}" "${t}" pre-upgrade-note; then pass upgrade-seeded-search "pre-transition document remains searchable" else fail upgrade-seeded-search "pre-transition document missing from search" fi local rc=0 t="$(token upgrade)" as_user "${TUSER}" "printf 'upgrade note %s\n' '${t}' > ~/upgrade-note.txt" as_user "${TUSER}" 'timeout 120 yams add ~/upgrade-note.txt' >/dev/null 2>&1 || rc=$? if [ "${rc}" -eq 0 ] && search_finds "${TUSER}" "${t}" upgrade-note; then pass upgrade-cli "${TUSER}'s CLI works against the user daemon" else fail upgrade-cli "add rc=${rc} or document not found" fi } # --------------------------------------------------------------------------- phase_minimal() { local pkg="${ARG:?package file}" local log=/var/tmp/yams-validate-minimal.log rc=0 case "${PM}" in deb) export DEBIAN_FRONTEND=noninteractive apt-get update >/dev/null 2>&1 || true apt-get install -y "${pkg}" >"${log}" 2>&1 || rc=$? ;; rpm) dnf install -y "${pkg}" >"${log}" 2>&1 || rc=$? ;; arch) pacman -Sy --noconfirm >/dev/null 2>&1 || true pacman -U --noconfirm "${pkg}" >"${log}" 2>&1 || rc=$? ;; esac if [ "${rc}" -eq 0 ]; then pass minimal-install "$(basename "${pkg}") installs on the bare base image" else fail minimal-install "rc=${rc}" tail -n 15 "${log}" | sed 's/^/INFO minimal-install-log /' return fi if [ "${PM}" = deb ]; then local status status="$(dpkg-query -W -f='${Status}' yams 2>/dev/null)" if [ "${status}" = "install ok installed" ]; then pass minimal-configured "dpkg status '${status}'" else fail minimal-configured "dpkg status '${status}' (half-configured?)" fi fi check minimal-user-unit "user unit installed" test -f "${USER_UNIT_DIR}/${UNIT}" check minimal-version "yams --version runs" yams --version local t out t="$(token minimal)" mkdir -p /var/tmp/yams-min-home printf 'minimal note %s\n' "${t}" >/var/tmp/yams-min-home/note.txt out="$(env -i HOME=/var/tmp/yams-min-home PATH=/usr/bin:/bin timeout 180 yams add /var/tmp/yams-min-home/note.txt 2>&1)" || true local found=0 for _ in $(seq 1 30); do if env -i HOME=/var/tmp/yams-min-home PATH=/usr/bin:/bin timeout 60 yams search "${t}" 2>/dev/null | grep -q note; then found=1 break fi sleep 1 done if [ "${found}" -eq 1 ]; then pass minimal-cli "yams add + search work without systemd" else fail minimal-cli "$(printf '%s' "${out}" | tail -n 2 | tr '\n' ' ')" fi } # --------------------------------------------------------------------------- check_no_dangling_links() { # check_no_dangling_links local dangling dangling="$(find /etc/systemd/system /etc/systemd/user -name "${UNIT}" -xtype l 2>/dev/null || true)" if [ -z "${dangling}" ]; then pass "$1" "no dangling enablement symlinks" else fail "$1" "$(printf '%s ' ${dangling})" fi } phase_remove() { local rc=0 log=/var/tmp/yams-validate-remove.log case "${PM}" in deb) apt-get remove -y yams >"${log}" 2>&1 || rc=$? ;; rpm) dnf remove -y yams >"${log}" 2>&1 || rc=$? ;; arch) pacman -R --noconfirm yams >"${log}" 2>&1 || rc=$? ;; esac if [ "${rc}" -eq 0 ]; then pass remove "package removed"; else fail remove "rc=${rc}" tail -n 20 "${log}" | sed 's/^/INFO remove-log /' fi local st st="$(uctl "${TUSER}" is-active "${UNIT}")" if [ "${st}" = active ]; then fail remove-user-stopped "${TUSER}'s user daemon still active after removal" else pass remove-user-stopped "${TUSER}'s user daemon stopped (${st:-gone})" fi if pgrep -f '^/usr/bin/yams-daemon' >/dev/null 2>&1; then fail remove-no-daemons "yams-daemon processes left: $(pgrep -fa '^/usr/bin/yams-daemon' | tr '\n' ' ')" else pass remove-no-daemons "no yams-daemon processes left" fi check remove-binaries "binaries removed" test ! -e /usr/bin/yams-daemon check remove-global-disabled "global enablement removed" test ! -e "/etc/systemd/user/default.target.wants/${UNIT}" check_no_dangling_links remove-no-dangling-links check remove-user-data-kept "${TUSER}'s corpus kept" as_user "${TUSER}" 'test -e ~/.local/share/yams/yams.db' if [ "${PM}" = deb ]; then rc=0 apt-get purge -y yams >>"${log}" 2>&1 || rc=$? if [ "${rc}" -eq 0 ]; then pass purge "package purged"; else fail purge "rc=${rc}"; fi check purge-user-data-kept "${TUSER}'s corpus kept after purge" as_user "${TUSER}" 'test -e ~/.local/share/yams/yams.db' fi } case "${PHASE}" in install) phase_install ;; layout) phase_layout ;; service) phase_service ;; user) phase_user ;; onnx) phase_onnx ;; persist) phase_persist ;; seed) phase_seed ;; upgraded) phase_upgraded ;; remove) phase_remove ;; minimal) phase_minimal ;; *) echo "unknown phase: ${PHASE}" >&2 exit 64 ;; esac exit "${FAILS}"