diff --git a/migrations/1788009111.sh b/migrations/1788009111.sh index fc2d262f..676fd5d8 100644 --- a/migrations/1788009111.sh +++ b/migrations/1788009111.sh @@ -1,149 +1,72 @@ echo "Temporarily remove automatic printer discovery" -# cups-browsed is the daemon that watches the network and creates print queues -# by itself. Hardening it (1787815267) took a root daemon with a predictable -# cache down to a confined service account, but a daemon that turns anything -# advertising itself on the network into a print queue is a lot of exposure for -# a convenience, so it comes out of the default install while that is reworked. -# Temporarily, and only the discovery half: CUPS itself stays, printing keeps -# working, and a printer is added by hand in Print Settings instead of -# appearing on its own. machine_marker="${OMARCHY_CUPS_BROWSED_REMOVAL_MARKER:-/var/lib/omarchy/migrations/1788009111}" [[ ! -e $machine_marker ]] || exit 0 - -# Nothing to do on a machine that never had it. Checked before any sudo so those -# runs never prompt for a password, and before the marker so no machine pays a -# password prompt for a removal it does not need. omarchy-pkg-present cups-browsed || exit 0 -# Ask pacman whether the removal is possible before touching the service. If -# something here depends on cups-browsed, the alternative is a machine whose -# discovery daemon has been stopped and whose package removal then failed -- -# broken rather than removed. -if ! pacman -R --print cups-browsed >/dev/null 2>&1; then - echo " Something else on this machine still depends on cups-browsed, so it is staying installed." - exit 0 -fi +# Check the full removal transaction before changing the service or queues. +pacman -Rs --print cups-browsed >/dev/null -# Disable before removing, and this is the only window in which it works. -# pacman deletes the unit file but not the enable symlink systemd wrote under -# /etc, and once the unit is gone `systemctl disable` refuses it by name and -# leaves the symlink dangling with nothing left that can clean it. Stopping is -# part of the same step: a daemon whose executable has been unlinked keeps -# running until it is told not to. A masked or already-disabled unit reports -# not-enabled and is left alone. Doing it before the queue list is read also -# means the list cannot grow a new entry while it is being acted on. +# Disable the unit while its package still owns the unit file so systemd can +# remove the enable symlink cleanly. if systemctl is-enabled --quiet cups-browsed.service 2>/dev/null; then - sudo systemctl disable --now cups-browsed.service + sudo systemctl disable --now cups-browsed.service >/dev/null elif systemctl is-active --quiet cups-browsed.service 2>/dev/null; then - sudo systemctl stop cups-browsed.service + sudo systemctl stop cups-browsed.service >/dev/null fi -# cups-browsed keeps the queues it generated when it stops -- -# KeepGeneratedQueuesOnShutdown defaults to Yes and nothing here overrides it -- -# and those queues route through its own implicitclass backend, which goes with -# the package, so none of them can print again. The idle ones are removed rather -# than left in Print Settings looking like printers; the ones with jobs on them -# are handled below. Only queues on that backend: a printer added by hand has an -# ipp:// or usb:// device and is left alone. +# cups-browsed leaves its implicitclass queues behind when stopped. Remove idle +# discovery queues before removing the backend they require, but leave queues +# with jobs for the user to resolve. # -# LC_ALL=C because lpstat translates "device for", and on a German or French -# machine the untranslated pattern would match nothing and read exactly like a -# machine that had no queues to clean up. Captured rather than piped so that -# failing to reach cupsd is distinguishable from finding nothing. The name is -# matched greedily because CUPS allows a colon in a queue name but never a -# space, so the last ": implicitclass://" is the separator and an earlier colon -# belongs to the name. -if ! queue_report=$(LC_ALL=C lpstat -v 2>/dev/null); then - echo " Could not ask CUPS which queues discovery had created." - echo " Discovery is off, but cups-browsed stays installed; remove it by hand once CUPS answers." - exit 0 +# A healthy CUPS server with no configured printers reports this condition on +# stderr and exits 1. Treat that as an empty queue list; every other failure +# keeps the migration pending so it can be retried. +if queue_report=$(LC_ALL=C lpstat -v 2>&1); then + : +elif [[ $queue_report == "lpstat: No destinations added." ]]; then + queue_report="" +else + printf '%s\n' "$queue_report" >&2 + exit 1 fi generated_queues=$(printf '%s\n' "$queue_report" | sed -n 's|^device for \(.*\): implicitclass://.*|\1|p') -unremoved=0 - while IFS= read -r queue; do [[ -n $queue ]] || continue - # A queue name is whatever the printer advertised, put through cups-browsed's - # own sanitizer. `lpstat -o` takes its destination as an optional argument, so - # a leading dash reads as the next option and a comma as a list separator, and - # "all" is its word for every destination. The jobs on such a queue cannot be - # asked about, so it is left alone and named. Never a reason to keep the - # package, though: that would hand a printer that picked its own name a veto - # over the removal. - if [[ $queue == -* || $queue == *,* || $queue == "all" ]]; then - echo " Cannot safely ask about jobs on the queue named '$queue'; remove it in Print Settings." - continue - fi - - # Close the queue to new jobs before looking at what is on it. Otherwise a job - # submitted between the check and the removal -- the sudo below can sit at a - # password prompt for as long as someone takes to type it -- is cancelled by a - # deletion that decided the queue was empty. It also stops more jobs piling - # onto a queue that is being left behind and can no longer route them. - if ! sudo cupsreject -r "Printer discovery has been removed from Omarchy" "$queue"; then - echo " Could not stop $queue accepting new jobs, so it is being left alone." - unremoved=1 - continue - fi - - # Removing a queue cancels the jobs on it. implicitclass needs cups-browsed - # only to pick a destination, so a job already past that point finishes on its - # own; one still waiting cannot, because the daemon that would route it has - # stopped. Neither is this migration's to throw away, so the queue is left for - # whoever owns them, and what will and will not happen is said rather than - # implied. - if job_report=$(LC_ALL=C lpstat -o "$queue" 2>/dev/null); then - if [[ -n $job_report ]]; then - echo " $queue still has jobs and is no longer taking new ones, so it is being left alone." - echo " Anything already sent to the printer finishes; anything still waiting cannot be routed now." - echo " Cancel what is left and remove the queue in Print Settings." + if ! reject_error=$(sudo cupsreject -r "Printer discovery has been removed from Omarchy" "$queue" 2>&1); then + if LC_ALL=C lpstat -p "$queue" >/dev/null 2>&1; then + printf '%s\n' "$reject_error" >&2 + exit 1 + else continue fi + fi + + if job_report=$(LC_ALL=C lpstat -o "$queue" 2>&1); then + [[ -z $job_report ]] || continue + elif LC_ALL=C lpstat -p "$queue" >/dev/null 2>&1; then + printf '%s\n' "$job_report" >&2 + exit 1 else - echo " Could not check for jobs on $queue, so it is being left alone." + # The queue disappeared after the initial snapshot, which is already the + # desired state. continue fi - # A queue another administrator removed while this was running is a queue that - # is gone, which is the outcome wanted -- not a failure worth keeping the - # package for. - if ! sudo lpadmin -x "$queue"; then + if ! delete_error=$(sudo lpadmin -x "$queue" 2>&1); then + # Treat a concurrent disappearance as success. A queue that still exists + # means CUPS did not complete the deletion, so retry the migration later. if LC_ALL=C lpstat -p "$queue" >/dev/null 2>&1; then - echo " Could not remove the queue $queue." - unremoved=1 + printf '%s\n' "$delete_error" >&2 + exit 1 fi fi done <<<"$generated_queues" -# A queue that would not delete is a CUPS that is not answering as expected, so -# the package stays rather than deleting the backend out from under it. Discovery -# is stopped either way, which is the half that mattered. omarchy-migrate records -# this migration for the user as soon as it exits zero, so this is where the -# machine stays until someone removes the package by hand -- said plainly rather -# than dressed up as a retry. -if ((unremoved)); then - echo " Leaving cups-browsed installed. Discovery is off; remove the package by hand once those queues are gone." - exit 0 -fi - -# Raw pacman rather than omarchy-pkg-drop, which passes -n: that discards the -# files pacman has marked as backups instead of renaming them .pacsave, and -# /etc/cups/cups-browsed.conf is one of them. A removal meant to be temporary -# should leave the machine's copy of its own configuration behind. Plain -R -# rather than -Rs, so this only ever removes the one package it names: -s also -# sweeps dependencies that have become unneeded, which is nothing today but is -# a promise the dependency graph of a rolling distribution cannot keep. -# pacman's systemd hook reloads the system manager once the unit file goes. -sudo pacman -R --noconfirm cups-browsed - -# Migration state is per user, so every account on this machine runs every -# migration. Without machine-wide state, an account whose first run comes after -# someone deliberately reinstalled cups-browsed would quietly take it back out -# again. This records that the machine has had its one removal. +omarchy-pkg-drop cups-browsed >/dev/null sudo install -Dm644 /dev/null "$machine_marker" diff --git a/test/acceptance.d/system-test.sh b/test/acceptance.d/system-test.sh index 75e9ac81..ab3affa7 100644 --- a/test/acceptance.d/system-test.sh +++ b/test/acceptance.d/system-test.sh @@ -83,6 +83,7 @@ verify_printing_security() { for path in \ /etc/cups/cups-browsed.conf \ + /etc/cups/cups-browsed.conf.pacsave \ /usr/bin/cups-browsed \ /usr/lib/cups/backend/implicitclass \ /usr/lib/systemd/system/cups-browsed.service \ diff --git a/test/shell.d/cups-browsed-removal-migration-test.sh b/test/shell.d/cups-browsed-removal-migration-test.sh index 63f6a053..a311e6e7 100644 --- a/test/shell.d/cups-browsed-removal-migration-test.sh +++ b/test/shell.d/cups-browsed-removal-migration-test.sh @@ -2,7 +2,7 @@ set -euo pipefail -source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" +source "$(cd -- "$(dirname -- "$0")" && pwd)/base-test.sh" migration="$ROOT/migrations/1788009111.sh" @@ -12,26 +12,31 @@ trap 'rm -rf "$test_tmp"' EXIT mock_bin="$test_tmp/bin" mkdir -p "$mock_bin" "$test_tmp/var/lib/omarchy/migrations" -# Every privileged step is stubbed: a real run here would take printer discovery -# off the developer's own machine. +# Every privileged step is stubbed: a real run here would take printer +# discovery off the developer's own machine. cat >"$mock_bin/omarchy-pkg-present" <<'SH' #!/bin/bash [[ " $BROWSED_INSTALLED " == *" $1 "* ]] SH -# pacman answers the removal preflight from the state each case sets up, and -# logs the removal itself so the flags it is called with are visible. +cat >"$mock_bin/omarchy-pkg-drop" <<'SH' +#!/bin/bash +printf 'omarchy-pkg-drop\t%s\n' "$*" >>"$BROWSED_LOG" +[[ -z $BROWSED_DROP_FAILS ]] +SH + cat >"$mock_bin/pacman" <<'SH' #!/bin/bash printf 'pacman\t%s\n' "$*" >>"$BROWSED_LOG" -[[ $* == *--print* ]] || exit 0 -[[ -z $BROWSED_REMOVAL_BLOCKED ]] +if [[ $* == *--print* ]]; then + [[ -z $BROWSED_REMOVAL_BLOCKED ]] +fi SH cat >"$mock_bin/systemctl" <<'SH' #!/bin/bash printf 'systemctl\t%s\n' "$*" >>"$BROWSED_LOG" -unit=${*: -1} +unit=$3 case $1 in is-enabled) [[ " $BROWSED_ENABLED " == *" $unit "* ]] ;; is-active) [[ " $BROWSED_ACTIVE " == *" $unit "* ]] ;; @@ -51,386 +56,303 @@ printf 'install\t%s\n' "$*" >>"$BROWSED_LOG" exec /usr/bin/install "$@" SH -# lpstat reports the queues cups-browsed generated. Only those route through its -# implicitclass backend; the ipp:// and usb:// entries are printers a person -# added and must survive. +# lpstat reports only package-generated queues on the implicitclass backend. +# Manual ipp:// and usb:// printers must survive. cat >"$mock_bin/lpstat" <<'SH' #!/bin/bash printf 'lpstat\t%s\n' "$*" >>"$BROWSED_LOG" case $1 in -v) - [[ -z $BROWSED_LPSTAT_FAILS ]] || exit 1 - # lpstat translates "device for". Only a caller that pinned the locale gets - # the string the migration parses. - if [[ ${LC_ALL:-} == "C" ]]; then + if [[ -n $BROWSED_LPSTAT_FAILS ]]; then + echo "lpstat: Scheduler is not responding." >&2 + exit 1 + elif [[ -z $BROWSED_QUEUES ]]; then + echo "lpstat: No destinations added." >&2 + exit 1 + elif [[ $LC_ALL == "C" ]]; then printf '%s\n' "$BROWSED_QUEUES" else printf '%s\n' "$BROWSED_QUEUES" | sed 's|^device for |Gerät für |' fi ;; -p) - # A destination that no longer exists is unknown to lpstat. [[ " $BROWSED_GONE_QUEUES " != *" $2 "* ]] - exit $? ;; -o) - [[ -z $BROWSED_LPSTAT_O_FAILS ]] || exit 1 - # Jobs are listed per queue: "- ". - [[ " $BROWSED_BUSY_QUEUES " == *" $2 "* ]] && printf '%s-7 alice 1024\n' "$2" + if [[ -n $BROWSED_LPSTAT_O_FAILS ]]; then + echo "lpstat: Scheduler is not responding." >&2 + exit 1 + fi + if [[ " $BROWSED_BUSY_QUEUES " == *" $2 "* ]]; then + printf '%s-7 alice 1024\n' "$2" + fi ;; esac -exit 0 SH cat >"$mock_bin/lpadmin" <<'SH' #!/bin/bash printf 'lpadmin\t%s\n' "$*" >>"$BROWSED_LOG" -[[ -z $BROWSED_LPADMIN_FAILS ]] +if [[ -n $BROWSED_LPADMIN_FAILS ]]; then + echo "lpadmin: Printer does not exist." >&2 + exit 1 +fi SH cat >"$mock_bin/cupsreject" <<'SH' #!/bin/bash printf 'cupsreject\t%s\n' "$*" >>"$BROWSED_LOG" -[[ -z $BROWSED_REJECT_FAILS ]] +if [[ -n $BROWSED_REJECT_FAILS ]]; then + echo "cupsreject: Unable to reject jobs." >&2 + exit 1 +fi SH chmod +x "$mock_bin"/* log="$test_tmp/actions.log" output="$test_tmp/migration.out" -marker="$test_tmp/var/lib/omarchy/migrations/1788009111" +errors="$test_tmp/migration.err" +status=0 + +start_case() { + installed="cups-browsed" + enabled="cups-browsed.service" + active="cups-browsed.service cups.service" + blocked="" + drop_fails="" + queues="" + busy="" + lpstat_fails="" + lpstat_o_fails="" + lpadmin_fails="" + reject_fails="" + gone_queues="" + use_marker="$test_tmp/var/lib/omarchy/migrations/$1" + rm -f "$use_marker" +} run_migration() { : >"$log" : >"$output" + : >"$errors" - # No LC_ALL in the environment, so the mock above sees "C" only when the - # migration pinned it for the call itself -- which is what a non-English - # desktop depends on. - env -u LC_ALL -u LANGUAGE \ + if env -u LC_ALL -u LANGUAGE \ BROWSED_LOG="$log" \ - BROWSED_INSTALLED="${installed:-}" \ - BROWSED_ENABLED="${enabled:-}" \ - BROWSED_ACTIVE="${active:-}" \ - BROWSED_REMOVAL_BLOCKED="${blocked:-}" \ - BROWSED_QUEUES="${queues:-}" \ - BROWSED_BUSY_QUEUES="${busy:-}" \ - BROWSED_LPSTAT_FAILS="${lpstat_fails:-}" \ - BROWSED_LPADMIN_FAILS="${lpadmin_fails:-}" \ - BROWSED_LPSTAT_O_FAILS="${lpstat_o_fails:-}" \ - BROWSED_REJECT_FAILS="${reject_fails:-}" \ - BROWSED_GONE_QUEUES="${gone_queues:-}" \ + BROWSED_INSTALLED="$installed" \ + BROWSED_ENABLED="$enabled" \ + BROWSED_ACTIVE="$active" \ + BROWSED_REMOVAL_BLOCKED="$blocked" \ + BROWSED_DROP_FAILS="$drop_fails" \ + BROWSED_QUEUES="$queues" \ + BROWSED_BUSY_QUEUES="$busy" \ + BROWSED_LPSTAT_FAILS="$lpstat_fails" \ + BROWSED_LPSTAT_O_FAILS="$lpstat_o_fails" \ + BROWSED_LPADMIN_FAILS="$lpadmin_fails" \ + BROWSED_REJECT_FAILS="$reject_fails" \ + BROWSED_GONE_QUEUES="$gone_queues" \ PATH="$mock_bin:$PATH" \ OMARCHY_PATH="$ROOT" \ - OMARCHY_CUPS_BROWSED_REMOVAL_MARKER="${use_marker:-$marker}" \ - bash -euo pipefail "$migration" >"$output" + OMARCHY_CUPS_BROWSED_REMOVAL_MARKER="$use_marker" \ + bash -euo pipefail "$migration" >"$output" 2>"$errors"; then + status=0 + else + status=$? + fi } -# ------------------------------------------------ a machine that has discovery +assert_description_only() { + [[ $(<"$output") == "Temporarily remove automatic printer discovery" ]] || + fail "the migration prints only its description" "$(cat "$output")" +} -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" -blocked="" -# CUPS allows a colon in a destination name but never a space, so "Front:Desk" -# is a legal queue and the separator is the last ": implicitclass://". -queues=$'device for Office: implicitclass://Office/\ndevice for Front:Desk: implicitclass://Front:Desk/\ndevice for -p: implicitclass://-p/\ndevice for a,b: implicitclass://a,b/\ndevice for all: implicitclass://all/\ndevice for Desk: ipp://192.168.1.9/ipp/print\ndevice for Attic: usb://HP/LaserJet%20P1102' -rm -f "$marker" +assert_quiet_success() { + (( status == 0 )) || fail "the migration succeeds" "$(cat "$errors")" + assert_description_only + [[ ! -s $errors ]] || fail "a successful migration is quiet" "$(cat "$errors")" +} + +# ------------------------------------------------ a machine that has printers + +start_case printers +queues=$'device for Office: implicitclass://Office/\ndevice for Front_Desk: implicitclass://Front_Desk/\ndevice for all: implicitclass://all/\ndevice for Desk: ipp://192.168.1.9/ipp/print\ndevice for Attic: usb://HP/LaserJet%20P1102' run_migration +assert_quiet_success +grep -qxF $'pacman\t-Rs --print cups-browsed' "$log" || + fail "the migration preflights the package removal" "$(cat "$log")" grep -qxF $'sudo\tsystemctl disable --now cups-browsed.service' "$log" || - fail "the removal disables and stops the discovery service" "$(cat "$log")" -pass "the removal disables and stops the discovery service" - -# -Rns would discard /etc/cups/cups-browsed.conf rather than keep it as a -# .pacsave, and a removal meant to be temporary must not delete configuration. -grep -qxF $'pacman\t-R --noconfirm cups-browsed' "$log" || - fail "the removal keeps configuration pacman marked as a backup" "$(cat "$log")" -if grep -qE -- '-Rns|-Rn ' "$log"; then - fail "the removal never passes -n" "$(cat "$log")" -fi -# -s would also sweep dependencies that became unneeded, which is a promise a -# rolling dependency graph cannot keep. -if grep -q -- '-Rs --noconfirm' "$log"; then - fail "the removal names only the package it means to remove" "$(cat "$log")" -fi -pass "the removal keeps the machine's own cups-browsed.conf and touches nothing else" - -# pacman deletes the unit file but not the enable symlink, and once the unit is -# gone systemd cannot resolve it by name to clean that up. -disable_line=$(grep -n 'disable --now' "$log" | tail -1 | cut -d: -f1 || true) -removal_line=$(grep -n $'^pacman\t-R --noconfirm' "$log" | head -1 | cut -d: -f1 || true) -[[ -n $disable_line && -n $removal_line ]] || - fail "the removal both disables the unit and removes the package" "$(cat "$log")" -(( disable_line < removal_line )) || - fail "the removal disables before the unit file goes" "$(cat "$log")" -pass "the removal disables before the unit file goes" - -# cups-browsed keeps its generated queues on shutdown, and they print through -# the implicitclass backend that goes with the package. -grep -qxF $'sudo\tlpadmin -x Office' "$log" || - fail "the queues discovery generated are removed with it" "$(cat "$log")" + fail "the migration disables and stops discovery" "$(cat "$log")" +for queue in Office Front_Desk all; do + grep -qxF "sudo lpadmin -x $queue" "$log" || + fail "the migration removes generated queue $queue" "$(cat "$log")" +done grep -q 'lpadmin -x Desk' "$log" && - fail "a printer added by hand over ipp survives" "$(cat "$log")" + fail "the migration leaves a manually-added IPP printer alone" "$(cat "$log")" grep -q 'lpadmin -x Attic' "$log" && - fail "a printer added by hand over usb survives" "$(cat "$log")" -grep -qxF $'sudo\tlpadmin -x Front:Desk' "$log" || - fail "a queue whose name contains a colon is still matched" "$(cat "$log")" + fail "the migration leaves a manually-added USB printer alone" "$(cat "$log")" +grep -qxF $'omarchy-pkg-drop\tcups-browsed' "$log" || + fail "the migration uses the standard package removal helper" "$(cat "$log")" +[[ -f $use_marker ]] || fail "the migration records machine-wide completion" -# cups-browsed names queues after what the printer advertised, so the name came -# off the network. lpstat and lpadmin take a destination as an option value: a -# leading dash reads as another option and a comma separates a list. -if grep -qF -- $'lpstat\t-o -p' "$log"; then - fail "a queue named like an option is never passed to lpstat" "$(cat "$log")" -fi -if grep -q -- 'lpadmin -x -p' "$log"; then - fail "a queue named like an option is never passed to lpadmin" "$(cat "$log")" -fi -if grep -q 'lpadmin -x a,b' "$log"; then - fail "a queue name holding a comma is never passed as a destination list" "$(cat "$log")" -fi -if grep -qF -- $'lpstat\t-o all' "$log"; then - fail "a queue named all is never asked about, since lpstat reads it as every destination" "$(cat "$log")" -fi -grep -qF -- "Cannot safely ask about jobs on the queue named '-p'" "$output" || - fail "a queue that cannot be asked about safely is reported" "$(cat "$output")" -pass "generated queues go, hand-added printers stay, unaddressable names are reported" - -# The queues have to go while cups-browsed's backend is still installed. -cleanup_line=$(grep -n 'lpadmin -x' "$log" | tail -1 | cut -d: -f1 || true) -[[ -n $cleanup_line ]] || fail "the removal cleans up generated queues" "$(cat "$log")" -(( cleanup_line < removal_line )) || - fail "generated queues go before the backend that serves them" "$(cat "$log")" -pass "generated queues go before the backend that serves them" - -[[ -f $marker ]] || fail "the removal records machine-wide completion" -pass "the removal records machine-wide completion" +disable_line=$(grep -n 'disable --now' "$log" | head -1 | cut -d: -f1) +cleanup_line=$(grep -n 'lpadmin -x' "$log" | tail -1 | cut -d: -f1) +removal_line=$(grep -n 'omarchy-pkg-drop' "$log" | head -1 | cut -d: -f1) +(( disable_line < cleanup_line && cleanup_line < removal_line )) || + fail "the service and queues are handled before package removal" "$(cat "$log")" +pass "generated queues are removed, manual printers survive, and normal output is quiet" # ------------------------------------ a second user, after a deliberate reinstall -# Migration state is per user. The account that runs this after someone put -# discovery back on purpose must not quietly take it away again. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" run_migration - -[[ ! -s $log ]] || fail "a machine that already had its removal is left alone" "$(cat "$log")" +assert_quiet_success +[[ ! -s $log ]] || fail "a completed machine-wide removal is left alone" "$(cat "$log")" pass "a second user does not undo a deliberate reinstall" # ----------------------------------------- a machine that never had discovery +start_case no-package installed="" -enabled="" -active="cups.service" -use_marker="$test_tmp/var/lib/omarchy/migrations/never-had-it" run_migration - +assert_quiet_success [[ ! -s $log ]] || fail "a machine without discovery is left alone" "$(cat "$log")" -[[ ! -e $use_marker ]] || - fail "a machine with nothing to remove is not made to pay for a marker" "$(cat "$log")" -pass "a machine without discovery does no privileged work and gets no password prompt" +[[ ! -e $use_marker ]] || fail "a machine without discovery gets no marker" +pass "a machine without cups-browsed does no privileged work" + +# --------------------------------------------- a machine that has no printers + +start_case no-printers +run_migration +assert_quiet_success +grep -qxF $'omarchy-pkg-drop\tcups-browsed' "$log" || + fail "no printers does not prevent package removal" "$(cat "$log")" +grep -qE 'cupsreject|lpadmin' "$log" && + fail "no printers requires no queue administration" "$(cat "$log")" +[[ -f $use_marker ]] || fail "the no-printer migration records completion" +pass "CUPS reporting no destinations is a successful empty migration" # ------------------------------------------------- a blocked package removal -# Something depending on cups-browsed makes pacman refuse. Stopping the service -# first and only then discovering that would leave discovery broken rather than -# removed. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" +start_case blocked blocked="1" -use_marker="$test_tmp/var/lib/omarchy/migrations/blocked" run_migration - +(( status != 0 )) || fail "a blocked package removal remains pending" +assert_description_only grep -q 'disable --now' "$log" && - fail "a refused removal never stops the service" "$(cat "$log")" -if grep -q $'^pacman\t-R --noconfirm' "$log"; then - fail "a refused removal does not go on to remove anything" "$(cat "$log")" -fi -[[ ! -e $use_marker ]] || fail "a refused removal is not recorded as done" -# The preflight has to ask about the same command the removal will run. -grep -qxF $'pacman\t-R --print cups-browsed' "$log" || - fail "the preflight asks pacman about the removal it will actually run" "$(cat "$log")" -pass "a removal pacman would refuse changes nothing at all" + fail "a blocked removal does not stop discovery" "$(cat "$log")" +grep -q 'omarchy-pkg-drop' "$log" && + fail "a blocked removal does not invoke package removal" "$(cat "$log")" +[[ ! -e $use_marker ]] || fail "a blocked removal gets no marker" +pass "a package dependency prevents any partial migration" -# --------------------------------------------------- a queue that is printing +# --------------------------------------------------- a queue that has jobs -# lpadmin -x cancels the jobs on the queue it removes. A stale queue can be -# deleted whenever someone notices it; an aborted print cannot come back. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" -blocked="" +start_case busy queues=$'device for Office: implicitclass://Office/\ndevice for Spare: implicitclass://Spare/' busy="Office" -use_marker="$test_tmp/var/lib/omarchy/migrations/printing" run_migration - -# The implicitclass backend only needs cups-browsed to choose a destination, so -# a job already past that point finishes even though the daemon has stopped. -# Deleting the queue would abort it. -if grep -q 'lpadmin -x Office' "$log"; then - fail "a queue with jobs on it is left for them to finish" "$(cat "$log")" -fi +assert_quiet_success +grep -q 'lpadmin -x Office' "$log" && + fail "a queue with jobs is not deleted" "$(cat "$log")" grep -qxF $'sudo\tlpadmin -x Spare' "$log" || - fail "an idle generated queue is still removed" "$(cat "$log")" + fail "an idle generated queue is deleted" "$(cat "$log")" +grep -qxF $'omarchy-pkg-drop\tcups-browsed' "$log" || + fail "a busy queue does not retain the discovery package" "$(cat "$log")" -# A job submitted between the check and the deletion -- the sudo in between can -# sit at a password prompt -- would be cancelled by a deletion that had decided -# the queue was empty. -reject_line=$(grep -n 'cupsreject.*Spare' "$log" | head -1 | cut -d: -f1 || true) -probe_line=$(grep -n $'^lpstat\t-o Spare' "$log" | head -1 | cut -d: -f1 || true) -delete_line=$(grep -n 'lpadmin -x Spare' "$log" | head -1 | cut -d: -f1 || true) -[[ -n $reject_line && -n $probe_line && -n $delete_line ]] || - fail "a queue is closed, inspected and removed in that order" "$(cat "$log")" +reject_line=$(grep -n 'cupsreject.*Spare' "$log" | head -1 | cut -d: -f1) +probe_line=$(grep -n $'^lpstat\t-o Spare' "$log" | head -1 | cut -d: -f1) +delete_line=$(grep -n 'lpadmin -x Spare' "$log" | head -1 | cut -d: -f1) (( reject_line < probe_line && probe_line < delete_line )) || - fail "a queue stops taking new jobs before it is inspected or removed" "$(cat "$log")" -pass "a queue stops taking new jobs before it is inspected or removed" - -grep -q 'Office still has jobs' "$output" || - fail "a queue left alone is named so it can be removed later" "$(cat "$output")" -# One printer's job must not keep discovery on the machine. -grep -qxF $'sudo\tpacman -R --noconfirm cups-browsed' "$log" || - fail "a busy queue does not hold up the removal" "$(cat "$log")" -pass "a queue with jobs is left for them to finish, and does not hold up the removal" + fail "a queue is closed before it is checked and deleted" "$(cat "$log")" +pass "busy queues survive while idle discovery queues are removed" # ------------------------------------------------- a job query that fails -# Treating a failed query as an idle queue would delete it and abort whatever -# was on it, which is the one outcome this is trying to avoid. +start_case job-query-fails +queues=$'device for Office: implicitclass://Office/' lpstat_o_fails="1" -use_marker="$test_tmp/var/lib/omarchy/migrations/nojobs" run_migration - -if grep -q 'lpadmin -x' "$log"; then - fail "a queue whose jobs could not be checked is left alone" "$(cat "$log")" -fi -grep -q 'Could not check for jobs' "$output" || - fail "a job query that failed is said out loud" "$(cat "$output")" -pass "a queue whose jobs cannot be checked is left alone, not assumed idle" - -lpstat_o_fails="" +(( status != 0 )) || fail "a failed job query keeps the migration pending" +assert_description_only +grep -q 'lpadmin -x' "$log" && + fail "a queue with unknown job state is not deleted" "$(cat "$log")" +grep -q 'omarchy-pkg-drop' "$log" && + fail "a failed job query retains the package" "$(cat "$log")" +[[ ! -e $use_marker ]] || fail "a failed job query gets no marker" +pass "a failed job query is retryable instead of falsely completing" # ------------------------------------------------------ CUPS out of reach -# Failing to reach cupsd must not read like a machine with no queues to clean. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service" -busy="" +start_case no-cups +queues=$'device for Office: implicitclass://Office/' lpstat_fails="1" -use_marker="$test_tmp/var/lib/omarchy/migrations/nocups" run_migration - -if grep -q 'lpadmin -x' "$log"; then - fail "nothing is removed when the queue list could not be read" "$(cat "$log")" -fi -# Removing the backend without having read the queue list would strand every -# generated queue permanently, and the marker would stop anyone retrying. -if grep -q $'^pacman\t-R --noconfirm' "$log"; then - fail "the package waits until the queue list can be read" "$(cat "$log")" -fi -[[ ! -e $use_marker ]] || - fail "an unread queue list is not recorded as a finished removal" -grep -qi 'could not ask cups' "$output" || - fail "a queue list that could not be read is said out loud" "$(cat "$output")" -# Stopping discovery is the half that mattered, and it is idempotent. -grep -qxF $'sudo\tsystemctl disable --now cups-browsed.service' "$log" || - fail "discovery is still stopped when the queue list cannot be read" "$(cat "$log")" -pass "an unreadable queue list stops discovery but finalizes nothing" - -lpstat_fails="" +(( status != 0 )) || fail "an unavailable CUPS server keeps the migration pending" +assert_description_only +grep -qxF "lpstat: Scheduler is not responding." "$errors" || + fail "the underlying CUPS failure is preserved" "$(cat "$errors")" +grep -q 'omarchy-pkg-drop' "$log" && + fail "an unavailable CUPS server retains the package" "$(cat "$log")" +[[ ! -e $use_marker ]] || fail "an unavailable CUPS server gets no marker" +pass "an actual CUPS failure remains pending without custom telemetry" # ------------------------------------------------ a queue that will not go -# A queue left behind would route through a backend the removal is about to -# delete, so the package waits rather than stranding it for good. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" -blocked="" +start_case stuck-queue queues=$'device for Office: implicitclass://Office/' -busy="" -lpstat_fails="" lpadmin_fails="1" -use_marker="$test_tmp/var/lib/omarchy/migrations/stuck" run_migration +(( status != 0 )) || fail "a failed queue deletion keeps the migration pending" +grep -q 'omarchy-pkg-drop' "$log" && + fail "a persistent generated queue retains the backend" "$(cat "$log")" +[[ ! -e $use_marker ]] || fail "a failed queue deletion gets no marker" +pass "a persistent generated queue prevents partial completion" -if grep -q $'^pacman\t-R --noconfirm' "$log"; then - fail "the package waits while a generated queue is still there" "$(cat "$log")" -fi -[[ ! -e $use_marker ]] || fail "a half-done cleanup is not recorded as finished" -grep -q 'Could not remove the queue Office' "$output" || - fail "a queue that would not go is named" "$(cat "$output")" -pass "a queue that will not go keeps the package and the marker back" +# --------------------------------------------- a queue removed concurrently -lpadmin_fails="" - -# --------------------------------------------- a queue removed by someone else - -# Another administrator deleting the queue mid-run is the outcome wanted, not a -# failure worth keeping the package installed for. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" -blocked="" +start_case vanished-queue queues=$'device for Office: implicitclass://Office/' -busy="" -lpstat_fails="" -lpstat_o_fails="" lpadmin_fails="1" gone_queues="Office" -use_marker="$test_tmp/var/lib/omarchy/migrations/vanished" run_migration +assert_quiet_success +grep -qxF $'omarchy-pkg-drop\tcups-browsed' "$log" || + fail "a concurrently removed queue does not block package removal" "$(cat "$log")" +pass "a queue removed concurrently counts as removed" -grep -qxF $'sudo\tpacman -R --noconfirm cups-browsed' "$log" || - fail "a queue that is already gone does not hold up the removal" "$(cat "$log")" -[[ -f $use_marker ]] || fail "a queue that is already gone still finishes the migration" -pass "a queue someone else removed counts as removed" +# ------------------------------------------- a queue that cannot be closed -lpadmin_fails="" -gone_queues="" - -# ------------------------------------------- a queue that will not stop taking jobs - -# Deleting a queue that is still accepting work races whatever arrives next. -installed="cups-browsed" -enabled="cups-browsed.service" -active="cups-browsed.service cups.service" +start_case open-queue +queues=$'device for Office: implicitclass://Office/' reject_fails="1" -use_marker="$test_tmp/var/lib/omarchy/migrations/openqueue" run_migration - -if grep -q 'lpadmin -x' "$log"; then +(( status != 0 )) || fail "a queue that cannot be closed keeps the migration pending" +grep -q 'lpadmin -x' "$log" && fail "a queue still accepting jobs is not deleted" "$(cat "$log")" -fi -if grep -q $'^pacman\t-R --noconfirm' "$log"; then - fail "the package waits while a queue is still accepting jobs" "$(cat "$log")" -fi -[[ ! -e $use_marker ]] || fail "a queue still taking jobs is not recorded as done" -pass "a queue that will not stop taking jobs is neither inspected nor deleted" - -reject_fails="" +grep -q 'omarchy-pkg-drop' "$log" && + fail "a queue still accepting jobs retains the backend" "$(cat "$log")" +[[ ! -e $use_marker ]] || fail "a queue still accepting jobs gets no marker" +pass "a queue that cannot be closed is retried later" # -------------------------------------------------- a masked but running daemon -installed="cups-browsed" +start_case masked enabled="" active="cups-browsed.service" -blocked="" -queues="" -use_marker="$test_tmp/var/lib/omarchy/migrations/masked" run_migration - +assert_quiet_success grep -qxF $'sudo\tsystemctl stop cups-browsed.service' "$log" || - fail "a running unit is stopped even when it is not enabled" "$(cat "$log")" + fail "a masked running daemon is stopped" "$(cat "$log")" grep -q 'disable --now' "$log" && fail "a masked unit is not disabled" "$(cat "$log")" -pass "a masked but running daemon is stopped without being disabled" +pass "a masked running daemon is stopped without noisy output" -# A machine whose discovery never created a queue has nothing to clean up, and -# no reason to reach for CUPS administration. -grep -q 'lpadmin' "$log" && - fail "no queues means no CUPS administration" "$(cat "$log")" -pass "a machine with no generated queues does not touch CUPS administration" +# ------------------------------------------------ a package removal that fails + +start_case drop-fails +drop_fails="1" +run_migration +(( status != 0 )) || fail "a failed package removal keeps the migration pending" +assert_description_only +[[ ! -e $use_marker ]] || fail "a failed package removal gets no marker" +pass "a package removal failure cannot be recorded as complete"