diff --git a/docs/superpowers/evidence/compulsive-block-stale-orig-2026-10-11.json b/docs/superpowers/evidence/compulsive-block-stale-orig-2026-10-11.json new file mode 100644 index 00000000..886ccc5e --- /dev/null +++ b/docs/superpowers/evidence/compulsive-block-stale-orig-2026-10-11.json @@ -0,0 +1,36 @@ +{ + "intent": "check_compulsive_blocker stops reporting a messaging app as wrapped when its package was removed and only /usr/bin/.orig (the real launcher, a blocker bypass) is left; the repair deletes it.", + "scope": [ + "linux_configuration/periodic_background/lib/services_apps.sh, linux_configuration/periodic_background/lib/tests/test_services_apps.sh", + "Installer/hook side lives in digital-wellbeing (commit 6867f29); non-goal: no change to the full-installer repair path" + ], + "changes": [ + "_pkg_owns_path reads ${SYSROOT}/var/lib/pacman/local/*/files (0 owned, 1 unowned, 2 undeterminable) instead of calling the wrapped pacman", + "A .orig on an unowned /usr/bin/ is a warning 'stale .orig left after package removal' and is removed via run (plus a dangling wrapper) unless --status; an unreadable db never prunes" + ], + "verification": [ + { + "command": "periodic_background/lib/tests/run_all.sh", + "result": "pass", + "evidence": "test_services_apps.sh 38 passed 0 failed (new cases fail 8/38 against HEAD); all services suites pass" + }, + { + "command": "vmbox guest cco: check_compulsive_blocker on a real stranded /usr/bin/discord.orig", + "result": "pass", + "evidence": "--status: warning, file kept; repair: FIXES=1, file gone; rerun: ok. Passed in vmbox, not verified on the host" + }, + { + "command": "shellcheck -x services_apps.sh test_services_apps.sh", + "result": "pass", + "evidence": "clean" + } + ], + "risks": [ + "A package that ships the app under a different path than /usr/bin/ would make its .orig look stale", + "The host's next periodic run will delete /usr/bin/discord.orig itself" + ], + "rollback": [ + "git revert the commit", + "Run check_and_enable_services.sh --status and confirm the compulsive_blocker section" + ] +} diff --git a/linux_configuration/periodic_background/lib/services_apps.sh b/linux_configuration/periodic_background/lib/services_apps.sh index 4975ca4f..79b28312 100755 --- a/linux_configuration/periodic_background/lib/services_apps.sh +++ b/linux_configuration/periodic_background/lib/services_apps.sh @@ -27,9 +27,19 @@ check_compulsive_blocker() { # Check if wrappers are installed for known apps local checked_any=false + local stale=() for app in beeper signal-desktop discord; do local wrapper_path="${SYSROOT}/usr/bin/$app" - if [[ -f "${wrapper_path}.orig" ]] || [[ -L "$wrapper_path" ]]; then + local owned=0 + _pkg_owns_path "/usr/bin/$app" || owned=$? + if [[ -f "${wrapper_path}.orig" && $owned -eq 1 ]]; then + # Package removed: pacman deleted the wrapper but not the backup, + # so `.orig` still launches the app around the blocker. + issues+=("stale $app.orig left after package removal") + stale+=("$app") + if [[ $status != "error" ]]; then status="warning"; fi + checked_any=true + elif [[ -f "${wrapper_path}.orig" ]] || [[ -L "$wrapper_path" ]]; then if [[ -f "${wrapper_path}.orig" ]]; then msg "$app wrapper installed (original backed up)" checked_any=true @@ -64,11 +74,38 @@ check_compulsive_blocker() { err_missing_script "Install script not found: $COMPULSIVE_BLOCK_SCRIPT" fi fi + + # A stale backup is repaired on its own, whatever the status: the full + # installer is only for a missing blocker. + if [[ $STATUS_ONLY -eq 0 ]]; then + for app in "${stale[@]}"; do + local stale_path="${SYSROOT}/usr/bin/$app" + note "Removing stale ${stale_path}.orig..." + run rm -f "${stale_path}.orig" + # A wrapper left behind would exec the .orig just deleted. + if grep -q "block-compulsive-opening" "$stale_path" 2>/dev/null; then + run rm -f "$stale_path" + fi + ((FIXES_APPLIED++)) || true + done + fi fi set_service_status "compulsive_blocker" "$status" } +# Does an installed package own ? Read from ${SYSROOT}'s pacman db +# rather than `pacman -Qqo`, so it honours SYSROOT and skips the pacman +# wrapper. grep's exit code is the answer: 0 owned, 1 unowned, 2 undeterminable +# (no db) -- only a definite 1 makes a .orig stale. +_pkg_owns_path() { # + local lists=("${SYSROOT}"/var/lib/pacman/local/*/files) + [[ -e ${lists[0]} ]] || return 2 + local rc=0 + grep -qxF -- "${1#/}" "${lists[@]}" 2>/dev/null || rc=$? + return "$rc" +} + check_leechblock() { header "LeechBlock Browser Extension" diff --git a/linux_configuration/periodic_background/lib/tests/test_services_apps.sh b/linux_configuration/periodic_background/lib/tests/test_services_apps.sh index 9bb1c7a9..c3272c38 100755 --- a/linux_configuration/periodic_background/lib/tests/test_services_apps.sh +++ b/linux_configuration/periodic_background/lib/tests/test_services_apps.sh @@ -24,6 +24,30 @@ _t_called_in() { # fi } +_t_not_in() { # + if [[ "$1" == *"$2"* ]]; then + _t_fail "$3 (unexpected '$2')" + else + _t_pass "$3" + fi +} + +# What install_wrapper leaves on /usr/bin/: the marker the repair greps. +wrapper_marker() { # + sysfile "$1" + printf 'exec /usr/local/bin/block-compulsive-opening.sh wrapper x\n' >"${SYSROOT_DIR}/$1" +} + +# Register as owned by in the fixture pacman db. +own_path() { # + sysfile "var/lib/pacman/local/$1-1-1/files" + printf '%%FILES%%\n%s\n' "$2" >"${SYSROOT_DIR}/var/lib/pacman/local/$1-1-1/files" +} + +sys_state() { # -> present|absent + if [[ -e "${SYSROOT_DIR}/$1" ]]; then echo present; else echo absent; fi +} + TEST_USER="${SUDO_USER:-$USER}" echo "== check_compulsive_blocker: no target apps present ==" @@ -58,9 +82,70 @@ sysfile usr/local/bin/block-compulsive-opening.sh present_command discord absent_command beeper signal-desktop sysfile usr/bin/discord.orig +wrapper_marker usr/bin/discord +own_path discord usr/bin/discord check_compulsive_blocker >"${TEST_TMPDIR}/out.txt" _t_called_in "$(cat "${TEST_TMPDIR}/out.txt")" "discord wrapper installed" "a backed-up original counts as wrapped" _t_eq "ok" "$(get_service_status "compulsive_blocker")" "a wrapped app leaves status ok" +_t_eq "0" "$FIXES_APPLIED" "an owned wrap is not touched" + +echo "== check_compulsive_blocker: a .orig left by a package removal is stale ==" +reset_state +sysfile usr/local/bin/block-compulsive-opening.sh +absent_command beeper signal-desktop discord +# pacman removed /usr/bin/discord (our wrapper) but not the backup; another +# package in the db proves the db is readable, so "unowned" is definite. +sysfile usr/bin/discord.orig +own_path signal-desktop usr/bin/signal-desktop +check_compulsive_blocker >"${TEST_TMPDIR}/out.txt" +_t_called_in "$(cat "${TEST_TMPDIR}/out.txt")" "stale discord.orig left after package removal" "the stale backup is named" +_t_not_in "$(cat "${TEST_TMPDIR}/out.txt")" "discord wrapper installed" "a stale backup is not reported as a wrapper" +_t_eq "warning" "$(get_service_status "compulsive_blocker")" "a stale backup is a warning" +_t_eq "absent" "$(sys_state usr/bin/discord.orig)" "the repair deletes the stale .orig" +_t_eq "1" "$FIXES_APPLIED" "the removal is counted as a fix" +_t_not_called 'ran block_compulsive_opening' "a stale backup does not rerun the installer" + +echo "== check_compulsive_blocker: a stale .orig takes its dangling wrapper with it ==" +reset_state +sysfile usr/local/bin/block-compulsive-opening.sh +absent_command beeper signal-desktop discord +sysfile usr/bin/discord.orig +wrapper_marker usr/bin/discord +own_path signal-desktop usr/bin/signal-desktop +check_compulsive_blocker >/dev/null +_t_eq "absent" "$(sys_state usr/bin/discord)" "our wrapper on an unowned path is deleted" +_t_eq "absent" "$(sys_state usr/bin/discord.orig)" "and so is its .orig" + +echo "== check_compulsive_blocker: --status reports a stale .orig but keeps it ==" +reset_state +sysfile usr/local/bin/block-compulsive-opening.sh +absent_command beeper signal-desktop discord +sysfile usr/bin/discord.orig +own_path signal-desktop usr/bin/signal-desktop +STATUS_ONLY=1 +check_compulsive_blocker >"${TEST_TMPDIR}/out.txt" +_t_called_in "$(cat "${TEST_TMPDIR}/out.txt")" "stale discord.orig" "--status still names the stale backup" +_t_eq "present" "$(sys_state usr/bin/discord.orig)" "--status deletes nothing" + +echo "== check_compulsive_blocker: --dry-run reports a stale .orig but keeps it ==" +reset_state +sysfile usr/local/bin/block-compulsive-opening.sh +absent_command beeper signal-desktop discord +sysfile usr/bin/discord.orig +own_path signal-desktop usr/bin/signal-desktop +DRY_RUN=1 +check_compulsive_blocker >/dev/null +_t_eq "present" "$(sys_state usr/bin/discord.orig)" "--dry-run deletes nothing" + +echo "== check_compulsive_blocker: an unreadable pacman db never prunes ==" +reset_state +sysfile usr/local/bin/block-compulsive-opening.sh +present_command discord +absent_command beeper signal-desktop +sysfile usr/bin/discord.orig +check_compulsive_blocker >"${TEST_TMPDIR}/out.txt" +_t_called_in "$(cat "${TEST_TMPDIR}/out.txt")" "discord wrapper installed" "no db: the backup still counts as a wrapper" +_t_eq "present" "$(sys_state usr/bin/discord.orig)" "no db: the .orig is kept" echo "== check_compulsive_blocker: a symlinked wrapper without .orig is skipped ==" reset_state