Files
ubuntu-based-kiosk/menus/addon_cups.sh
T
Claude 1b16bcf3ee Migrate CUPS Printing addon; restructure install.sh into Core Settings/Addons/Advanced; bump to v2.5.0
First Addon migrated: menus/addon_cups.sh (install, reconfigure for
network access, complete uninstall/purge). Different risk profile from
everything migrated so far - it genuinely mutates real system state
(apt install/remove --purge, /etc/cups, ufw) at fixed paths CUPS itself
doesn't let us relocate, unlike the systemd/cron/bin paths this project
already controls via $SYSTEMD_DIR etc. Only the polkit rule's directory
is parameterized ($POLKIT_DIR, lib/config.sh, since that one is ours to
place); everything else gets full command-level `sudo` stubbing in
every test - there is no scratch equivalent for a real apt-managed
subsystem's own file layout. Also added $BUILD_USER (the admin account
actually running the tool, as opposed to $KIOSK_USER) since CUPS needs
to grant it lpadmin group membership.

Restructured install.sh's top-level menu into Core Settings / Addons /
Advanced (matching the legacy tool) instead of one flat list, now that
Addons exists as its own category - cheap to do with one item in it,
much more annoying to retrofit once the flat list has fifteen.

Two bugs caught and fixed before they shipped:

- A "wait for CUPS to start" retry loop used a bare `cmd1 && cmd2 &&
  break` as its body while "simplifying" the legacy script's `if cmd1
  && cmd2; then break; fi`. Being inside a loop doesn't protect a bare
  &&/|| list from set -e - only if/while/until conditions and the
  protected side of &&/|| do that - so the first command failing on an
  early iteration (near-certain right after a fresh install, before
  CUPS has actually started) would have crashed the entire session.
  Restored the `if` form; noted the lesson in the file's own header
  comment since it's a general trap, not CUPS-specific.

- Resolved real uncertainty, rather than assuming: how far does
  run_menu's `handler || true` guard (v2.1.0) actually protect? Wrote a
  minimal isolated test (a bare `false` three function calls deep,
  called via `outer || true` at the top) and confirmed bash's errexit
  exemption for the left side of `||` covers the *entire* evaluation,
  arbitrarily deep through function calls - not just the immediately
  invoked function. So the session-crash risk this project has been
  chasing since v2.1.0 is already covered end-to-end by that one fix.
  Per-statement guards (`|| true`, explicit `if`) still earn their keep
  for a different reason: without them a deep failure bubbles silently
  past the menu actually responsible for it to wherever the nearest
  `|| true` happens to sit, which can be several menu levels above
  where the user actually was - not a crash, but a confusing jump.

Verified:
- Full regression: re-ran every existing scratch-config/stub test suite
  after the lib/config.sh change (new $BUILD_USER/$POLKIT_DIR) and
  after the install.sh restructuring - all still clean.
- New scratch/stub test for addon_cups.sh: full state-machine coverage
  (not installed -> decline -> install -> running -> reconfigure ->
  stopped -> start -> uninstall decline -> uninstall confirm -> not
  installed again) with every `sudo` call intercepted and only `rm`
  targeting the scratch $POLKIT_DIR ever actually executed; confirmed
  the polkit rule's content and that declining install makes zero sudo
  calls. Added both apt-failure paths (update fails, install fails)
  and confirmed the tool reports clearly and returns to the menu
  instead of dying, exercising the exact bug class just fixed.
- End-to-end: ran the real install.sh as a genuine non-root, non-
  "kiosk" user through the full new three-level structure - Core
  Settings -> Sites -> back -> back, Addons -> CUPS -> declined install
  (using this container's real, unstubbed dpkg check, correctly
  reporting "not installed" and making no apt/systemctl calls) -> back
  -> back, Advanced -> Diagnostics -> System status -> back -> back ->
  Exit. Zero invalid-choice errors, clean exit code 0 throughout.
2026-08-18 19:10:43 +00:00

157 lines
5.5 KiB
Bash

#!/bin/bash
################################################################################
# menus/addon_cups.sh - "CUPS Printing" addon (from the legacy Addons menu).
#
# First Addon migrated. Genuinely mutates real system state - installs/
# purges apt packages, writes /etc/cups/cupsd.conf and a polkit rule,
# touches ufw - at fixed paths CUPS itself doesn't let us relocate the
# way $SYSTEMD_DIR/$CRON_D_DIR/etc let us relocate our own files. Only
# the polkit rule's directory is parameterized ($POLKIT_DIR, since that's
# ours to place); everything else (cupsd.conf, apt, systemctl, ufw) gets
# full command-level `sudo` stubbing in every test - there is no scratch
# equivalent for a real apt-managed subsystem's own file layout.
#
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################
cups_is_installed() {
dpkg -l 2>/dev/null | grep -q "^ii\s\+cups\s"
}
cups_is_active() {
systemctl is-active --quiet cups
}
addon_cups_status() {
if cups_is_installed && cups_is_active; then
echo "CUPS: installed and running (http://$(get_ip_address):631)"
elif cups_is_installed; then
echo "CUPS: installed but not running"
else
echo "CUPS: not installed"
fi
}
addon_cups_menu_builder() {
if cups_is_installed && cups_is_active; then
MENU_LABELS=("Reconfigure for network access" "Complete uninstall (purge)")
MENU_HANDLERS=(action_reconfigure_cups action_cups_uninstall)
elif cups_is_installed; then
MENU_LABELS=("Start CUPS" "Complete uninstall (purge)")
MENU_HANDLERS=(action_start_cups action_cups_uninstall)
else
MENU_LABELS=("Install CUPS printing")
MENU_HANDLERS=(action_install_cups)
fi
}
addon_cups_menu() {
run_menu "CUPS PRINTING SUPPORT" addon_cups_menu_builder addon_cups_status
}
################################################################################
# Actions
################################################################################
action_install_cups() {
echo
ask_yes_no "Install CUPS printing?" "n" || { echo "Cancelled"; return; }
echo "Installing CUPS from scratch..."
if ! sudo apt update; then
log_error "apt update failed - check network/package sources and try again"
return 1
fi
if ! sudo apt install -y cups cups-client cups-filters printer-driver-all \
printer-driver-cups-pdf hplip printer-driver-gutenprint \
foomatic-db-compressed-ppds openprinting-ppds; then
log_error "CUPS package installation failed"
return 1
fi
sudo systemctl enable cups 2>/dev/null || true
sudo systemctl start cups 2>/dev/null || true
echo "Waiting for CUPS to start..."
for _ in {1..30}; do
# Must stay in an `if` - a bare `cmd1 && cmd2` statement is
# subject to set -e itself when cmd1 fails, which is virtually
# guaranteed on early iterations right after install.
if cups_is_active && lpstat -r &>/dev/null 2>&1; then
break
fi
sleep 1
done
# $BUILD_USER already resolves to $SUDO_USER when the tool was run via
# sudo, so a single usermod covers it - the legacy code ran this twice
# (once for a hardcoded computed user, once again for $SUDO_USER
# directly), which was harmless but genuinely redundant.
sudo usermod -aG lpadmin "$BUILD_USER"
action_reconfigure_cups
log_success "CUPS installed"
echo " Web interface: http://$(get_ip_address):631"
}
action_start_cups() {
sudo systemctl enable cups
sudo systemctl start cups
log_success "CUPS started"
}
action_reconfigure_cups() {
if [[ -f /etc/cups/cupsd.conf ]]; then
sudo cp /etc/cups/cupsd.conf "/etc/cups/cupsd.conf.backup-$(date +%Y%m%d-%H%M%S)"
fi
if command -v cupsctl &>/dev/null; then
sudo cupsctl --remote-admin --remote-any --share-printers 2>/dev/null || true
fi
sudo sed -i 's/^Listen localhost:631/Port 631/' /etc/cups/cupsd.conf 2>/dev/null || true
sudo sed -i 's/^Listen 127.0.0.1:631/Port 631/' /etc/cups/cupsd.conf 2>/dev/null || true
sudo mkdir -p "$POLKIT_DIR"
sudo tee "$POLKIT_DIR/kiosk-printing.pkla" > /dev/null <<EOF
[Allow kiosk printing]
Identity=unix-user:${KIOSK_USER}
Action=org.opensuse.cupspkhelper.mechanism.*
ResultAny=yes
ResultInactive=yes
ResultActive=yes
EOF
sudo ufw allow 631/tcp comment 'CUPS' 2>/dev/null || true
sudo systemctl restart cups 2>/dev/null || true
log_success "CUPS configured for network access"
}
action_cups_uninstall() {
echo
ask_yes_no "Completely remove CUPS, including all queues and settings (purge)?" "n" || { echo "Cancelled"; return; }
echo "Performing complete CUPS uninstall..."
sudo systemctl stop cups cups-browsed 2>/dev/null || true
sudo systemctl disable cups cups-browsed 2>/dev/null || true
sudo apt remove --purge -y cups cups-daemon cups-client cups-filters \
cups-common cups-core-drivers cups-server-common cups-browsed \
cups-ppdc cups-bsd libcups2 libcupsimage2 2>/dev/null || true
sudo apt remove --purge -y printer-driver-all printer-driver-cups-pdf \
hplip printer-driver-gutenprint foomatic-db-compressed-ppds \
openprinting-ppds 2>/dev/null || true
sudo rm -rf /etc/cups /var/cache/cups /var/spool/cups /var/log/cups /usr/share/cups
sudo rm -f "$POLKIT_DIR/kiosk-printing.pkla"
sudo apt autoremove -y
sudo apt clean
log_success "CUPS completely removed"
}