Temporarily remove automatic printer discovery
cups-browsed is the daemon that watches the network and creates print queues by itself. Hardening it 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. Only the discovery half: CUPS itself stays and printing keeps working, with each printer added by hand in Print Settings. The migration disables the unit before removing the package because that is the only order that works: pacman deletes the unit file but not the enable symlink, and once the unit is gone systemd can no longer resolve it by name to clean that up. It then removes the queues discovery generated. cups-browsed keeps those when it stops, since KeepGeneratedQueuesOnShutdown defaults to Yes, and they route through its own implicitclass backend, which goes with the package, so they cannot print again. Idle ones go. A queue with jobs on it is left alone and named: implicitclass only needs cups-browsed to choose a destination, so a job already past that point finishes on its own, and deleting the queue would abort it. One printer's job does not hold up the removal. A printer added by hand has an ipp:// or usb:// device and is left where it is. A queue whose jobs cannot be asked about is left alone rather than assumed idle, including one named so that lpstat would misread it -- "all" is its word for every destination, and a leading dash or a comma reads as another option or a list. Where CUPS does not answer at all, or a queue will not delete, discovery is still stopped but the package stays and no marker is written. omarchy-migrate records a migration for the user as soon as it exits zero, so that is where the machine stays until someone removes the package by hand, and the message says so rather than implying a retry. The queue list is read under LC_ALL=C because lpstat translates "device for", and captured rather than piped, so a cupsd it cannot reach is reported instead of reading like a machine with nothing to clean up. It removes with plain pacman -R rather than omarchy-pkg-drop, which passes -n and would discard /etc/cups/cups-browsed.conf instead of keeping it as a .pacsave. A removal meant to be temporary should not delete the machine's copy of its own configuration. Without -s either, so it only ever removes the package it names: sweeping newly unneeded dependencies is nothing today, but it is not a promise a rolling dependency graph can keep. Queue names come off the network, since cups-browsed names its queues after what the printer advertised. CUPS allows every printable character but space, tab, / and #, and lpstat and lpadmin take a destination as an option value, so a name with a leading dash or a comma is reported rather than passed to them and guessed at. Migration state is per user, so a machine-wide marker records the one removal. Without it, an account whose first migration run came after someone deliberately reinstalled discovery would quietly take it back out again. The install-time override for cups-browsed.conf now waits for cups-browsed rather than for CUPS. Guarding it on a file CUPS still ships would write a configuration file for a package nothing installed, and pacman would later land the package's own copy beside it as a .pacnew. The hardened configuration stays in the tree. omarchy-settings still ships the cups-browsed.conf override, the sysusers account and the service drop-in, so they are what discovery returns onto. Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
co-authored by
Codex XHigh
parent
0b3f1b7ead
commit
24c18df5b7
@@ -1364,7 +1364,6 @@ EOF
|
||||
as_root systemctl disable docker.service >/dev/null 2>&1 || true
|
||||
|
||||
enable_system_service cups.service
|
||||
enable_system_service cups-browsed.service
|
||||
enable_system_service avahi-daemon.service
|
||||
enable_system_service linux-modules-cleanup.service
|
||||
enable_system_service docker.socket
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# Enable services only. Installs are followed by reboot, so don't start/reload
|
||||
# daemons mid-install. UFW and hardware-gated services stay in their own scripts.
|
||||
systemctl enable cups.service
|
||||
systemctl enable cups-browsed.service
|
||||
systemctl enable avahi-daemon.service
|
||||
systemctl enable linux-modules-cleanup.service
|
||||
systemctl enable docker.socket
|
||||
systemctl enable systemd-resolved.service
|
||||
systemctl enable NetworkManager.service
|
||||
# Don't let network-online.target (pulled in by cups-browsed) hold up
|
||||
# graphical.target waiting for DHCP/Wi-Fi association. Nothing in the session
|
||||
# needs to block on the network. Mirrors the systemd-networkd-wait-online mask
|
||||
# in install/hardware/network.sh.
|
||||
# Don't let network-online.target hold up graphical.target waiting for
|
||||
# DHCP/Wi-Fi association. Nothing in the session needs to block on the network.
|
||||
# Mirrors the systemd-networkd-wait-online mask in install/hardware/network.sh.
|
||||
systemctl mask NetworkManager-wait-online.service
|
||||
systemctl enable power-profiles-daemon.service
|
||||
systemctl enable sddm.service
|
||||
|
||||
@@ -17,7 +17,6 @@ chromium
|
||||
clang
|
||||
cliamp
|
||||
cups
|
||||
cups-browsed
|
||||
cups-filters
|
||||
cups-pk-helper
|
||||
ddcutil
|
||||
|
||||
@@ -3,13 +3,22 @@
|
||||
cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf
|
||||
cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist
|
||||
|
||||
# omarchy-settings skips these overrides until CUPS is actually present to
|
||||
# avoid pacman creating .pacnew files during ISO package installation.
|
||||
if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -f /etc/cups/cups-files.conf ]]; then
|
||||
# Each override waits for the package that owns the file it replaces, the way
|
||||
# omarchy-settings does, so pacman does not turn it into a .pacnew during ISO
|
||||
# package installation.
|
||||
if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-files.conf && -f /etc/cups/cups-files.conf ]]; then
|
||||
install -m 0640 -o root -g cups "$OMARCHY_PATH/etc-overrides/cups-cups-files.conf" /etc/cups/cups-files.conf
|
||||
rm -f /etc/cups/cups-files.conf.pacnew
|
||||
fi
|
||||
|
||||
# cups-browsed is no longer part of the default install, so this waits for a
|
||||
# machine that adds discovery back by hand. Writing the override before then
|
||||
# would leave a configuration file for a package nothing installed, and pacman
|
||||
# would later land the package's own copy beside it as a .pacnew.
|
||||
if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -f /etc/cups/cups-browsed.conf ]]; then
|
||||
systemd-sysusers /etc/sysusers.d/omarchy-cups-browsed.conf
|
||||
cp -f "$OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf" /etc/cups/cups-browsed.conf
|
||||
install -m 0640 -o root -g cups "$OMARCHY_PATH/etc-overrides/cups-cups-files.conf" /etc/cups/cups-files.conf
|
||||
rm -f /etc/cups/cups-browsed.conf.pacnew /etc/cups/cups-files.conf.pacnew
|
||||
rm -f /etc/cups/cups-browsed.conf.pacnew
|
||||
fi
|
||||
|
||||
source "$OMARCHY_INSTALL/hardware/pacman.sh"
|
||||
|
||||
+7
-1
@@ -44,7 +44,13 @@ The plain open source Chromium build doesn't ship with the OAuth credentials tha
|
||||
|
||||
### How do I add a printer?
|
||||
|
||||
Printing is set up and running out of the box, so a printer on your network is usually already discovered. Launch _Print Settings_ from the app launcher (`Super + Space`) to see what's there, add one by hand, or set the default. Printing to a PDF file works without any printer at all.
|
||||
Printing is set up and running out of the box, and you add each printer yourself from _Print Settings_ in the app launcher (`Super + Space`).
|
||||
|
||||
Choose _Add_, and give it a moment to look: a printer plugged in over USB, and most network printers, are found for you. If yours isn't in the list, pick _Network Printer > Internet Printing Protocol (ipp)_ and enter its address — the printer's own display or its web page will tell you what that is, usually something like `192.168.1.50` with a queue of `ipp/print`. _Forward_ then offers a driver, where a modern printer works best on the driverless _IPP Everywhere_ profile and an older one wants the model's own driver.
|
||||
|
||||
Right-click a printer and choose _Set as Default_ to pick which one your apps reach for first, and _Properties_ to set paper size, duplex and quality.
|
||||
|
||||
Automatic discovery, where printers on the network appear without being added, is temporarily switched off while it's reworked, which is why the first step above is yours rather than automatic. Printing to a PDF file works without any printer at all.
|
||||
|
||||
### How do I change where screenshots or screenrecordings are saved?
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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
|
||||
|
||||
# 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.
|
||||
if systemctl is-enabled --quiet cups-browsed.service 2>/dev/null; then
|
||||
sudo systemctl disable --now cups-browsed.service
|
||||
elif systemctl is-active --quiet cups-browsed.service 2>/dev/null; then
|
||||
sudo systemctl stop cups-browsed.service
|
||||
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.
|
||||
#
|
||||
# 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
|
||||
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
|
||||
|
||||
# Removing a queue aborts what is printing on it. The implicitclass backend
|
||||
# only needs cups-browsed to pick a destination, so a job already past that
|
||||
# point finishes on its own even though the daemon has stopped -- and a job
|
||||
# that has not is one this cannot route anyway. Either way the queue is left
|
||||
# for the person whose job it is, and named so they know to remove it.
|
||||
if job_report=$(LC_ALL=C lpstat -o "$queue" 2>/dev/null); then
|
||||
if [[ -n $job_report ]]; then
|
||||
echo " $queue still has jobs, so it is being left alone."
|
||||
echo " Once they finish or are cancelled, remove it in Print Settings; it cannot print again."
|
||||
continue
|
||||
fi
|
||||
else
|
||||
echo " Could not check for jobs on $queue, so it is being left alone."
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! sudo lpadmin -x "$queue"; then
|
||||
echo " Could not remove the queue $queue."
|
||||
unremoved=1
|
||||
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.
|
||||
sudo install -Dm644 /dev/null "$machine_marker"
|
||||
@@ -47,7 +47,7 @@ verify_services() {
|
||||
local unit
|
||||
|
||||
for unit in \
|
||||
avahi-daemon.service cups.service cups-browsed.service docker.socket \
|
||||
avahi-daemon.service cups.service docker.socket \
|
||||
NetworkManager.service power-profiles-daemon.service sddm.service \
|
||||
systemd-resolved.service ufw.service; do
|
||||
systemctl is-enabled --quiet "$unit" || fail "core system services are enabled" "$unit is not enabled"
|
||||
@@ -65,29 +65,24 @@ verify_services() {
|
||||
}
|
||||
|
||||
verify_printing_security() {
|
||||
local cups_browsed_pid lpinfo_output printer_name printer_process printer_tmp
|
||||
local lpinfo_output
|
||||
|
||||
! pacman -Q cups-pdf >/dev/null 2>&1 || fail "CUPS-PDF is absent"
|
||||
pass "the root CUPS-PDF backend is not installed"
|
||||
|
||||
getent passwd cups-browsed >/dev/null || fail "the cups-browsed service account exists"
|
||||
[[ $(systemctl show -P User cups-browsed.service) == "cups-browsed" ]] ||
|
||||
fail "cups-browsed runs as its service account"
|
||||
[[ $(systemctl show -P Group cups-browsed.service) == "cups-browsed" ]] ||
|
||||
fail "cups-browsed runs as its service group"
|
||||
systemctl is-active --quiet cups-browsed.service || fail "cups-browsed is running"
|
||||
# Automatic discovery is temporarily out of the default install: a daemon that
|
||||
# turns anything advertising itself on the network into a print queue is more
|
||||
# exposure than the convenience is worth while it is reworked. CUPS itself
|
||||
# stays, so what a stock machine proves here is that printing runs and that
|
||||
# the desktop user still cannot administer it without authenticating.
|
||||
! pacman -Q cups-browsed >/dev/null 2>&1 || fail "automatic printer discovery is not installed"
|
||||
! systemctl is-enabled --quiet cups-browsed.service 2>/dev/null ||
|
||||
fail "no discovery service is enabled"
|
||||
! systemctl is-active --quiet cups-browsed.service 2>/dev/null ||
|
||||
fail "no discovery service is running"
|
||||
pass "automatic printer discovery is absent from a stock install"
|
||||
|
||||
cups_browsed_pid=$(systemctl show -P MainPID cups-browsed.service)
|
||||
[[ -r /proc/$cups_browsed_pid/status ]] || fail "cups-browsed has a readable process status"
|
||||
[[ $(awk '/^Uid:/{print $2}' "/proc/$cups_browsed_pid/status") != 0 ]] ||
|
||||
fail "cups-browsed does not run with root UID"
|
||||
[[ $(awk '/^CapEff:/{print $2}' "/proc/$cups_browsed_pid/status") == "0000000000000000" ]] ||
|
||||
fail "cups-browsed has no effective Linux capabilities"
|
||||
|
||||
[[ $(stat -c '%a %U:%G' /var/cache/cups-browsed) == "750 cups-browsed:cups-browsed" ]] ||
|
||||
fail "cups-browsed has an isolated cache" "$(stat -c '%a %U:%G' /var/cache/cups-browsed)"
|
||||
[[ " $(id -nG cups-browsed) " != *" cups "* ]] ||
|
||||
fail "cups-browsed is separate from the print-filter group"
|
||||
systemctl is-active --quiet cups.service || fail "CUPS is running"
|
||||
|
||||
if lpinfo_output=$(LC_ALL=C timeout 10 lpinfo -v </dev/null 2>&1); then
|
||||
fail "the desktop user cannot administer CUPS without authentication"
|
||||
@@ -95,39 +90,7 @@ verify_printing_security() {
|
||||
fail "CUPS explicitly denies unauthenticated desktop administration" "$lpinfo_output"
|
||||
fi
|
||||
|
||||
pass "CUPS discovery is isolated from root, filters, and passwordless desktop administration"
|
||||
|
||||
# A live driverless printer proves the non-root daemon can still discover and
|
||||
# create queues without the CAP_NET_BIND_SERVICE Ubuntu carries downstream.
|
||||
printer_name="OmarchyAcceptancePrinter"
|
||||
printer_tmp=$(mktemp -d)
|
||||
printf '#!/bin/bash\nexit 0\n' >"$printer_tmp/command"
|
||||
chmod 0700 "$printer_tmp/command"
|
||||
mkdir -m 0700 "$printer_tmp/spool"
|
||||
|
||||
ippeveprinter -p 18631 -d "$printer_tmp/spool" -c "$printer_tmp/command" "$printer_name" \
|
||||
>"$printer_tmp/ippeveprinter.log" 2>&1 &
|
||||
printer_process=$!
|
||||
|
||||
printing_test_cleanup() {
|
||||
kill "$printer_process" >/dev/null 2>&1 || true
|
||||
wait "$printer_process" >/dev/null 2>&1 || true
|
||||
rm -rf "$printer_tmp"
|
||||
}
|
||||
trap printing_test_cleanup EXIT
|
||||
|
||||
for _ in {1..30}; do
|
||||
lpstat -v "$printer_name" 2>/dev/null | grep -q "implicitclass://$printer_name/" && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
lpstat -v "$printer_name" 2>/dev/null | grep -q "implicitclass://$printer_name/" ||
|
||||
fail "non-root cups-browsed discovers a driverless IPP printer" "$(<"$printer_tmp/ippeveprinter.log")"
|
||||
|
||||
printing_test_cleanup
|
||||
trap - EXIT
|
||||
|
||||
pass "non-root cups-browsed still creates driverless IPP queues without capabilities"
|
||||
pass "CUPS runs with passwordless desktop administration still denied"
|
||||
}
|
||||
|
||||
verify_runtime_tools() {
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1788009111.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
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.
|
||||
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/pacman" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'pacman\t%s\n' "$*" >>"$BROWSED_LOG"
|
||||
[[ $* == *--print* ]] || exit 0
|
||||
[[ -z $BROWSED_REMOVAL_BLOCKED ]]
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'systemctl\t%s\n' "$*" >>"$BROWSED_LOG"
|
||||
unit=${*: -1}
|
||||
case $1 in
|
||||
is-enabled) [[ " $BROWSED_ENABLED " == *" $unit "* ]] ;;
|
||||
is-active) [[ " $BROWSED_ACTIVE " == *" $unit "* ]] ;;
|
||||
*) : ;;
|
||||
esac
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'sudo\t%s\n' "$*" >>"$BROWSED_LOG"
|
||||
exec "$@"
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/install" <<'SH'
|
||||
#!/bin/bash
|
||||
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.
|
||||
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
|
||||
printf '%s\n' "$BROWSED_QUEUES"
|
||||
else
|
||||
printf '%s\n' "$BROWSED_QUEUES" | sed 's|^device for |Gerät für |'
|
||||
fi
|
||||
;;
|
||||
-o)
|
||||
[[ -z $BROWSED_LPSTAT_O_FAILS ]] || exit 1
|
||||
# Jobs are listed per queue: "<queue>-<id> <user> <size>".
|
||||
[[ " $BROWSED_BUSY_QUEUES " == *" $2 "* ]] && printf '%s-7 alice 1024\n' "$2"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/lpadmin" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'lpadmin\t%s\n' "$*" >>"$BROWSED_LOG"
|
||||
[[ -z $BROWSED_LPADMIN_FAILS ]]
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
log="$test_tmp/actions.log"
|
||||
output="$test_tmp/migration.out"
|
||||
marker="$test_tmp/var/lib/omarchy/migrations/1788009111"
|
||||
|
||||
run_migration() {
|
||||
: >"$log"
|
||||
: >"$output"
|
||||
|
||||
# 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 \
|
||||
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:-}" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_BROWSED_REMOVAL_MARKER="${use_marker:-$marker}" \
|
||||
bash -euo pipefail "$migration" >"$output"
|
||||
}
|
||||
|
||||
# ------------------------------------------------ a machine that has discovery
|
||||
|
||||
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"
|
||||
run_migration
|
||||
|
||||
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")"
|
||||
grep -q 'lpadmin -x Desk' "$log" &&
|
||||
fail "a printer added by hand over ipp survives" "$(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")"
|
||||
|
||||
# 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"
|
||||
|
||||
# ------------------------------------ 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")"
|
||||
pass "a second user does not undo a deliberate reinstall"
|
||||
|
||||
# ----------------------------------------- a machine that never had discovery
|
||||
|
||||
installed=""
|
||||
enabled=""
|
||||
active="cups.service"
|
||||
use_marker="$test_tmp/var/lib/omarchy/migrations/never-had-it"
|
||||
run_migration
|
||||
|
||||
[[ ! -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"
|
||||
|
||||
# ------------------------------------------------- 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"
|
||||
blocked="1"
|
||||
use_marker="$test_tmp/var/lib/omarchy/migrations/blocked"
|
||||
run_migration
|
||||
|
||||
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"
|
||||
|
||||
# --------------------------------------------------- a queue that is printing
|
||||
|
||||
# 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=""
|
||||
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
|
||||
grep -qxF $'sudo\tlpadmin -x Spare' "$log" ||
|
||||
fail "an idle generated queue is still removed" "$(cat "$log")"
|
||||
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"
|
||||
|
||||
# ------------------------------------------------- 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.
|
||||
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=""
|
||||
|
||||
# ------------------------------------------------------ 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=""
|
||||
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=""
|
||||
|
||||
# ------------------------------------------------ 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=""
|
||||
queues=$'device for Office: implicitclass://Office/'
|
||||
busy=""
|
||||
lpstat_fails=""
|
||||
lpadmin_fails="1"
|
||||
use_marker="$test_tmp/var/lib/omarchy/migrations/stuck"
|
||||
run_migration
|
||||
|
||||
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"
|
||||
|
||||
lpadmin_fails=""
|
||||
|
||||
# -------------------------------------------------- a masked but running daemon
|
||||
|
||||
installed="cups-browsed"
|
||||
enabled=""
|
||||
active="cups-browsed.service"
|
||||
blocked=""
|
||||
queues=""
|
||||
use_marker="$test_tmp/var/lib/omarchy/migrations/masked"
|
||||
run_migration
|
||||
|
||||
grep -qxF $'sudo\tsystemctl stop cups-browsed.service' "$log" ||
|
||||
fail "a running unit is stopped even when it is not enabled" "$(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"
|
||||
|
||||
# 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"
|
||||
@@ -10,11 +10,38 @@ cups_files_conf="$ROOT/etc/cups/cups-files.conf"
|
||||
sysusers_conf="$ROOT/etc/sysusers.d/omarchy-cups-browsed.conf"
|
||||
service_dropin="$ROOT/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf"
|
||||
|
||||
grep -qxF cups-browsed "$packages" || fail "cups-browsed remains in the base package set"
|
||||
# Only discovery goes. Everything else printing needs stays, or this stops
|
||||
# being a removal of one daemon and becomes a removal of printing.
|
||||
grep -qxF cups "$packages" || fail "CUPS itself remains in the base package set"
|
||||
grep -qxF cups-filters "$packages" || fail "the CUPS filters remain in the base package set"
|
||||
grep -qxF system-config-printer "$packages" || fail "Print Settings remains in the base package set"
|
||||
grep -qxF cups-pk-helper "$packages" || fail "Polkit printer administration is installed"
|
||||
! grep -qxF cups-pdf "$packages" || fail "the root CUPS-PDF backend is removed"
|
||||
|
||||
pass "the base install keeps discovery and replaces CUPS-PDF with Polkit administration"
|
||||
# Automatic discovery is temporarily out of the default install while it is
|
||||
# reworked. The hardened configuration below still ships: it is what a
|
||||
# hand-installed cups-browsed gets, and what discovery comes back onto.
|
||||
! grep -qxF cups-browsed "$packages" || fail "automatic printer discovery is out of the base package set"
|
||||
! grep -q 'cups-browsed' "$ROOT/install/config/enable-services.sh" ||
|
||||
fail "a fresh install does not enable a discovery service it no longer installs"
|
||||
! grep -q 'enable_system_service cups-browsed' "$ROOT/bin/omarchy-upgrade-to-quattro" ||
|
||||
fail "the Quattro upgrade does not enable a discovery service it no longer installs"
|
||||
|
||||
pass "the base install keeps CUPS and Polkit administration, without automatic discovery"
|
||||
|
||||
# The install-time override for a file has to wait for the package that owns
|
||||
# it. CUPS still ships /etc/cups/cups-files.conf, so a guard on that file no
|
||||
# longer says anything about cups-browsed: writing its override on a machine
|
||||
# without the package leaves a configuration file for a package nothing
|
||||
# installed, and pacman lands its own copy beside it as a .pacnew later.
|
||||
post_install_pacman="$ROOT/install/post-install/pacman.sh"
|
||||
|
||||
grep -q 'cups-cups-browsed.conf && -f /etc/cups/cups-browsed.conf' "$post_install_pacman" ||
|
||||
fail "the discovery override waits for the package that owns the file it replaces"
|
||||
grep -q 'cups-cups-files.conf && -f /etc/cups/cups-files.conf' "$post_install_pacman" ||
|
||||
fail "the CUPS authorization override waits for the file it replaces"
|
||||
|
||||
pass "install-time overrides wait for the packages that own their files"
|
||||
|
||||
grep -qxF 'CacheDir /var/cache/cups-browsed' "$cups_browsed_conf" ||
|
||||
fail "cups-browsed keeps state outside the print-filter cache"
|
||||
|
||||
Reference in New Issue
Block a user