mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-12 22:30:52 +01:00
Follow-up to9085a4e5, which recorded services_units.sh at 84.51% because its remaining branches sit behind [[ -f /etc/systemd/system/*.service ]] -- absolute root-owned paths that exist on this machine, so the else-branch never runs and no PATH shim can redirect it. All 87 such paths across the seven check libs now go through $SYSROOT, read from SERVICES_ROOT. The prefix is deliberately NOT defaulted. Three commands in hosts_fix_resolved run outside `run` and so are not stopped by --dry-run (chattr -i on resolved.conf.d, find -delete over its *.conf, and an append to resolved.conf); with an empty default, a test that forgot to set SERVICES_ROOT would really delete this machine's resolved.conf drop-ins. Unset is therefore a hard error and only an explicitly empty value means the real filesystem. The harness now owns PATH outright: $FAKE_BIN for shims and staged fake apps, $REAL_BIN for a curated set of symlinks to genuine coreutils. The machine's /usr/bin is deliberately absent -- a really-installed discord and chromium otherwise make the "no target apps present" branches untestable, which is exactly what happened until these tests caught it. Coverage, measured against run_all.sh because two test files jointly cover services_common.sh after the 250-cap split: common 55/55, report 27/27, units 72/72 (was 84.51%), wrappers 100/100, apps 66/66, browser 79/79 -- seven libs at 100%. services_hosts.sh reads 86.05% and services_hosts_fix.sh 97.30%, and those 14 lines are a kcov attribution artifact rather than a gap. Falsified rather than assumed: the suite prints "51 passed, 0 failed" when run under kcov, so no group is skipped, and every supposedly-uncovered line is an issues+=("...") whose string can only reach stdout through the print loop below it -- with a passing assertion matching that exact string. Two falsification checks, both re-run after the rewrite: `--status --dry-run` as root stays byte-identical to rev6691cb26modulo the Date: line, and the sha256sums of /etc/hosts, /etc/nsswitch.conf and /etc/systemd/resolved.conf plus the contents of resolved.conf.d are unchanged across a full suite run. The second one is what makes a missed prefix site detectable instead of a matter of inspection: the repair paths really do write. 220 assertions across 8 test files. Also fixes a bug in the harness's own lsattr shim, where printf with a leading-dash format string parsed the dashes as options and silently produced nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
181 lines
7.0 KiB
Bash
Executable File
181 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# lib/services_common.sh — output helpers, root re-exec, deployment-drift
|
|
# verification and the shared report-then-fix path used by every check.
|
|
#
|
|
# Sourced by check_and_enable_services.sh before any check lib. Reads the
|
|
# colour globals, DRY_RUN, STATUS_ONLY, ISSUES_FOUND, FIXES_APPLIED and
|
|
# SERVICE_STATUS from the caller; appends to MISSING_SCRIPTS.
|
|
|
|
######################################################################
|
|
# Helpers
|
|
######################################################################
|
|
# Absolute paths the checks below probe are prefixed with $SYSROOT, which is
|
|
# empty in production and a fixture tree under test. It is deliberately NOT
|
|
# defaulted here: several repairs in this family write outside `run` (chattr,
|
|
# find -delete, an append to resolved.conf), so a test that forgot to set it
|
|
# would edit the real /etc. Unset is a hard error; empty is the real filesystem.
|
|
SYSROOT="${SERVICES_ROOT?SERVICES_ROOT must be set (empty = the real filesystem)}"
|
|
|
|
msg() { printf "${GREEN}[✓]${NC} %s\n" "$*"; }
|
|
|
|
note() { printf "${BLUE}[i]${NC} %s\n" "$*"; }
|
|
|
|
warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; }
|
|
|
|
err() { printf "${RED}[✗]${NC} %s\n" "$*"; }
|
|
|
|
header() { printf "\n${CYAN}=== %s ===${NC}\n" "$*"; }
|
|
|
|
err_missing_script() {
|
|
err "$*"
|
|
logger -t check-and-enable-services -p user.err "MISSING REPAIR SCRIPT: $*"
|
|
MISSING_SCRIPTS+=("$*")
|
|
}
|
|
|
|
# Query a USER systemd unit from root.
|
|
# `sudo -u <user> systemctl --user ...` does NOT work here: sudo gives it no
|
|
# XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS, so it cannot reach the user bus and
|
|
# fails with "Failed to connect to user scope bus via local transport". Every
|
|
# check using it therefore reported EVERY user service as "not enabled" — a false
|
|
# error for services that were enabled and running (workout-locker was reported
|
|
# broken for months while working fine). systemd's own error suggests the fix:
|
|
# --machine=<user>@.host connects to another user's bus correctly.
|
|
user_systemctl() { # <user> <systemctl args...>
|
|
local u="$1"
|
|
shift
|
|
systemctl --user --machine="${u}@.host" "$@"
|
|
}
|
|
|
|
# Are the pacman hooks that unlock/relock the guarded hosts files installed?
|
|
# The hosts guard migrated to guard-lib's GENERIC unlock-all/relock-all hooks
|
|
# (which cover every registered file-guard: hosts, nsswitch, resolved, ...), and
|
|
# the old per-file 10-unlock-etc-hosts.hook / 90-relock-etc-hosts.hook names have
|
|
# not existed since. This check was never updated, so it reported a phantom
|
|
# "Pacman hooks not installed" warning forever. pacman_wrapper.sh treats the
|
|
# guard-lib pair as authoritative (see pacman_hooks_manage_guard_lib) — match it,
|
|
# while still accepting the legacy pair if an older install is present.
|
|
hosts_pacman_hooks_installed() {
|
|
[[ -f "${SYSROOT}/etc/pacman.d/hooks/10-guard-lib-unlock-all.hook" ]] &&
|
|
[[ -f "${SYSROOT}/etc/pacman.d/hooks/90-guard-lib-relock-all.hook" ]]
|
|
}
|
|
|
|
# True if guard-lib's file-guard instance <name> is installed AND healthy:
|
|
# the path unit is active and the target carries the immutable attribute.
|
|
# `guardctl file-guard status` exits 1 for an unregistered instance (missing
|
|
# /etc/guard-lib/targets/<name>.conf), so a missing instance and an unhealthy
|
|
# one both fail this check without needing separate handling.
|
|
guard_lib_instance_healthy() { # <name>
|
|
local name="$1" out
|
|
out=$(guardctl file-guard status "$name" 2>/dev/null) || return 1
|
|
grep -q '^path unit: active$' <<<"$out" || return 1
|
|
grep -q '^target attrs: .*i' <<<"$out" || return 1
|
|
}
|
|
|
|
# Replay a drift manifest written at install time. The manifest holds plain
|
|
# sha256sum lines covering BOTH the repo sources an installer copied from and
|
|
# the installed copies it produced, so one `sha256sum -c` answers both "has the
|
|
# repo moved on?" and "did someone edit the deployed file?".
|
|
#
|
|
# This exists because an existence check is not a deployment check. On
|
|
# 2026-08-03 /usr/local/bin/pacman_wrapper was 19916 B against a 27510 B source
|
|
# — missing the policy integrity manifest, the pacman_lock_lib stale-lock fix
|
|
# and the guard-lib fallbacks — while every check here reported "ok" hourly,
|
|
# because the file was present and /usr/bin/pacman was a symlink. Both of those
|
|
# facts stayed true the entire time the wrapper was a week out of date.
|
|
#
|
|
# Return: 0 verified, 1 drift detected, 2 no manifest (install predates this).
|
|
# Callers must treat 2 as actionable rather than a pass: "cannot verify" is
|
|
# precisely the state that hid the stale wrapper.
|
|
deployment_drift() { # <manifest-path>
|
|
local manifest="$1"
|
|
[[ -f $manifest ]] || return 2
|
|
sha256sum -c --status "$manifest" 2>/dev/null || return 1
|
|
return 0
|
|
}
|
|
|
|
run() {
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
echo -e "${YELLOW}DRY-RUN:${NC} $*"
|
|
return 0
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
require_root() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo "This script requires root privileges."
|
|
echo "Re-executing with sudo..."
|
|
exec sudo -E bash "$0" "$@"
|
|
fi
|
|
}
|
|
|
|
######################################################################
|
|
# Report issues and optionally run fix script
|
|
# Usage: report_and_fix issues_array status_var status_key fix_note setup_script verify_service [args...]
|
|
######################################################################
|
|
report_and_fix() {
|
|
local -n _issues=$1
|
|
local -n _status=$2
|
|
local status_key="$3"
|
|
local fix_note="$4"
|
|
local setup_script="$5"
|
|
local verify_service="${6:-}"
|
|
shift 6
|
|
local script_args=("$@")
|
|
|
|
if [[ $_status != "ok" ]]; then
|
|
for issue in "${_issues[@]}"; do
|
|
if [[ $_status == "error" ]]; then
|
|
err "$issue"
|
|
else
|
|
warn "$issue"
|
|
fi
|
|
done
|
|
((ISSUES_FOUND++)) || true
|
|
|
|
if [[ $STATUS_ONLY -eq 0 && $_status == "error" ]]; then
|
|
note "$fix_note"
|
|
if [[ -f $setup_script ]]; then
|
|
run bash "$setup_script" "${script_args[@]}"
|
|
((FIXES_APPLIED++)) || true
|
|
# Re-verify after fix
|
|
if [[ $DRY_RUN -eq 0 && -n $verify_service ]] && systemctl is-enabled "$verify_service" &>/dev/null; then
|
|
_status="ok"
|
|
fi
|
|
else
|
|
err_missing_script "Setup script not found: $setup_script"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
set_service_status "$status_key" "$_status"
|
|
}
|
|
|
|
# Record one service's verdict.
|
|
#
|
|
# The check libs write SERVICE_STATUS while only services_report.sh reads it.
|
|
# Split across files that is a write-only global, which shellcheck reports as
|
|
# SC2034 in every writing lib — correctly, since the hook runs it with no `-x`
|
|
# and cannot see the reader. Routing every write through this setter, which
|
|
# lives in the same file as a reader, turns the cross-file global into a
|
|
# function call and removes the finding without suppressing it.
|
|
#
|
|
# Writing an already-recorded key is a caller bug: each check owns exactly one
|
|
# key and runs once, so a second write means two checks are fighting over one
|
|
# summary row. Fail loudly rather than let the later one win silently.
|
|
set_service_status() { # <key> <status>
|
|
local key="$1"
|
|
local value="$2"
|
|
if [[ -n ${SERVICE_STATUS[$key]:-} ]]; then
|
|
err "internal error: status for '${key}' already recorded as '$(get_service_status "$key")'"
|
|
return 1
|
|
fi
|
|
SERVICE_STATUS["$key"]="$value"
|
|
}
|
|
|
|
# Read one service's recorded verdict, defaulting to "unknown".
|
|
get_service_status() { # <key>
|
|
printf '%s\n' "${SERVICE_STATUS[$1]:-unknown}"
|
|
}
|