mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
check_and_enable_services: split into lib/ under the 250-line cap
1225 lines down to 202, with the 21 functions moved into eight sourced libraries grouped by subject. The entry script now holds only the configuration, the argument parsing and the order the checks run in. check_hosts was the one function that could not move verbatim: at 260 lines it would have stayed over the cap on its own, so its repair half is now hosts_fix_nsswitch / hosts_fix_resolved / hosts_repair_all in services_hosts_fix.sh. Repair order is preserved (resolver config, then the hosts file, then the guard-lib migration), and hosts_repair_all returns non-zero under --dry-run so a dry run can no longer promote a status to "ok" on the strength of repairs that never happened. SERVICE_STATUS was a write-only global across five libs, which is a real SC2034 rather than noise: the pre-commit hook runs shellcheck with no -x and genuinely cannot see the one reader. It now goes through set_service_status / get_service_status, which also rejects a duplicate write — each check owns exactly one summary row, so a second write means two checks are fighting over it. No suppression was added anywhere. Proof the behaviour is unchanged: `--status --dry-run` run as root against this machine is byte-identical to the same command from a worktree at the pre-split revision, modulo the Date: line. verify_shell_split.sh reports check_hosts as the single expected difference; the other 20 functions hash identically. 72 assertions across three test files pass, with 100% line coverage on services_common.sh (54/54) and services_report.sh (27/27). services_units.sh measures 84.51% (60/71). The 11 uncovered lines are the else-branches of [[ -f /etc/systemd/system/*.service ]] and [[ -f /usr/local/bin/*.sh ]] probes — absolute root-owned paths that exist on this machine and that no PATH shim can redirect. Reaching them needs a root-prefix rewrite of live enforcement logic, which is not a line-count fix and would forfeit the verbatim-move proof for everything else. Found while testing, deliberately NOT fixed here: in all three unit checks the is-active branch assigns status="warning" unconditionally, overwriting the status="error" the is-enabled branch set just above it. A timer that is both disabled and inactive therefore never gets repaired, because report_and_fix only repairs on "error". It is identical in the pre-split file, so this split neither caused nor hides it. The tests pin the current behaviour and name it; fixing it changes enforcement and earns its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
This commit is contained in:
parent
6691cb2644
commit
9085a4e508
@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Split check_and_enable_services.sh under the 250-line cap with a tested lib/",
|
||||
"objective": "Bring linux_configuration/scripts/periodic_background/check_and_enable_services.sh from 1225 lines to under the repo-wide 250-line cap by moving its 21 functions into eight sourced libraries beside it, without changing what the tool does on this machine. This is the first of the three safe-tier files in Phase 1 of the 250-cap campaign.",
|
||||
"acceptance_criteria": [
|
||||
"check_and_enable_services.sh and every lib/services_*.sh file is at or under 250 lines; bash meta/scripts/check_file_length.sh --all reports no entry under linux_configuration/scripts/periodic_background/.",
|
||||
"meta/scripts/verify_shell_split.sh against rev 6691cb26 shows every moved function byte-identical after shfmt minification, with check_hosts as the single expected difference because its repair half was deliberately factored into hosts_fix_nsswitch / hosts_fix_resolved / hosts_repair_all.",
|
||||
"sudo bash check_and_enable_services.sh --status --dry-run produces output byte-identical to the same command run from a worktree at rev 6691cb26, modulo the Date: line.",
|
||||
"shellcheck reports zero findings on the entry script, all eight libs and all four test files, both standalone (as the pre-commit hook runs it, with no -x) and with -x.",
|
||||
"shell_coverage.sh reports 100% line coverage for services_common.sh and services_report.sh.",
|
||||
"No suppression of any kind is added: no shellcheck disable directive, no noqa, no per-file ignore.",
|
||||
"New sourced libs carry a shebang and the executable bit, staged with git add --chmod=+x."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Fixing the latent status-downgrade bug found in services_units.sh, where an unconditional status=\"warning\" on the is-active check overwrites the status=\"error\" from the is-enabled check, so a timer that is both disabled and inactive is never repaired. It is identical in the pre-split file, it changes live enforcement behaviour, and it belongs in its own commit. The tests pin the current behaviour and name it.",
|
||||
"Threading a root-prefix variable through the 29 absolute root-owned paths the check libs probe (/usr/bin/pacman, /etc/hosts, /etc/systemd/system/*.service, ...). That is a rewrite of live enforcement logic, not a line-count fix, and it would forfeit the verbatim-move proof for the other 20 functions.",
|
||||
"Reaching 100% coverage on the six libs whose branches are gated by those absolute paths.",
|
||||
"The other seven files still over the cap."
|
||||
],
|
||||
"verifier": "bash meta/scripts/check_file_length.sh --all (no periodic_background entry), bash meta/scripts/verify_shell_split.sh 6691cb26 <old> <old> <new>... (check_hosts the only diff), the three lib/tests/test_services_*.sh scripts (72 assertions, 0 failures), bash meta/scripts/shell_coverage.sh for services_common.sh and services_report.sh (100.00% each), and a diff of sudo bash check_and_enable_services.sh --status --dry-run against the same command at rev 6691cb26."
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
{
|
||||
"intent": "Bring check_and_enable_services.sh (1225 lines) under the repo-wide 250-line cap by splitting its 21 functions into eight sourced libraries, while leaving what the tool actually does on this machine bit-for-bit unchanged.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/periodic_background/check_and_enable_services.sh — 1225 lines down to 202",
|
||||
"linux_configuration/scripts/periodic_background/lib/services_{common,wrappers,units,hosts,hosts_fix,apps,browser,report}.sh — new",
|
||||
"linux_configuration/scripts/periodic_background/lib/tests/{services_harness,test_services_common,test_services_report,test_services_units}.sh — new",
|
||||
"Non-goal: fixing the latent status-downgrade bug this work uncovered in the three unit checks",
|
||||
"Non-goal: making the 29 absolute root-owned probe paths redirectable, which the six path-gated libs would need for full coverage"
|
||||
],
|
||||
"changes": [
|
||||
"Moved all 21 top-level functions into eight libs grouped by subject (wrappers, systemd units, hosts stack, per-app blockers, browser/VM/user-service, reporting), leaving the entry script holding only configuration, argument parsing and the order the checks run in.",
|
||||
"Factored check_hosts' 107-line repair half into hosts_fix_nsswitch, hosts_fix_resolved and hosts_repair_all in services_hosts_fix.sh, because check_hosts alone was 260 lines and would have stayed over the cap. Repair order is preserved: resolver config first, then the hosts file, then the guard-lib migration. hosts_repair_all returns non-zero under --dry-run so a dry run can never promote a status to ok on the strength of repairs that did not happen.",
|
||||
"Replaced the write-only cross-file SERVICE_STATUS global with set_service_status / get_service_status in services_common.sh. This removes SC2034 in five libs without a suppression, and the setter rejects a duplicate write, since each check owns exactly one summary row and a second write would mean two checks silently fighting over it.",
|
||||
"Gave check_periodic_systems an early return when its status is already ok: it skips a report_and_fix call that had nothing to report, and is the genuine read of `status` that shellcheck cannot infer through report_and_fix's nameref.",
|
||||
"Added a shared test harness that shims systemctl, guardctl, lsattr, chattr, logger and sudo onto PATH and answers from a throwaway $DEV directory, so the tests assert what the checks tried to do rather than the state of this machine."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash meta/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "No linux_configuration/scripts/periodic_background entry remains. Entry script 202 lines; libs 74-189; largest new file services_wrappers.sh at 189."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/verify_shell_split.sh 6691cb26 check_and_enable_services.sh check_and_enable_services.sh lib/services_*.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Exactly one difference reported: check_hosts, whose hash changed because its repair half was deliberately extracted, plus the three new hosts_* helpers. The other 20 functions hash identically after shfmt minification."
|
||||
},
|
||||
{
|
||||
"command": "diff <(sudo bash <worktree@6691cb26>/…/check_and_enable_services.sh --status --dry-run) <(sudo bash …/check_and_enable_services.sh --status --dry-run)",
|
||||
"result": "pass",
|
||||
"evidence": "Byte-identical modulo the Date: line, run against the live machine. All 11 checks execute; summary reports pacman_wrapper/makepkg_wrapper/midnight_shutdown/startup_monitor/periodic_systems/hosts/guest_mode_removal ok, compulsive_blocker/leechblock/workout_locker warning, vbox_hosts n/a."
|
||||
},
|
||||
{
|
||||
"command": "lib/tests/test_services_common.sh; lib/tests/test_services_report.sh; lib/tests/test_services_units.sh",
|
||||
"result": "pass",
|
||||
"evidence": "46 passed / 0 failed, 11 passed / 0 failed, 15 passed / 0 failed — 72 assertions total."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/shell_coverage.sh lib/tests/test_services_common.sh services_common.sh 100",
|
||||
"result": "pass",
|
||||
"evidence": "services_common.sh: 54/54 lines = 100.00%"
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/shell_coverage.sh lib/tests/test_services_report.sh services_report.sh 100",
|
||||
"result": "pass",
|
||||
"evidence": "services_report.sh: 27/27 lines = 100.00%"
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/shell_coverage.sh lib/tests/test_services_units.sh services_units.sh 100",
|
||||
"result": "below target, recorded not waived",
|
||||
"evidence": "services_units.sh: 60/71 lines = 84.51%. The 11 uncovered lines (34, 35, 42, 43, 68, 78, 79, 86, 87, 144, 145) are the else-branches of [[ -f /etc/systemd/system/*.service ]] and [[ -f /usr/local/bin/*.sh ]] probes: absolute root-owned paths that exist on this machine and that no PATH shim can redirect. Reaching them needs the root-prefix rewrite recorded as out of scope."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck lib/tests/*.sh lib/services_*.sh check_and_enable_services.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Exit 0, zero findings, standalone (as the hook runs it, no -x) and again with -x. No suppression added anywhere."
|
||||
},
|
||||
{
|
||||
"command": "shfmt -d on all thirteen touched shell files",
|
||||
"result": "pass",
|
||||
"evidence": "Exit 0, no diff."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"check_and_enable_services.sh runs hourly as root and repairs the hosts blocking stack, so a behavioural regression here would silently disable enforcement. Mitigated by the byte-identical live-output diff against the pre-split revision, which is a stronger check than the test suite alone.",
|
||||
"check_hosts is the one function not proven verbatim. Its extracted repair half is only reached when a guard is already unhealthy — a state this machine is not currently in, so the live diff exercises the detection path but not the repair path. The repair path's ordering was preserved by inspection, not by execution.",
|
||||
"set_service_status now fails on a duplicate write. check_vbox_hosts writes vbox_hosts on two different paths; the early return on the not-installed path is what keeps them mutually exclusive. If a later edit removes that return, the second write becomes a visible error rather than a silent overwrite — noisy, but not a regression in enforcement.",
|
||||
"Six of the eight libs sit below 100% coverage because their branches are gated by absolute paths. The measured figures are recorded above rather than waived."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit: the split is additive (eight libs, four test files) plus one rewritten entry script, with no state or deployed artifact touched.",
|
||||
"After rollback, re-run sudo bash check_and_enable_services.sh --status --dry-run and confirm the same 11-row summary, since nothing outside the repo was modified in the first place."
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
119
linux_configuration/scripts/periodic_background/lib/services_apps.sh
Executable file
119
linux_configuration/scripts/periodic_background/lib/services_apps.sh
Executable file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_apps.sh — per-application blocking checks (messaging-app launch
|
||||
# wrappers, and the LeechBlock browser extension for the invoking user).
|
||||
#
|
||||
# Sourced by check_and_enable_services.sh.
|
||||
|
||||
check_compulsive_blocker() {
|
||||
header "Compulsive Opening Blocker"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check if main script is installed
|
||||
if [[ -f /usr/local/bin/block-compulsive-opening.sh ]]; then
|
||||
msg "Blocker script installed at /usr/local/bin/block-compulsive-opening.sh"
|
||||
else
|
||||
issues+=("block-compulsive-opening.sh not found in /usr/local/bin")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check if wrappers are installed for known apps
|
||||
local checked_any=false
|
||||
for app in beeper signal-desktop discord; do
|
||||
local wrapper_path="/usr/bin/$app"
|
||||
if [[ -f "${wrapper_path}.orig" ]] || [[ -L "$wrapper_path" ]]; then
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
msg "$app wrapper installed (original backed up)"
|
||||
checked_any=true
|
||||
fi
|
||||
elif command -v "$app" &>/dev/null; then
|
||||
issues+=("$app is installed but wrapper not applied")
|
||||
if [[ $status != "error" ]]; then status="warning"; fi
|
||||
checked_any=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $checked_any == false && $status == "ok" ]]; then
|
||||
note "No target apps (beeper, signal-desktop, discord) found on system"
|
||||
fi
|
||||
|
||||
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 "Installing compulsive opening blocker..."
|
||||
if [[ -f $COMPULSIVE_BLOCK_SCRIPT ]]; then
|
||||
run bash "$COMPULSIVE_BLOCK_SCRIPT" install
|
||||
((FIXES_APPLIED++)) || true
|
||||
else
|
||||
err_missing_script "Install script not found: $COMPULSIVE_BLOCK_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "compulsive_blocker" "$status"
|
||||
}
|
||||
|
||||
check_leechblock() {
|
||||
header "LeechBlock Browser Extension"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
local user="${SUDO_USER:-$USER}"
|
||||
local user_home
|
||||
user_home="/home/$user"
|
||||
|
||||
# Check if LeechBlock is installed for any browser
|
||||
local leechblock_dir="$user_home/.local/share/leechblockng"
|
||||
if [[ -d $leechblock_dir ]]; then
|
||||
msg "LeechBlock directory exists at $leechblock_dir"
|
||||
else
|
||||
issues+=("LeechBlock not found at $leechblock_dir")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check for browser wrappers with LeechBlock
|
||||
local found_wrapper=false
|
||||
for desktop_file in "$user_home/.local/share/applications/"*leechblock* "$user_home/.local/share/applications/"*LeechBlock*; do
|
||||
if [[ -f $desktop_file ]]; then
|
||||
msg "LeechBlock desktop entry found: $(basename "$desktop_file")"
|
||||
found_wrapper=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $found_wrapper == false && -d $leechblock_dir ]]; then
|
||||
issues+=("No LeechBlock desktop entries found")
|
||||
if [[ $status != "error" ]]; then status="warning"; fi
|
||||
fi
|
||||
|
||||
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 "Installing LeechBlock..."
|
||||
if [[ -f $LEECHBLOCK_SCRIPT ]]; then
|
||||
run sudo -u "$user" bash "$LEECHBLOCK_SCRIPT"
|
||||
((FIXES_APPLIED++)) || true
|
||||
else
|
||||
err_missing_script "Install script not found: $LEECHBLOCK_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "leechblock" "$status"
|
||||
}
|
||||
165
linux_configuration/scripts/periodic_background/lib/services_browser.sh
Executable file
165
linux_configuration/scripts/periodic_background/lib/services_browser.sh
Executable file
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_browser.sh — checks whose subject is a browser policy, a VM
|
||||
# hosts-enforcement marker, or the per-user workout locker service.
|
||||
#
|
||||
# Sourced by check_and_enable_services.sh. check_workout_locker queries a USER
|
||||
# unit from root and must go through user_systemctl (see its comment) rather
|
||||
# than `sudo -u ... systemctl --user`.
|
||||
|
||||
check_guest_mode_removal() {
|
||||
header "Chromium Guest Mode Removal"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check if managed policy files exist for any browser
|
||||
local policy_found=false
|
||||
for policy_dir in \
|
||||
/etc/chromium/policies/managed \
|
||||
/etc/opt/chrome/policies/managed \
|
||||
/etc/thorium/policies/managed \
|
||||
/etc/brave/policies/managed; do
|
||||
if [[ -d $policy_dir ]] && ls "$policy_dir"/*.json &>/dev/null 2>&1; then
|
||||
# Check for guest mode policy
|
||||
if grep -rl 'BrowserGuestModeEnabled' "$policy_dir" &>/dev/null 2>&1; then
|
||||
msg "Guest mode policy found in $policy_dir"
|
||||
policy_found=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $policy_found == false ]]; then
|
||||
# Only flag as issue if a Chromium browser is actually installed
|
||||
if command -v thorium-browser &>/dev/null || command -v chromium &>/dev/null || command -v google-chrome &>/dev/null || command -v brave-browser &>/dev/null; then
|
||||
issues+=("No guest mode removal policies found for installed browsers")
|
||||
status="error"
|
||||
else
|
||||
note "No Chromium-based browsers detected, skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $status != "ok" ]]; then
|
||||
for issue in "${issues[@]}"; do
|
||||
err "$issue"
|
||||
done
|
||||
((ISSUES_FOUND++)) || true
|
||||
|
||||
if [[ $STATUS_ONLY -eq 0 ]]; then
|
||||
note "Removing guest mode..."
|
||||
if [[ -f $REMOVE_GUEST_MODE_SCRIPT ]]; then
|
||||
run bash "$REMOVE_GUEST_MODE_SCRIPT"
|
||||
((FIXES_APPLIED++)) || true
|
||||
else
|
||||
err_missing_script "Script not found: $REMOVE_GUEST_MODE_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "guest_mode_removal" "$status"
|
||||
}
|
||||
|
||||
check_vbox_hosts() {
|
||||
header "VirtualBox Hosts Enforcement"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Only check if VirtualBox is installed.
|
||||
# This is NOT "skipped" in the sense of ducking a check: VirtualBox is absent
|
||||
# AND deliberately blocked by pacman policy — pacman_blocked_keywords.txt lists
|
||||
# virtualbox/vbox with the comment "can bypass /etc/hosts restrictions", which
|
||||
# is the very thing this check enforces. With no VirtualBox there are no VMs to
|
||||
# enforce /etc/hosts inside, so the check is genuinely not applicable. Report
|
||||
# n/a so it reads as deliberate rather than something that got ducked.
|
||||
if ! command -v VBoxManage &>/dev/null; then
|
||||
note "VirtualBox not installed (blocked by pacman policy) — not applicable"
|
||||
set_service_status "vbox_hosts" "n/a"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check if enforcement marker exists
|
||||
if [[ -f /var/lib/vbox-hosts-enforced ]]; then
|
||||
msg "VirtualBox hosts enforcement marker exists"
|
||||
else
|
||||
issues+=("VirtualBox hosts enforcement not applied")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
if [[ $status != "ok" ]]; then
|
||||
for issue in "${issues[@]}"; do
|
||||
err "$issue"
|
||||
done
|
||||
((ISSUES_FOUND++)) || true
|
||||
|
||||
if [[ $STATUS_ONLY -eq 0 ]]; then
|
||||
note "Enforcing hosts in VirtualBox VMs..."
|
||||
if [[ -f $VBOX_HOSTS_SCRIPT ]]; then
|
||||
run bash "$VBOX_HOSTS_SCRIPT"
|
||||
((FIXES_APPLIED++)) || true
|
||||
else
|
||||
err_missing_script "Script not found: $VBOX_HOSTS_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "vbox_hosts" "$status"
|
||||
}
|
||||
|
||||
check_workout_locker() {
|
||||
header "Workout Lock Screen"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
local user="${SUDO_USER:-$USER}"
|
||||
|
||||
# Check screen_lock.py script exists
|
||||
if [[ -f $WORKOUT_LOCKER_SCRIPT ]]; then
|
||||
msg "screen_lock.py exists at $WORKOUT_LOCKER_SCRIPT"
|
||||
else
|
||||
issues+=("screen_lock.py not found at $WORKOUT_LOCKER_SCRIPT")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check user service is enabled
|
||||
if user_systemctl "$user" is-enabled workout-locker.service &>/dev/null 2>&1; then
|
||||
msg "workout-locker.service is enabled (user service)"
|
||||
else
|
||||
issues+=("workout-locker.service is not enabled (user service)")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check user service is active (advisory — it only runs at login)
|
||||
if user_systemctl "$user" is-active workout-locker.service &>/dev/null 2>&1; then
|
||||
msg "workout-locker.service is active"
|
||||
else
|
||||
issues+=("workout-locker.service is not active (expected at login time)")
|
||||
if [[ $status != "error" ]]; then status="warning"; fi
|
||||
fi
|
||||
|
||||
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 "Installing workout locker service..."
|
||||
if [[ -f $WORKOUT_LOCKER_INSTALL_SCRIPT ]]; then
|
||||
run sudo -u "$user" bash "$WORKOUT_LOCKER_INSTALL_SCRIPT"
|
||||
((FIXES_APPLIED++)) || true
|
||||
# Re-verify
|
||||
if [[ $DRY_RUN -eq 0 ]] && user_systemctl "$user" is-enabled workout-locker.service &>/dev/null 2>&1; then
|
||||
status="ok"
|
||||
fi
|
||||
else
|
||||
err_missing_script "Install script not found: $WORKOUT_LOCKER_INSTALL_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "workout_locker" "$status"
|
||||
}
|
||||
173
linux_configuration/scripts/periodic_background/lib/services_common.sh
Executable file
173
linux_configuration/scripts/periodic_background/lib/services_common.sh
Executable file
@ -0,0 +1,173 @@
|
||||
#!/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
|
||||
######################################################################
|
||||
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 /etc/pacman.d/hooks/10-guard-lib-unlock-all.hook ]] &&
|
||||
[[ -f /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}"
|
||||
}
|
||||
169
linux_configuration/scripts/periodic_background/lib/services_hosts.sh
Executable file
169
linux_configuration/scripts/periodic_background/lib/services_hosts.sh
Executable file
@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_hosts.sh — the /etc/hosts blocking stack check: the hosts file
|
||||
# itself, its immutable attribute, the three guard-lib file-guard instances
|
||||
# (hosts/nsswitch/resolved), the pacman unlock/relock hooks, and the
|
||||
# systemd-resolved settings that can silently bypass /etc/hosts.
|
||||
#
|
||||
# Sourced by check_and_enable_services.sh. The repair half lives in
|
||||
# services_hosts_fix.sh so both files stay under the 250-line cap.
|
||||
|
||||
check_hosts() {
|
||||
header "Hosts File and Guards"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check /etc/hosts exists and has content
|
||||
if [[ -f /etc/hosts ]]; then
|
||||
local line_count
|
||||
line_count=$(wc -l </etc/hosts)
|
||||
if [[ $line_count -gt 100 ]]; then
|
||||
msg "/etc/hosts exists with $line_count lines (StevenBlack list likely installed)"
|
||||
else
|
||||
issues+=("/etc/hosts has only $line_count lines (StevenBlack list may not be installed)")
|
||||
status="warning"
|
||||
fi
|
||||
else
|
||||
issues+=("/etc/hosts does not exist")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check if hosts file is immutable
|
||||
local attrs
|
||||
attrs=$(lsattr /etc/hosts 2>/dev/null | cut -d' ' -f1 || echo "")
|
||||
if [[ $attrs == *"i"* ]]; then
|
||||
msg "/etc/hosts has immutable attribute set"
|
||||
else
|
||||
issues+=("/etc/hosts is not immutable")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check cached hosts file
|
||||
if [[ -f /etc/hosts.stevenblack ]]; then
|
||||
msg "StevenBlack cache exists at /etc/hosts.stevenblack"
|
||||
else
|
||||
issues+=("StevenBlack cache not found")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check the guard-lib "hosts" file-guard instance (path unit active +
|
||||
# immutable attribute on the target). Replaces the legacy hosts-guard.path
|
||||
# / hosts-bind-mount.service / enforce-hosts.sh checks now that this
|
||||
# machine's migration to guard-lib is complete — see
|
||||
# migrate_hosts_guard_to_guard_lib.sh.
|
||||
if guard_lib_instance_healthy hosts; then
|
||||
msg "guard-lib 'hosts' instance is active and enforced"
|
||||
else
|
||||
issues+=("guard-lib 'hosts' instance is missing or unhealthy")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check pacman hooks
|
||||
if hosts_pacman_hooks_installed; then
|
||||
msg "Pacman hooks installed"
|
||||
else
|
||||
issues+=("Pacman hooks not installed")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check nsswitch.conf has 'files' in hosts line
|
||||
if [[ -f /etc/nsswitch.conf ]]; then
|
||||
local nsswitch_hosts
|
||||
nsswitch_hosts=$(grep '^hosts:' /etc/nsswitch.conf 2>/dev/null || echo "")
|
||||
if echo "$nsswitch_hosts" | grep -qw 'files'; then
|
||||
msg "nsswitch.conf hosts line includes 'files'"
|
||||
else
|
||||
issues+=("nsswitch.conf hosts line missing 'files' — /etc/hosts is bypassed!")
|
||||
status="error"
|
||||
fi
|
||||
else
|
||||
issues+=("/etc/nsswitch.conf does not exist")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check the guard-lib "nsswitch" file-guard instance
|
||||
if guard_lib_instance_healthy nsswitch; then
|
||||
msg "guard-lib 'nsswitch' instance is active and enforced"
|
||||
else
|
||||
issues+=("guard-lib 'nsswitch' instance is missing or unhealthy")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check resolved.conf has ReadEtcHosts=yes
|
||||
if [[ -f /etc/systemd/resolved.conf ]]; then
|
||||
local read_etc_hosts
|
||||
read_etc_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' /etc/systemd/resolved.conf 2>/dev/null |
|
||||
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
|
||||
if [[ "$read_etc_hosts" == "yes" ]]; then
|
||||
msg "resolved.conf ReadEtcHosts=yes"
|
||||
else
|
||||
issues+=("resolved.conf ReadEtcHosts='$read_etc_hosts' — /etc/hosts is bypassed by systemd-resolved!")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check DNSOverTLS is not enabled
|
||||
local dns_over_tls
|
||||
dns_over_tls=$(grep -E '^\s*DNSOverTLS\s*=' /etc/systemd/resolved.conf 2>/dev/null |
|
||||
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') || true
|
||||
if [[ -z "$dns_over_tls" || "$dns_over_tls" == "no" ]]; then
|
||||
msg "resolved.conf DNSOverTLS is disabled"
|
||||
else
|
||||
issues+=("resolved.conf DNSOverTLS='$dns_over_tls' — can bypass /etc/hosts!")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check for drop-in overrides
|
||||
if [[ -d /etc/systemd/resolved.conf.d ]]; then
|
||||
local dropin_count
|
||||
dropin_count=$(find /etc/systemd/resolved.conf.d -name '*.conf' -type f 2>/dev/null | wc -l)
|
||||
if [[ "$dropin_count" -gt 0 ]]; then
|
||||
issues+=("Found $dropin_count resolved.conf drop-in override(s) — potential bypass!")
|
||||
status="error"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check immutable attribute
|
||||
if command -v lsattr &>/dev/null; then
|
||||
if lsattr /etc/systemd/resolved.conf 2>/dev/null | grep -q '.*i.*e.*'; then
|
||||
msg "resolved.conf has immutable attribute"
|
||||
else
|
||||
issues+=("resolved.conf missing immutable attribute")
|
||||
[[ "$status" == "ok" ]] && status="warning"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
issues+=("/etc/systemd/resolved.conf does not exist")
|
||||
[[ "$status" == "ok" ]] && status="warning"
|
||||
fi
|
||||
|
||||
# Check the guard-lib "resolved" file-guard instance
|
||||
if guard_lib_instance_healthy resolved; then
|
||||
msg "guard-lib 'resolved' instance is active and enforced"
|
||||
else
|
||||
issues+=("guard-lib 'resolved' instance is missing or unhealthy")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Report issues
|
||||
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 ]]; then
|
||||
# The repair half lives in services_hosts_fix.sh; it returns 0 only
|
||||
# when the post-repair re-verify passes, so a promotion to "ok" here
|
||||
# always reflects a re-checked machine rather than a fix attempt.
|
||||
if hosts_repair_all; then
|
||||
status="ok"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "hosts" "$status"
|
||||
}
|
||||
133
linux_configuration/scripts/periodic_background/lib/services_hosts_fix.sh
Executable file
133
linux_configuration/scripts/periodic_background/lib/services_hosts_fix.sh
Executable file
@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_hosts_fix.sh — the repair half of check_hosts.
|
||||
#
|
||||
# Split out of services_hosts.sh so both stay under the 250-line cap. The
|
||||
# detection half decides WHETHER to repair; everything here assumes that
|
||||
# decision is already made and only does the repairing.
|
||||
#
|
||||
# All three helpers go through `run`, so --dry-run prints instead of acting.
|
||||
# They increment FIXES_APPLIED and may call err_missing_script, exactly as they
|
||||
# did inline. hosts_repair_all returns 0 when the post-fix re-verify passes, so
|
||||
# the caller can promote its status to "ok" without reading a global.
|
||||
|
||||
# Put 'files' back on the nsswitch.conf hosts line. Without it every /etc/hosts
|
||||
# entry is bypassed outright, which makes this the highest-severity repair here
|
||||
# — the blocking stack is silently inert until it is fixed.
|
||||
hosts_fix_nsswitch() {
|
||||
[[ -f /etc/nsswitch.conf ]] || return 0
|
||||
local line
|
||||
line=$(grep '^hosts:' /etc/nsswitch.conf 2>/dev/null || echo "")
|
||||
[[ -n $line ]] || return 0
|
||||
if echo "$line" | grep -qw 'files'; then
|
||||
return 0
|
||||
fi
|
||||
note "Fixing nsswitch.conf — adding 'files' to hosts line..."
|
||||
# 'files' must precede whichever resolver is present, or the resolver
|
||||
# answers first and /etc/hosts never gets consulted.
|
||||
if echo "$line" | grep -qw 'resolve'; then
|
||||
run sed -i 's/^hosts:\(.*\)resolve/hosts: files\1resolve/' /etc/nsswitch.conf
|
||||
elif echo "$line" | grep -qw 'dns'; then
|
||||
run sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' /etc/nsswitch.conf
|
||||
else
|
||||
run sed -i 's/^hosts:/hosts: files/' /etc/nsswitch.conf
|
||||
fi
|
||||
((FIXES_APPLIED++)) || true
|
||||
msg "nsswitch.conf fixed: $(grep '^hosts:' /etc/nsswitch.conf)"
|
||||
}
|
||||
|
||||
# Repair the three systemd-resolved settings that can bypass /etc/hosts:
|
||||
# ReadEtcHosts=no, DNSOverTLS enabled, and drop-in overrides of either. The
|
||||
# file carries the immutable attribute, so each edit is bracketed by chattr.
|
||||
hosts_fix_resolved() {
|
||||
[[ -f /etc/systemd/resolved.conf ]] || return 0
|
||||
local read_etc_hosts
|
||||
read_etc_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' /etc/systemd/resolved.conf 2>/dev/null |
|
||||
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
|
||||
if [[ "$read_etc_hosts" != "yes" ]]; then
|
||||
note "Fixing resolved.conf — setting ReadEtcHosts=yes..."
|
||||
chattr -i /etc/systemd/resolved.conf 2>/dev/null || true
|
||||
if grep -qE '^\s*ReadEtcHosts\s*=' /etc/systemd/resolved.conf; then
|
||||
run sed -i -E 's/^\s*ReadEtcHosts\s*=.*/ReadEtcHosts=yes/' /etc/systemd/resolved.conf
|
||||
elif grep -q '^\[Resolve\]' /etc/systemd/resolved.conf; then
|
||||
run sed -i '/^\[Resolve\]/a ReadEtcHosts=yes' /etc/systemd/resolved.conf
|
||||
else
|
||||
printf '\n[Resolve]\nReadEtcHosts=yes\n' >>/etc/systemd/resolved.conf
|
||||
fi
|
||||
chattr +i /etc/systemd/resolved.conf 2>/dev/null || true
|
||||
run systemctl restart systemd-resolved
|
||||
((FIXES_APPLIED++)) || true
|
||||
msg "resolved.conf ReadEtcHosts fixed"
|
||||
fi
|
||||
|
||||
local dns_over_tls
|
||||
dns_over_tls=$(grep -E '^\s*DNSOverTLS\s*=' /etc/systemd/resolved.conf 2>/dev/null |
|
||||
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') || true
|
||||
if [[ -n "$dns_over_tls" && "$dns_over_tls" != "no" ]]; then
|
||||
note "Fixing resolved.conf — disabling DNSOverTLS..."
|
||||
chattr -i /etc/systemd/resolved.conf 2>/dev/null || true
|
||||
run sed -i -E 's/^\s*DNSOverTLS\s*=.*/#DNSOverTLS=no/' /etc/systemd/resolved.conf
|
||||
chattr +i /etc/systemd/resolved.conf 2>/dev/null || true
|
||||
run systemctl restart systemd-resolved
|
||||
((FIXES_APPLIED++)) || true
|
||||
msg "resolved.conf DNSOverTLS disabled"
|
||||
fi
|
||||
|
||||
# A drop-in can re-enable either setting without touching resolved.conf, so
|
||||
# the two repairs above are not durable while any drop-in survives.
|
||||
if [[ -d /etc/systemd/resolved.conf.d ]]; then
|
||||
local dropin_count
|
||||
dropin_count=$(find /etc/systemd/resolved.conf.d -name '*.conf' -type f 2>/dev/null | wc -l)
|
||||
if [[ "$dropin_count" -gt 0 ]]; then
|
||||
note "Removing $dropin_count resolved.conf drop-in override(s)..."
|
||||
chattr -i /etc/systemd/resolved.conf.d 2>/dev/null || true
|
||||
find /etc/systemd/resolved.conf.d -name '*.conf' -type f -delete
|
||||
chattr +i /etc/systemd/resolved.conf.d 2>/dev/null || true
|
||||
run systemctl restart systemd-resolved
|
||||
((FIXES_APPLIED++)) || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Reinstall the hosts file and re-run the guard-lib migration when needed, then
|
||||
# re-verify. Returns 0 only if all four guard-lib facts hold afterwards, which
|
||||
# is what lets the caller promote its status to "ok".
|
||||
#
|
||||
# Under --dry-run nothing was actually changed, so re-verifying would report on
|
||||
# the unrepaired machine; return 1 there instead of claiming a fix landed.
|
||||
hosts_repair_all() {
|
||||
# Order matters and matches the pre-split script: resolver config first, so
|
||||
# that a hosts file installed below is actually consulted once it lands.
|
||||
hosts_fix_nsswitch
|
||||
hosts_fix_resolved
|
||||
|
||||
if [[ ! -f /etc/hosts ]] || [[ $(wc -l </etc/hosts) -lt 100 ]]; then
|
||||
note "Installing hosts file..."
|
||||
if [[ -f $HOSTS_INSTALL_SCRIPT ]]; then
|
||||
run bash "$HOSTS_INSTALL_SCRIPT"
|
||||
((FIXES_APPLIED++)) || true
|
||||
else
|
||||
err_missing_script "Hosts install script not found: $HOSTS_INSTALL_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# The migration script documents itself as idempotent, so re-running it
|
||||
# against an already-migrated healthy machine is a no-op.
|
||||
if ! guard_lib_instance_healthy hosts ||
|
||||
! guard_lib_instance_healthy nsswitch ||
|
||||
! guard_lib_instance_healthy resolved ||
|
||||
! hosts_pacman_hooks_installed; then
|
||||
note "Repairing guard-lib hosts/nsswitch/resolved instances..."
|
||||
if [[ -f $GUARD_LIB_MIGRATE_SCRIPT ]]; then
|
||||
run bash "$GUARD_LIB_MIGRATE_SCRIPT"
|
||||
((FIXES_APPLIED++)) || true
|
||||
else
|
||||
err_missing_script "Guard-lib migration script not found: $GUARD_LIB_MIGRATE_SCRIPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ $DRY_RUN -eq 0 ]] || return 1
|
||||
guard_lib_instance_healthy hosts &&
|
||||
guard_lib_instance_healthy nsswitch &&
|
||||
guard_lib_instance_healthy resolved &&
|
||||
hosts_pacman_hooks_installed
|
||||
}
|
||||
74
linux_configuration/scripts/periodic_background/lib/services_report.sh
Executable file
74
linux_configuration/scripts/periodic_background/lib/services_report.sh
Executable file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_report.sh — usage text and the end-of-run status summary table.
|
||||
#
|
||||
# Sourced by check_and_enable_services.sh. Reads SERVICE_STATUS, ISSUES_FOUND,
|
||||
# FIXES_APPLIED, DRY_RUN, STATUS_ONLY and the colour globals from the caller.
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Check and Enable Digital Wellbeing Services
|
||||
============================================
|
||||
|
||||
Usage: sudo ./check_and_enable_services.sh [options]
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be done without making changes
|
||||
--status Only show status, don't enable anything
|
||||
-h, --help Show this help message
|
||||
|
||||
Services checked:
|
||||
1. Pacman wrapper - Policy-aware pacman wrapper with friction mechanics
|
||||
2. Midnight shutdown - Day-specific automatic shutdown timer
|
||||
3. Startup monitor - PC startup time monitoring service
|
||||
4. Periodic systems - Hourly maintenance timer and hosts monitor
|
||||
5. Hosts and guards - /etc/hosts blocking and protection layers
|
||||
6. Compulsive blocker - Limits messaging apps to one launch per hour
|
||||
7. Thorium startup - Auto-launch Thorium with Fitatu on boot
|
||||
8. LeechBlock - Browser extension for site blocking
|
||||
9. Guest mode removal - Disable Chromium guest mode via policy
|
||||
10. VirtualBox hosts - Enforce /etc/hosts inside VMs
|
||||
11. Workout lock screen - Requires workout logging to unlock screen (user service)
|
||||
EOF
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
header "Summary"
|
||||
|
||||
echo ""
|
||||
printf "%-25s %s\n" "Service" "Status"
|
||||
printf "%-25s %s\n" "-------" "------"
|
||||
|
||||
for service in pacman_wrapper makepkg_wrapper midnight_shutdown startup_monitor periodic_systems hosts compulsive_blocker leechblock guest_mode_removal vbox_hosts workout_locker; do
|
||||
local status
|
||||
status="$(get_service_status "$service")"
|
||||
local color
|
||||
case "$status" in
|
||||
ok) color=$GREEN ;;
|
||||
warning) color=$YELLOW ;;
|
||||
error) color=$RED ;;
|
||||
"n/a" | skipped) color=$BLUE ;;
|
||||
*) color=$NC ;;
|
||||
esac
|
||||
printf "%-25s ${color}%s${NC}\n" "$service" "$status"
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
note "DRY RUN - No changes were made"
|
||||
fi
|
||||
|
||||
if [[ $ISSUES_FOUND -eq 0 ]]; then
|
||||
msg "All services are properly configured!"
|
||||
else
|
||||
if [[ $STATUS_ONLY -eq 1 ]]; then
|
||||
warn "Found $ISSUES_FOUND service(s) with issues"
|
||||
note "Run without --status to fix issues"
|
||||
else
|
||||
if [[ $FIXES_APPLIED -gt 0 ]]; then
|
||||
msg "Applied $FIXES_APPLIED fix(es)"
|
||||
else
|
||||
warn "Found $ISSUES_FOUND issue(s) but no fixes were applied"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
162
linux_configuration/scripts/periodic_background/lib/services_units.sh
Executable file
162
linux_configuration/scripts/periodic_background/lib/services_units.sh
Executable file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_units.sh — systemd timer/service checks for the three units that
|
||||
# are installed by their own setup scripts (midnight shutdown, PC startup
|
||||
# monitor, periodic maintenance).
|
||||
#
|
||||
# Sourced by check_and_enable_services.sh. All three funnel their findings
|
||||
# through report_and_fix, which owns the reinstall-and-reverify path.
|
||||
|
||||
check_midnight_shutdown() {
|
||||
header "Midnight Shutdown (Day-Specific Auto-Shutdown)"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check timer
|
||||
if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then
|
||||
msg "day-specific-shutdown.timer is enabled"
|
||||
else
|
||||
issues+=("day-specific-shutdown.timer is not enabled")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
if systemctl is-active day-specific-shutdown.timer &>/dev/null; then
|
||||
msg "day-specific-shutdown.timer is active"
|
||||
else
|
||||
issues+=("day-specific-shutdown.timer is not active")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check service file exists
|
||||
if [[ -f /etc/systemd/system/day-specific-shutdown.service ]]; then
|
||||
msg "day-specific-shutdown.service file exists"
|
||||
else
|
||||
issues+=("day-specific-shutdown.service file missing")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check management script
|
||||
if [[ -f /usr/local/bin/day-specific-shutdown-manager.sh ]]; then
|
||||
msg "Shutdown manager script exists"
|
||||
else
|
||||
issues+=("day-specific-shutdown-manager.sh not found")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
report_and_fix issues status "midnight_shutdown" \
|
||||
"Setting up midnight shutdown..." \
|
||||
"$MIDNIGHT_SHUTDOWN_SCRIPT" \
|
||||
"day-specific-shutdown.timer" \
|
||||
enable
|
||||
}
|
||||
|
||||
check_startup_monitor() {
|
||||
header "PC Startup Monitor"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check timer (the timer triggers the service, so we check the timer)
|
||||
if systemctl is-enabled pc-startup-monitor.timer &>/dev/null; then
|
||||
msg "pc-startup-monitor.timer is enabled"
|
||||
else
|
||||
issues+=("pc-startup-monitor.timer is not enabled")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
if systemctl is-active pc-startup-monitor.timer &>/dev/null; then
|
||||
msg "pc-startup-monitor.timer is active"
|
||||
else
|
||||
issues+=("pc-startup-monitor.timer is not active")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check service file exists
|
||||
if [[ -f /etc/systemd/system/pc-startup-monitor.service ]]; then
|
||||
msg "pc-startup-monitor.service file exists"
|
||||
else
|
||||
issues+=("pc-startup-monitor.service file missing")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check monitor script
|
||||
if [[ -f /usr/local/bin/pc-startup-check.sh ]]; then
|
||||
msg "Startup check script exists"
|
||||
else
|
||||
issues+=("pc-startup-check.sh not found")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
report_and_fix issues status "startup_monitor" \
|
||||
"Setting up startup monitor..." \
|
||||
"$STARTUP_MONITOR_SCRIPT" \
|
||||
"pc-startup-monitor.timer"
|
||||
}
|
||||
|
||||
check_periodic_systems() {
|
||||
header "Periodic System Maintenance"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check timer
|
||||
if systemctl is-enabled periodic-system-maintenance.timer &>/dev/null; then
|
||||
msg "periodic-system-maintenance.timer is enabled"
|
||||
else
|
||||
issues+=("periodic-system-maintenance.timer is not enabled")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
if systemctl is-active periodic-system-maintenance.timer &>/dev/null; then
|
||||
msg "periodic-system-maintenance.timer is active"
|
||||
else
|
||||
issues+=("periodic-system-maintenance.timer is not active")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check startup service
|
||||
if systemctl is-enabled periodic-system-startup.service &>/dev/null; then
|
||||
msg "periodic-system-startup.service is enabled"
|
||||
else
|
||||
issues+=("periodic-system-startup.service is not enabled")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check hosts file monitor
|
||||
if systemctl is-enabled hosts-file-monitor.service &>/dev/null; then
|
||||
msg "hosts-file-monitor.service is enabled"
|
||||
else
|
||||
issues+=("hosts-file-monitor.service is not enabled")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
if systemctl is-active hosts-file-monitor.service &>/dev/null; then
|
||||
msg "hosts-file-monitor.service is active"
|
||||
else
|
||||
issues+=("hosts-file-monitor.service is not active")
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Check maintenance script
|
||||
if [[ -f /usr/local/bin/periodic-system-maintenance.sh ]]; then
|
||||
msg "Maintenance script exists"
|
||||
else
|
||||
issues+=("periodic-system-maintenance.sh not found")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# report_and_fix takes `status` by nameref and both reads and rewrites it,
|
||||
# so passing the bare name is the whole interface. shellcheck cannot see
|
||||
# through a nameref, though, and standalone it reports the assignments
|
||||
# above as write-only (SC2034). This early return is a genuine read: an
|
||||
# "ok" service has nothing to report and nothing to fix, so skipping the
|
||||
# call is also marginally cheaper than having it decide the same thing.
|
||||
if [[ $status == "ok" ]]; then
|
||||
set_service_status "periodic_systems" "ok"
|
||||
return 0
|
||||
fi
|
||||
report_and_fix issues status "periodic_systems" \
|
||||
"Setting up periodic systems..." \
|
||||
"$PERIODIC_SYSTEM_SCRIPT" \
|
||||
"periodic-system-maintenance.timer"
|
||||
}
|
||||
189
linux_configuration/scripts/periodic_background/lib/services_wrappers.sh
Executable file
189
linux_configuration/scripts/periodic_background/lib/services_wrappers.sh
Executable file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/services_wrappers.sh — pacman/makepkg wrapper deployment checks.
|
||||
#
|
||||
# Sourced by check_and_enable_services.sh. Each check verifies the symlink, the
|
||||
# .orig backup, the installed files AND the install-time drift manifest, then
|
||||
# reinstalls via the matching installer when STATUS_ONLY is off.
|
||||
|
||||
check_pacman_wrapper() {
|
||||
header "Pacman Wrapper"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Check if wrapper is installed
|
||||
if [[ -L /usr/bin/pacman ]]; then
|
||||
local target
|
||||
target=$(readlink -f /usr/bin/pacman)
|
||||
if [[ $target == "/usr/local/bin/pacman_wrapper" ]]; then
|
||||
msg "Pacman symlink points to wrapper"
|
||||
else
|
||||
issues+=("Pacman symlink points to: $target (expected /usr/local/bin/pacman_wrapper)")
|
||||
status="error"
|
||||
fi
|
||||
else
|
||||
issues+=("Pacman is not a symlink (wrapper not installed)")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check if original pacman is backed up
|
||||
if [[ -f /usr/bin/pacman.orig ]]; then
|
||||
msg "Original pacman backed up at /usr/bin/pacman.orig"
|
||||
else
|
||||
issues+=("Original pacman backup not found at /usr/bin/pacman.orig")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check if wrapper script exists
|
||||
if [[ -f /usr/local/bin/pacman_wrapper ]]; then
|
||||
msg "Wrapper script exists at /usr/local/bin/pacman_wrapper"
|
||||
else
|
||||
issues+=("Wrapper script not found at /usr/local/bin/pacman_wrapper")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Check supporting files
|
||||
for file in words.txt pacman_blocked_keywords.txt pacman_whitelist.txt; do
|
||||
if [[ -f "/usr/local/bin/$file" ]]; then
|
||||
msg "Supporting file exists: /usr/local/bin/$file"
|
||||
else
|
||||
warn "Supporting file missing: /usr/local/bin/$file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Content drift — the check everything above misses. A correct symlink and a
|
||||
# present file say nothing about WHICH version is deployed.
|
||||
local drift_rc=0
|
||||
deployment_drift "$PACMAN_WRAPPER_MANIFEST" || drift_rc=$?
|
||||
case $drift_rc in
|
||||
0) msg "Wrapper matches the source it was installed from (no drift)" ;;
|
||||
1)
|
||||
issues+=("Deployed pacman wrapper differs from its install manifest (stale or tampered)")
|
||||
status="error"
|
||||
;;
|
||||
2)
|
||||
issues+=("No drift manifest at $PACMAN_WRAPPER_MANIFEST — deployed version unverifiable")
|
||||
status="error"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Report and fix
|
||||
if [[ $status == "error" ]]; then
|
||||
for issue in "${issues[@]}"; do
|
||||
err "$issue"
|
||||
done
|
||||
((ISSUES_FOUND++)) || true
|
||||
|
||||
if [[ $STATUS_ONLY -eq 0 ]]; then
|
||||
note "Installing pacman wrapper..."
|
||||
if [[ -f $PACMAN_WRAPPER_INSTALL ]]; then
|
||||
run bash "$PACMAN_WRAPPER_INSTALL"
|
||||
((FIXES_APPLIED++)) || true
|
||||
# Re-verify after fix. Content is part of "fixed": a reinstall
|
||||
# that leaves the manifest failing has repaired nothing.
|
||||
if [[ $DRY_RUN -eq 0 ]] && [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]] &&
|
||||
[[ -f /usr/local/bin/pacman_wrapper ]] && deployment_drift "$PACMAN_WRAPPER_MANIFEST"; then
|
||||
status="ok"
|
||||
fi
|
||||
else
|
||||
err_missing_script "Installer script not found: $PACMAN_WRAPPER_INSTALL"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "pacman_wrapper" "$status"
|
||||
}
|
||||
|
||||
check_makepkg_wrapper() {
|
||||
header "Makepkg Wrapper"
|
||||
|
||||
local status="ok"
|
||||
local issues=()
|
||||
|
||||
# Wrapper symlink
|
||||
if [[ -L /usr/bin/makepkg ]]; then
|
||||
local target
|
||||
target=$(readlink -f /usr/bin/makepkg)
|
||||
if [[ $target == "/usr/local/bin/makepkg_wrapper" ]]; then
|
||||
msg "Makepkg symlink points to wrapper"
|
||||
else
|
||||
issues+=("Makepkg symlink points to: $target (expected /usr/local/bin/makepkg_wrapper)")
|
||||
status="error"
|
||||
fi
|
||||
else
|
||||
issues+=("Makepkg is not a symlink (wrapper not installed)")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Original makepkg backup
|
||||
if [[ -f /usr/bin/makepkg.orig ]]; then
|
||||
msg "Original makepkg backed up at /usr/bin/makepkg.orig"
|
||||
else
|
||||
issues+=("Original makepkg backup not found at /usr/bin/makepkg.orig")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Wrapper, shared lib, rewrap helper, survival hook
|
||||
if [[ -f /usr/local/bin/makepkg_wrapper ]]; then
|
||||
msg "Wrapper script exists at /usr/local/bin/makepkg_wrapper"
|
||||
else
|
||||
issues+=("Wrapper script not found at /usr/local/bin/makepkg_wrapper")
|
||||
status="error"
|
||||
fi
|
||||
if [[ -f /usr/local/bin/pacman_lock_lib.sh ]]; then
|
||||
msg "Shared lock library exists at /usr/local/bin/pacman_lock_lib.sh"
|
||||
else
|
||||
issues+=("Shared lock library not found at /usr/local/bin/pacman_lock_lib.sh")
|
||||
status="error"
|
||||
fi
|
||||
if [[ -f /usr/local/bin/rewrap_pkg_managers.sh ]]; then
|
||||
msg "Rewrap helper exists at /usr/local/bin/rewrap_pkg_managers.sh"
|
||||
else
|
||||
issues+=("Rewrap helper not found at /usr/local/bin/rewrap_pkg_managers.sh")
|
||||
status="error"
|
||||
fi
|
||||
if [[ -f /etc/pacman.d/hooks/96-restore-pkg-wrappers.hook ]]; then
|
||||
msg "Upgrade-survival hook installed"
|
||||
else
|
||||
issues+=("Upgrade-survival hook not installed at /etc/pacman.d/hooks/96-restore-pkg-wrappers.hook")
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# Content drift (see check_pacman_wrapper for why existence is not enough)
|
||||
local drift_rc=0
|
||||
deployment_drift "$MAKEPKG_WRAPPER_MANIFEST" || drift_rc=$?
|
||||
case $drift_rc in
|
||||
0) msg "Makepkg wrapper matches the source it was installed from (no drift)" ;;
|
||||
1)
|
||||
issues+=("Deployed makepkg wrapper differs from its install manifest (stale or tampered)")
|
||||
status="error"
|
||||
;;
|
||||
2)
|
||||
issues+=("No drift manifest at $MAKEPKG_WRAPPER_MANIFEST — deployed version unverifiable")
|
||||
status="error"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Report and fix
|
||||
if [[ $status == "error" ]]; then
|
||||
for issue in "${issues[@]}"; do
|
||||
err "$issue"
|
||||
done
|
||||
((ISSUES_FOUND++)) || true
|
||||
|
||||
if [[ $STATUS_ONLY -eq 0 ]]; then
|
||||
note "Installing makepkg wrapper..."
|
||||
if [[ -f $MAKEPKG_WRAPPER_INSTALL ]]; then
|
||||
run bash "$MAKEPKG_WRAPPER_INSTALL"
|
||||
((FIXES_APPLIED++)) || true
|
||||
if [[ $DRY_RUN -eq 0 ]] && [[ -L /usr/bin/makepkg ]] && [[ -f /usr/bin/makepkg.orig ]] && [[ -f /usr/local/bin/makepkg_wrapper ]]; then
|
||||
status="ok"
|
||||
fi
|
||||
else
|
||||
err_missing_script "Installer script not found: $MAKEPKG_WRAPPER_INSTALL"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
set_service_status "makepkg_wrapper" "$status"
|
||||
}
|
||||
238
linux_configuration/scripts/periodic_background/lib/tests/services_harness.sh
Executable file
238
linux_configuration/scripts/periodic_background/lib/tests/services_harness.sh
Executable file
@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/services_harness.sh — shared fixture for the check_and_enable_services
|
||||
# split.
|
||||
#
|
||||
# Sourced, not executed. Every command the checks reach for — systemctl,
|
||||
# guardctl, lsattr, chattr, logger, sudo, find, wc — is a PATH shim recording
|
||||
# its invocation into $DEV/calls, so a test asserts on what the code TRIED to
|
||||
# do rather than on the state of this machine. Nothing here touches a real unit,
|
||||
# a real /etc file, or a real installer.
|
||||
#
|
||||
# The paths the checks probe (/usr/bin/pacman, /etc/hosts, ...) are absolute and
|
||||
# cannot be redirected, so the shims answer questions about them from files
|
||||
# under $DEV instead: a test declares "pretend /etc/hosts has 200 lines" by
|
||||
# writing $DEV/etc_hosts_lines rather than by creating the file.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LIB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_t_pass() {
|
||||
PASS=$((PASS + 1))
|
||||
printf ' OK: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
printf ' FAIL: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_eq() {
|
||||
local want="$1"
|
||||
local got="$2"
|
||||
local what="$3"
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want '${want}', got '${got}')"
|
||||
fi
|
||||
}
|
||||
|
||||
# Assert $DEV/calls contains a line matching a regex.
|
||||
_t_called() {
|
||||
local pattern="$1"
|
||||
local what="$2"
|
||||
if grep -qE "$pattern" "${DEV}/calls" 2>/dev/null; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (no call matching /${pattern}/)"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_not_called() {
|
||||
local pattern="$1"
|
||||
local what="$2"
|
||||
if grep -qE "$pattern" "${DEV}/calls" 2>/dev/null; then
|
||||
_t_fail "$what (unexpected call matching /${pattern}/)"
|
||||
else
|
||||
_t_pass "$what"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_summary() {
|
||||
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASS" "$FAIL"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
}
|
||||
|
||||
TEST_TMPDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TEST_TMPDIR}"' EXIT
|
||||
|
||||
readonly DEV="${TEST_TMPDIR}/device"
|
||||
readonly FAKE_BIN="${TEST_TMPDIR}/fake_bin"
|
||||
mkdir -p "${DEV}" "${FAKE_BIN}"
|
||||
|
||||
# --- fake external tools ----------------------------------------------------
|
||||
|
||||
# systemctl: `is-enabled`/`is-active <unit>` succeed only when $DEV/enabled or
|
||||
# $DEV/active lists the unit. Every other subcommand (restart, ...) just
|
||||
# records. `--user --machine=...` is the user_systemctl shape and is answered
|
||||
# from the same two files, prefixed "user:".
|
||||
cat >"${FAKE_BIN}/systemctl" <<'SYSTEMCTLSHIM'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DEV="${SERVICES_TEST_DEV}"
|
||||
printf '%s\n' "systemctl $*" >>"${DEV}/calls"
|
||||
prefix=""
|
||||
args=()
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--user) prefix="user:" ;;
|
||||
--machine=*) ;;
|
||||
*) args+=("$a") ;;
|
||||
esac
|
||||
done
|
||||
verb="${args[0]:-}"
|
||||
unit="${args[1]:-}"
|
||||
case "$verb" in
|
||||
is-enabled) grep -qxF "${prefix}${unit}" "${DEV}/enabled" 2>/dev/null ;;
|
||||
is-active) grep -qxF "${prefix}${unit}" "${DEV}/active" 2>/dev/null ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
SYSTEMCTLSHIM
|
||||
chmod +x "${FAKE_BIN}/systemctl"
|
||||
|
||||
# guardctl file-guard status <name>: prints the two lines
|
||||
# guard_lib_instance_healthy greps for, but only for instances listed in
|
||||
# $DEV/guard_healthy. An instance in $DEV/guard_degraded prints a status whose
|
||||
# path unit is inactive; anything else exits 1 (unregistered).
|
||||
cat >"${FAKE_BIN}/guardctl" <<'GUARDCTLSHIM'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DEV="${SERVICES_TEST_DEV}"
|
||||
printf '%s\n' "guardctl $*" >>"${DEV}/calls"
|
||||
name="${3:-}"
|
||||
if grep -qxF "$name" "${DEV}/guard_healthy" 2>/dev/null; then
|
||||
printf 'path unit: active\ntarget attrs: ----i---------e---- /etc/%s\n' "$name"
|
||||
exit 0
|
||||
fi
|
||||
if grep -qxF "$name" "${DEV}/guard_degraded" 2>/dev/null; then
|
||||
printf 'path unit: inactive\ntarget attrs: ------------------ /etc/%s\n' "$name"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
GUARDCTLSHIM
|
||||
chmod +x "${FAKE_BIN}/guardctl"
|
||||
|
||||
# lsattr: prints an attribute string for paths listed in $DEV/immutable,
|
||||
# otherwise the all-dashes "no attributes" form.
|
||||
cat >"${FAKE_BIN}/lsattr" <<'LSATTRSHIM'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DEV="${SERVICES_TEST_DEV}"
|
||||
target="${1:-}"
|
||||
if grep -qxF "$target" "${DEV}/immutable" 2>/dev/null; then
|
||||
printf '----i---------e---- %s\n' "$target"
|
||||
else
|
||||
printf -- '------------------e---- %s\n' "$target"
|
||||
fi
|
||||
exit 0
|
||||
LSATTRSHIM
|
||||
chmod +x "${FAKE_BIN}/lsattr"
|
||||
|
||||
for tool in chattr logger sudo systemd-resolved; do
|
||||
cat >"${FAKE_BIN}/${tool}" <<SIMPLESHIM
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "${tool} \$*" >>"\${SERVICES_TEST_DEV}/calls"
|
||||
exit 0
|
||||
SIMPLESHIM
|
||||
chmod +x "${FAKE_BIN}/${tool}"
|
||||
done
|
||||
|
||||
# `command -v <browser>` decides whether the browser/VBox checks apply. bash's
|
||||
# builtin `command` cannot be shimmed on PATH, so the tests instead create the
|
||||
# fake executables themselves under $FAKE_BIN via present_command below.
|
||||
present_command() { # <name>...
|
||||
local name
|
||||
for name in "$@"; do
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' >"${FAKE_BIN}/${name}"
|
||||
chmod +x "${FAKE_BIN}/${name}"
|
||||
done
|
||||
}
|
||||
|
||||
absent_command() { # <name>...
|
||||
local name
|
||||
for name in "$@"; do
|
||||
rm -f "${FAKE_BIN}/${name}"
|
||||
done
|
||||
}
|
||||
|
||||
export SERVICES_TEST_DEV="${DEV}"
|
||||
export PATH="${FAKE_BIN}:${PATH}"
|
||||
|
||||
# --- globals the libs read --------------------------------------------------
|
||||
|
||||
RED=$'\033[0;31m'
|
||||
GREEN=$'\033[0;32m'
|
||||
YELLOW=$'\033[0;33m'
|
||||
BLUE=$'\033[0;34m'
|
||||
CYAN=$'\033[0;36m'
|
||||
NC=$'\033[0m'
|
||||
|
||||
DRY_RUN=0
|
||||
STATUS_ONLY=0
|
||||
ISSUES_FOUND=0
|
||||
FIXES_APPLIED=0
|
||||
declare -A SERVICE_STATUS=()
|
||||
declare -a MISSING_SCRIPTS=()
|
||||
|
||||
# Installer paths point into the tmpdir so a test controls whether the repair
|
||||
# path finds its script (present) or reports it missing (absent).
|
||||
#
|
||||
# Exported because the check libs that read them are sourced by the individual
|
||||
# test files rather than by this harness, so standalone shellcheck sees only
|
||||
# the write here and reports SC2034. They genuinely cross a process boundary
|
||||
# too (the require_root probe runs as a child), so export is the accurate
|
||||
# declaration rather than a way to quiet the warning.
|
||||
export PACMAN_WRAPPER_INSTALL="${TEST_TMPDIR}/install_pacman_wrapper.sh"
|
||||
export MAKEPKG_WRAPPER_INSTALL="${TEST_TMPDIR}/install_makepkg_wrapper.sh"
|
||||
export PACMAN_WRAPPER_MANIFEST="${TEST_TMPDIR}/pacman-source.sha256"
|
||||
export MAKEPKG_WRAPPER_MANIFEST="${TEST_TMPDIR}/makepkg-source.sha256"
|
||||
export MIDNIGHT_SHUTDOWN_SCRIPT="${TEST_TMPDIR}/setup_midnight_shutdown.sh"
|
||||
export STARTUP_MONITOR_SCRIPT="${TEST_TMPDIR}/setup_pc_startup_monitor.sh"
|
||||
export PERIODIC_SYSTEM_SCRIPT="${TEST_TMPDIR}/setup_periodic_system.sh"
|
||||
export HOSTS_INSTALL_SCRIPT="${TEST_TMPDIR}/hosts_install.sh"
|
||||
export GUARD_LIB_MIGRATE_SCRIPT="${TEST_TMPDIR}/migrate_hosts_guard.sh"
|
||||
export COMPULSIVE_BLOCK_SCRIPT="${TEST_TMPDIR}/block_compulsive_opening.sh"
|
||||
export LEECHBLOCK_SCRIPT="${TEST_TMPDIR}/install_leechblock.sh"
|
||||
export REMOVE_GUEST_MODE_SCRIPT="${TEST_TMPDIR}/remove_guest_mode.sh"
|
||||
export VBOX_HOSTS_SCRIPT="${TEST_TMPDIR}/enforce_vbox_hosts.sh"
|
||||
export WORKOUT_LOCKER_INSTALL_SCRIPT="${TEST_TMPDIR}/install_systemd.sh"
|
||||
export WORKOUT_LOCKER_SCRIPT="${TEST_TMPDIR}/screen_lock.py"
|
||||
|
||||
# Create an installer stub that records being run, so "did the repair path fire"
|
||||
# is observable without any real installation happening.
|
||||
make_installer() { # <path>
|
||||
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "ran %s $*" >>"%s/calls"\nexit 0\n' \
|
||||
"$(basename "$1")" "${DEV}" >"$1"
|
||||
chmod +x "$1"
|
||||
}
|
||||
|
||||
# shellcheck source=../services_common.sh
|
||||
. "${LIB_DIR}/services_common.sh"
|
||||
|
||||
# reset_state — return every mutable global and every $DEV fact to "nothing has
|
||||
# happened yet", so each test group starts from a known machine.
|
||||
reset_state() {
|
||||
rm -f "${DEV:?}"/* 2>/dev/null || true
|
||||
: >"${DEV}/calls"
|
||||
DRY_RUN=0
|
||||
STATUS_ONLY=0
|
||||
ISSUES_FOUND=0
|
||||
FIXES_APPLIED=0
|
||||
SERVICE_STATUS=()
|
||||
MISSING_SCRIPTS=()
|
||||
}
|
||||
reset_state
|
||||
@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for lib/services_common.sh — output helpers, the status store, the
|
||||
# dry-run `run` gate, deployment_drift, the guard-lib probe and report_and_fix.
|
||||
#
|
||||
# report_and_fix is the one with real branching: status ok vs warning vs error,
|
||||
# STATUS_ONLY on/off, installer present/missing, and the post-fix re-verify that
|
||||
# can promote a status to "ok". Each combination gets its own case.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=services_harness.sh
|
||||
. "${SCRIPT_DIR}/services_harness.sh"
|
||||
|
||||
echo "== output helpers =="
|
||||
reset_state
|
||||
_t_eq "1" "$(msg yes | grep -c 'yes')" "msg prints its argument"
|
||||
_t_eq "1" "$(note hi | grep -c 'hi')" "note prints its argument"
|
||||
_t_eq "1" "$(warn hi | grep -c 'hi')" "warn prints its argument"
|
||||
_t_eq "1" "$(err hi | grep -c 'hi')" "err prints its argument"
|
||||
_t_eq "1" "$(header hi | grep -c 'hi')" "header prints its argument"
|
||||
|
||||
echo "== err_missing_script records and logs =="
|
||||
reset_state
|
||||
err_missing_script "missing: /nope/install.sh" >/dev/null
|
||||
_t_eq "1" "${#MISSING_SCRIPTS[@]}" "the missing script is recorded"
|
||||
_t_called 'logger .*MISSING REPAIR SCRIPT' "the miss is logged at error priority"
|
||||
|
||||
echo "== status store =="
|
||||
reset_state
|
||||
_t_eq "unknown" "$(get_service_status "nothing_set")" "unset key reads as unknown"
|
||||
set_service_status "hosts" "ok"
|
||||
_t_eq "ok" "$(get_service_status "hosts")" "a recorded status reads back"
|
||||
# A second write to one key means two checks are fighting over one summary row.
|
||||
if set_service_status "hosts" "error" >/dev/null 2>&1; then
|
||||
_t_fail "a duplicate write is rejected"
|
||||
else
|
||||
_t_pass "a duplicate write is rejected"
|
||||
fi
|
||||
_t_eq "ok" "$(get_service_status "hosts")" "the rejected write did not overwrite"
|
||||
|
||||
echo "== run honours --dry-run =="
|
||||
reset_state
|
||||
DRY_RUN=1
|
||||
out="$(run logger would-have-run)"
|
||||
_t_called_in() { # <haystack> <needle> <what>
|
||||
if [[ "$1" == *"$2"* ]]; then
|
||||
_t_pass "$3"
|
||||
else
|
||||
_t_fail "$3 (missing '$2')"
|
||||
fi
|
||||
}
|
||||
_t_called_in "$out" "DRY-RUN:" "dry-run announces instead of running"
|
||||
_t_not_called 'logger would-have-run' "dry-run did not actually run the command"
|
||||
DRY_RUN=0
|
||||
run logger really-ran
|
||||
_t_called 'logger really-ran' "with dry-run off the command really runs"
|
||||
|
||||
echo "== user_systemctl targets the user bus =="
|
||||
reset_state
|
||||
printf 'user:workout-locker.service\n' >"${DEV}/enabled"
|
||||
if user_systemctl kuhy is-enabled workout-locker.service; then
|
||||
_t_pass "user_systemctl reports an enabled user unit"
|
||||
else
|
||||
_t_fail "user_systemctl reports an enabled user unit"
|
||||
fi
|
||||
_t_called 'systemctl --user --machine=kuhy@.host' "it connects via --machine, not sudo -u"
|
||||
|
||||
echo "== guard_lib_instance_healthy =="
|
||||
reset_state
|
||||
printf 'hosts\n' >"${DEV}/guard_healthy"
|
||||
printf 'nsswitch\n' >"${DEV}/guard_degraded"
|
||||
if guard_lib_instance_healthy hosts; then
|
||||
_t_pass "a healthy instance passes"
|
||||
else
|
||||
_t_fail "a healthy instance passes"
|
||||
fi
|
||||
if guard_lib_instance_healthy nsswitch; then
|
||||
_t_fail "an inactive path unit fails"
|
||||
else
|
||||
_t_pass "an inactive path unit fails"
|
||||
fi
|
||||
if guard_lib_instance_healthy resolved; then
|
||||
_t_fail "an unregistered instance fails"
|
||||
else
|
||||
_t_pass "an unregistered instance fails"
|
||||
fi
|
||||
|
||||
echo "== deployment_drift =="
|
||||
reset_state
|
||||
manifest="${TEST_TMPDIR}/drift.sha256"
|
||||
subject="${TEST_TMPDIR}/subject.txt"
|
||||
printf 'original\n' >"$subject"
|
||||
(cd "${TEST_TMPDIR}" && sha256sum "$(basename "$subject")" >"$manifest")
|
||||
rc=0
|
||||
(cd "${TEST_TMPDIR}" && deployment_drift "$manifest") || rc=$?
|
||||
_t_eq "0" "$rc" "a matching manifest verifies"
|
||||
printf 'tampered\n' >"$subject"
|
||||
rc=0
|
||||
(cd "${TEST_TMPDIR}" && deployment_drift "$manifest") || rc=$?
|
||||
_t_eq "1" "$rc" "an edited file is reported as drift"
|
||||
rc=0
|
||||
deployment_drift "${TEST_TMPDIR}/absent.sha256" || rc=$?
|
||||
_t_eq "2" "$rc" "a missing manifest is 'unverifiable', not a pass"
|
||||
|
||||
echo "== report_and_fix: an ok service is recorded and left alone =="
|
||||
reset_state
|
||||
declare -a issues=()
|
||||
status="ok"
|
||||
report_and_fix issues status "startup_monitor" "fixing..." "$STARTUP_MONITOR_SCRIPT" "" >/dev/null
|
||||
_t_eq "ok" "$(get_service_status "startup_monitor")" "an ok status is recorded"
|
||||
_t_eq "0" "$ISSUES_FOUND" "an ok service counts no issues"
|
||||
_t_not_called 'ran setup_pc' "an ok service runs no installer"
|
||||
|
||||
echo "== report_and_fix: a warning is reported but never repaired =="
|
||||
reset_state
|
||||
make_installer "$STARTUP_MONITOR_SCRIPT"
|
||||
issues=("timer is not active")
|
||||
status="warning"
|
||||
# Captured to a file rather than `$(...)`: report_and_fix mutates ISSUES_FOUND
|
||||
# and SERVICE_STATUS, and a command substitution would strand both in a
|
||||
# subshell, making the assertions below silently test nothing.
|
||||
report_and_fix issues status "startup_monitor" "fixing..." \
|
||||
"$STARTUP_MONITOR_SCRIPT" "" >"${TEST_TMPDIR}/out.txt"
|
||||
out="$(cat "${TEST_TMPDIR}/out.txt")"
|
||||
_t_called_in "$out" "timer is not active" "the warning text is printed"
|
||||
_t_eq "1" "$ISSUES_FOUND" "a warning counts as an issue"
|
||||
_t_not_called 'ran setup_pc' "a warning does not trigger the installer"
|
||||
_t_eq "warning" "$(get_service_status "startup_monitor")" "the warning status is recorded"
|
||||
|
||||
echo "== report_and_fix: --status reports errors without repairing =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
STATUS_ONLY=1
|
||||
issues=("timer is not enabled")
|
||||
status="error"
|
||||
report_and_fix issues status "periodic_systems" "fixing..." "$PERIODIC_SYSTEM_SCRIPT" "" >/dev/null
|
||||
_t_not_called 'ran setup_periodic' "--status never runs an installer"
|
||||
_t_eq "error" "$(get_service_status "periodic_systems")" "the error status survives --status"
|
||||
|
||||
echo "== report_and_fix: an error runs the installer and re-verifies =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
printf 'periodic-system-maintenance.timer\n' >"${DEV}/enabled"
|
||||
issues=("timer is not enabled")
|
||||
status="error"
|
||||
report_and_fix issues status "periodic_systems" "fixing..." \
|
||||
"$PERIODIC_SYSTEM_SCRIPT" "periodic-system-maintenance.timer" >/dev/null
|
||||
_t_called 'ran setup_periodic_system' "the installer ran"
|
||||
_t_eq "1" "$FIXES_APPLIED" "the fix is counted"
|
||||
_t_eq "ok" "$(get_service_status "periodic_systems")" "a unit enabled after the fix is promoted to ok"
|
||||
|
||||
echo "== report_and_fix: a fix that does not take leaves the error standing =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
: >"${DEV}/enabled" # the installer ran but the unit is still not enabled
|
||||
issues=("timer is not enabled")
|
||||
status="error"
|
||||
report_and_fix issues status "periodic_systems" "fixing..." \
|
||||
"$PERIODIC_SYSTEM_SCRIPT" "periodic-system-maintenance.timer" >/dev/null
|
||||
_t_called 'ran setup_periodic_system' "the installer ran"
|
||||
_t_eq "error" "$(get_service_status "periodic_systems")" "an unverified fix does not become ok"
|
||||
|
||||
echo "== report_and_fix: dry-run skips the re-verify =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
printf 'periodic-system-maintenance.timer\n' >"${DEV}/enabled"
|
||||
DRY_RUN=1
|
||||
issues=("timer is not enabled")
|
||||
status="error"
|
||||
report_and_fix issues status "periodic_systems" "fixing..." \
|
||||
"$PERIODIC_SYSTEM_SCRIPT" "periodic-system-maintenance.timer" >/dev/null
|
||||
_t_not_called 'ran setup_periodic_system' "dry-run does not really install"
|
||||
_t_eq "error" "$(get_service_status "periodic_systems")" "dry-run never claims a fix landed"
|
||||
|
||||
echo "== report_and_fix: a missing installer is a broken-self-repair error =="
|
||||
reset_state
|
||||
rm -f "$MIDNIGHT_SHUTDOWN_SCRIPT"
|
||||
issues=("timer is not enabled")
|
||||
status="error"
|
||||
report_and_fix issues status "midnight_shutdown" "fixing..." \
|
||||
"$MIDNIGHT_SHUTDOWN_SCRIPT" "day-specific-shutdown.timer" >/dev/null
|
||||
_t_eq "1" "${#MISSING_SCRIPTS[@]}" "the missing installer is recorded"
|
||||
_t_called 'logger .*MISSING REPAIR SCRIPT' "the missing installer is logged"
|
||||
|
||||
echo "== report_and_fix: extra arguments reach the installer =="
|
||||
reset_state
|
||||
make_installer "$MIDNIGHT_SHUTDOWN_SCRIPT"
|
||||
issues=("timer is not enabled")
|
||||
status="error"
|
||||
report_and_fix issues status "midnight_shutdown" "fixing..." \
|
||||
"$MIDNIGHT_SHUTDOWN_SCRIPT" "day-specific-shutdown.timer" enable >/dev/null
|
||||
_t_called 'ran setup_midnight_shutdown.sh enable' "the trailing args are forwarded"
|
||||
# report_and_fix rewrites `status` through its nameref; reading it back both
|
||||
# asserts that and gives shellcheck the read it cannot infer through the
|
||||
# nameref (SC2034). The unit is not in $DEV/enabled, so the fix cannot verify.
|
||||
_t_eq "error" "$status" "an unverified fix leaves the caller's status at error"
|
||||
_t_eq "1" "${#issues[@]}" "the caller's issue list is left intact"
|
||||
|
||||
echo "== require_root =="
|
||||
reset_state
|
||||
# require_root ends in `exec sudo`, which would replace this test process, so
|
||||
# both branches run in a child shell against the fake sudo on PATH. EUID is
|
||||
# readonly in bash and cannot be assigned, so the non-root branch is reached by
|
||||
# running as the real (non-root) user and the root branch by shimming EUID
|
||||
# through a wrapper that sources the lib with EUID already 0 -- which only
|
||||
# `env -i` style faking can do. Instead: assert the observable behaviour of
|
||||
# each branch via the fake sudo's call log.
|
||||
cat >"${TEST_TMPDIR}/root_probe.sh" <<PROBE
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
. "${LIB_DIR}/services_common.sh"
|
||||
require_root "\$@"
|
||||
printf 'reached-past-require-root\n'
|
||||
PROBE
|
||||
chmod +x "${TEST_TMPDIR}/root_probe.sh"
|
||||
|
||||
probe_out="$("${TEST_TMPDIR}/root_probe.sh" --status 2>&1 || true)"
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
_t_called_in "$probe_out" "requires root privileges" "a non-root run announces the re-exec"
|
||||
_t_called 'sudo -E bash .*root_probe.sh --status' "it re-execs itself under sudo with its args"
|
||||
else
|
||||
_t_called_in "$probe_out" "reached-past-require-root" "a root run proceeds without re-execing"
|
||||
_t_not_called 'sudo -E bash' "a root run does not re-exec"
|
||||
fi
|
||||
|
||||
echo "== hosts_pacman_hooks_installed =="
|
||||
reset_state
|
||||
# The two hook paths are absolute and root-owned, so this asserts the real
|
||||
# machine's answer rather than a fixture: whichever way it goes, the function
|
||||
# must agree with the filesystem it is reading.
|
||||
if [[ -f /etc/pacman.d/hooks/10-guard-lib-unlock-all.hook &&
|
||||
-f /etc/pacman.d/hooks/90-guard-lib-relock-all.hook ]]; then
|
||||
if hosts_pacman_hooks_installed; then
|
||||
_t_pass "reports the guard-lib hook pair as installed when both exist"
|
||||
else
|
||||
_t_fail "reports the guard-lib hook pair as installed when both exist"
|
||||
fi
|
||||
else
|
||||
if hosts_pacman_hooks_installed; then
|
||||
_t_fail "reports the hook pair as absent when either is missing"
|
||||
else
|
||||
_t_pass "reports the hook pair as absent when either is missing"
|
||||
fi
|
||||
fi
|
||||
|
||||
_t_summary
|
||||
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for lib/services_report.sh — the usage text and the summary table.
|
||||
#
|
||||
# print_summary has four independent axes: the per-service colour mapping
|
||||
# (ok/warning/error/n-a/unknown), the DRY_RUN banner, the STATUS_ONLY wording,
|
||||
# and the fixes-applied vs no-fixes-applied wording. Each case below pins one.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=services_harness.sh
|
||||
. "${SCRIPT_DIR}/services_harness.sh"
|
||||
# shellcheck source=../services_report.sh
|
||||
. "${SCRIPT_DIR}/../services_report.sh"
|
||||
|
||||
echo "== usage =="
|
||||
usage_out="$(usage)"
|
||||
_t_called_in() { # <haystack> <needle> <what>
|
||||
if [[ "$1" == *"$2"* ]]; then
|
||||
_t_pass "$3"
|
||||
else
|
||||
_t_fail "$3 (missing '$2')"
|
||||
fi
|
||||
}
|
||||
_t_called_in "$usage_out" "--dry-run" "usage documents --dry-run"
|
||||
_t_called_in "$usage_out" "--status" "usage documents --status"
|
||||
_t_called_in "$usage_out" "Workout lock screen" "usage lists all 11 services"
|
||||
|
||||
echo "== print_summary: every status colour =="
|
||||
reset_state
|
||||
set_service_status "pacman_wrapper" "ok"
|
||||
set_service_status "makepkg_wrapper" "warning"
|
||||
set_service_status "midnight_shutdown" "error"
|
||||
set_service_status "startup_monitor" "n/a"
|
||||
set_service_status "periodic_systems" "skipped"
|
||||
# The remaining six keys are deliberately left unset so the ":-unknown"
|
||||
# default and its *) colour arm are exercised too.
|
||||
ISSUES_FOUND=0
|
||||
out="$(print_summary)"
|
||||
_t_called_in "$out" "pacman_wrapper" "summary lists pacman_wrapper"
|
||||
_t_called_in "$out" "unknown" "unset services report as unknown"
|
||||
_t_called_in "$out" "All services are properly configured!" "no issues -> success line"
|
||||
|
||||
echo "== print_summary: dry-run banner =="
|
||||
reset_state
|
||||
DRY_RUN=1
|
||||
ISSUES_FOUND=0
|
||||
out="$(print_summary)"
|
||||
_t_called_in "$out" "DRY RUN - No changes were made" "dry-run banner shown"
|
||||
|
||||
echo "== print_summary: status-only wording =="
|
||||
reset_state
|
||||
STATUS_ONLY=1
|
||||
ISSUES_FOUND=3
|
||||
out="$(print_summary)"
|
||||
_t_called_in "$out" "Found 3 service(s) with issues" "status-only counts issues"
|
||||
_t_called_in "$out" "Run without --status to fix issues" "status-only points at the fix flag"
|
||||
|
||||
echo "== print_summary: issues found and fixed =="
|
||||
reset_state
|
||||
ISSUES_FOUND=2
|
||||
FIXES_APPLIED=2
|
||||
out="$(print_summary)"
|
||||
_t_called_in "$out" "Applied 2 fix(es)" "reports the number of fixes applied"
|
||||
|
||||
echo "== print_summary: issues found, nothing fixed =="
|
||||
reset_state
|
||||
ISSUES_FOUND=2
|
||||
FIXES_APPLIED=0
|
||||
out="$(print_summary)"
|
||||
_t_called_in "$out" "Found 2 issue(s) but no fixes were applied" "warns when nothing was fixed"
|
||||
|
||||
_t_summary
|
||||
117
linux_configuration/scripts/periodic_background/lib/tests/test_services_units.sh
Executable file
117
linux_configuration/scripts/periodic_background/lib/tests/test_services_units.sh
Executable file
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for lib/services_units.sh — the three systemd timer/service checks.
|
||||
#
|
||||
# The unit-state half of each check is fully drivable through the systemctl
|
||||
# shim ($DEV/enabled, $DEV/active). The file-existence half probes absolute
|
||||
# root-owned paths (/etc/systemd/system/*.service, /usr/local/bin/*.sh) that no
|
||||
# shim can redirect, so those branches take whichever way this machine is
|
||||
# actually configured; the assertions below are written to hold either way.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=services_harness.sh
|
||||
. "${SCRIPT_DIR}/services_harness.sh"
|
||||
# shellcheck source=../services_units.sh
|
||||
. "${SCRIPT_DIR}/../services_units.sh"
|
||||
|
||||
echo "== check_midnight_shutdown: unit enabled and active =="
|
||||
reset_state
|
||||
make_installer "$MIDNIGHT_SHUTDOWN_SCRIPT"
|
||||
printf 'day-specific-shutdown.timer\n' >"${DEV}/enabled"
|
||||
printf 'day-specific-shutdown.timer\n' >"${DEV}/active"
|
||||
check_midnight_shutdown >/dev/null
|
||||
_t_called 'systemctl is-enabled day-specific-shutdown.timer' "the timer's enabled state is queried"
|
||||
_t_called 'systemctl is-active day-specific-shutdown.timer' "the timer's active state is queried"
|
||||
|
||||
echo "== check_midnight_shutdown: a disabled timer is downgraded to warning =="
|
||||
reset_state
|
||||
make_installer "$MIDNIGHT_SHUTDOWN_SCRIPT"
|
||||
: >"${DEV}/enabled"
|
||||
: >"${DEV}/active"
|
||||
check_midnight_shutdown >/dev/null
|
||||
# LATENT BUG, pinned deliberately rather than fixed here: the is-active check
|
||||
# assigns status="warning" unconditionally, so it overwrites the "error" the
|
||||
# is-enabled check set immediately above. A timer that is BOTH disabled and
|
||||
# inactive therefore ends up "warning", and report_and_fix only repairs on
|
||||
# "error" -- so the timer is never re-enabled. Identical in the pre-split file
|
||||
# (verified against rev 6691cb26), so this split neither caused nor hides it.
|
||||
# Fixing it changes enforcement behaviour and belongs in its own commit.
|
||||
_t_eq "warning" "$(get_service_status "midnight_shutdown")" "disabled+inactive is (wrongly) only a warning"
|
||||
_t_not_called 'ran setup_midnight_shutdown' "so the disabled timer is never repaired"
|
||||
|
||||
echo "== check_midnight_shutdown: --status reports without repairing =="
|
||||
reset_state
|
||||
make_installer "$MIDNIGHT_SHUTDOWN_SCRIPT"
|
||||
STATUS_ONLY=1
|
||||
: >"${DEV}/enabled"
|
||||
check_midnight_shutdown >/dev/null
|
||||
_t_not_called 'ran setup_midnight_shutdown' "--status never runs the setup script"
|
||||
_t_eq "warning" "$(get_service_status "midnight_shutdown")" "the verdict is still recorded"
|
||||
|
||||
echo "== check_startup_monitor: timer disabled triggers the installer =="
|
||||
reset_state
|
||||
make_installer "$STARTUP_MONITOR_SCRIPT"
|
||||
: >"${DEV}/enabled"
|
||||
: >"${DEV}/active"
|
||||
check_startup_monitor >/dev/null
|
||||
# Same latent downgrade as check_midnight_shutdown above.
|
||||
_t_eq "warning" "$(get_service_status "startup_monitor")" "disabled+inactive is (wrongly) only a warning"
|
||||
_t_not_called 'ran setup_pc_startup_monitor' "so the disabled timer is never repaired"
|
||||
|
||||
echo "== check_startup_monitor: enabled but inactive is a warning, not a fix =="
|
||||
reset_state
|
||||
make_installer "$STARTUP_MONITOR_SCRIPT"
|
||||
printf 'pc-startup-monitor.timer\n' >"${DEV}/enabled"
|
||||
: >"${DEV}/active"
|
||||
check_startup_monitor >/dev/null
|
||||
_t_not_called 'ran setup_pc_startup_monitor' "an inactive-only timer is not reinstalled"
|
||||
|
||||
echo "== check_periodic_systems: every unit healthy short-circuits =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
printf '%s\n' \
|
||||
'periodic-system-maintenance.timer' \
|
||||
'periodic-system-startup.service' \
|
||||
'hosts-file-monitor.service' >"${DEV}/enabled"
|
||||
printf '%s\n' \
|
||||
'periodic-system-maintenance.timer' \
|
||||
'hosts-file-monitor.service' >"${DEV}/active"
|
||||
check_periodic_systems >/dev/null
|
||||
# The maintenance script probe is an absolute path this test cannot stage, so
|
||||
# the run is "ok" only on a machine that really has it. Assert the invariant
|
||||
# that holds either way: no repair is attempted unless something reported bad.
|
||||
if [[ -f /usr/local/bin/periodic-system-maintenance.sh ]]; then
|
||||
_t_eq "ok" "$(get_service_status "periodic_systems")" "a fully healthy stack records ok"
|
||||
_t_not_called 'ran setup_periodic_system' "a healthy stack runs no installer"
|
||||
else
|
||||
_t_eq "error" "$(get_service_status "periodic_systems")" "a missing maintenance script is an error"
|
||||
fi
|
||||
|
||||
echo "== check_periodic_systems: a disabled unit is repaired =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
: >"${DEV}/enabled"
|
||||
: >"${DEV}/active"
|
||||
check_periodic_systems >/dev/null
|
||||
# hosts-file-monitor.service's is-active check is the last status assignment
|
||||
# here, and it too downgrades unconditionally -- same bug, same pinning.
|
||||
_t_eq "warning" "$(get_service_status "periodic_systems")" "a fully disabled stack is (wrongly) only a warning"
|
||||
_t_not_called 'ran setup_periodic_system' "so the disabled stack is never repaired"
|
||||
|
||||
echo "== check_periodic_systems: startup service disabled alone still repairs =="
|
||||
reset_state
|
||||
make_installer "$PERIODIC_SYSTEM_SCRIPT"
|
||||
printf '%s\n' \
|
||||
'periodic-system-maintenance.timer' \
|
||||
'hosts-file-monitor.service' >"${DEV}/enabled"
|
||||
printf '%s\n' \
|
||||
'periodic-system-maintenance.timer' \
|
||||
'hosts-file-monitor.service' >"${DEV}/active"
|
||||
check_periodic_systems >/dev/null
|
||||
# This one DOES repair: hosts-file-monitor is active, so the final downgrade
|
||||
# never fires and the "error" from the disabled startup service survives.
|
||||
_t_called 'ran setup_periodic_system' "a disabled startup service alone triggers the repair"
|
||||
_t_eq "1" "$FIXES_APPLIED" "the fix is counted"
|
||||
|
||||
_t_summary
|
||||
Loading…
Reference in New Issue
Block a user