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.
This commit is contained in:
Claude
2026-08-18 19:10:43 +00:00
parent 459da53182
commit 1b16bcf3ee
5 changed files with 258 additions and 17 deletions
+16 -5
View File
@@ -1,6 +1,6 @@
# Ubuntu Based Kiosk
**Current Version:** 2.4.0 (check script header for latest version)
**Current Version:** 2.5.0 (check script header for latest version)
**Built with Claude Sonnet 4.6 AI assistance**
**License:** GPL v3 - Keep derivatives open source
**Repository:** https://github.com/outis1one/ubuntu-based-kiosk/
@@ -1205,7 +1205,12 @@ terminal menu and the web UI, so they can't drift apart).
- `menus/diagnostics.sh`**Diagnostics**: system status, log viewing,
audio diagnostics, network test — 4 of the legacy Advanced menu's 12
items, all read-only.
- `install.sh` — entry point for the modular tool. Run it against an
- `menus/addon_cups.sh`**CUPS Printing** (Addons): install,
reconfigure for network access, complete uninstall (purge). The first
Addon migrated — genuinely mutates real system state (apt packages,
`/etc/cups`, ufw) rather than this project's own files.
- `install.sh` — entry point for the modular tool, now grouped **Core
Settings / Addons / Advanced** like the legacy menu. Run it against an
*already-installed* kiosk:
```bash
git clone https://github.com/outis1one/ubuntu-based-kiosk/
@@ -1216,7 +1221,7 @@ terminal menu and the web UI, so they can't drift apart).
**Honest status:** this does not yet replace first-time installation, or
most of the old installer. `ubuntu-based-kiosk.sh` is still ~12,000
lines and still contains its own unremoved, unmodified copies of every
menu above (plus Upgrade, Reinstall, Uninstall, all Addons, and the
menu above (plus Upgrade, Reinstall, Uninstall, 4 more Addons, and the
other 8 Advanced items — none of that has moved yet). Both copies
coexist deliberately: the old ones stay until enough
of Core Settings/Addons/Advanced is migrated to
@@ -1229,9 +1234,15 @@ at all.
## Project Status & Future Plans
**Current Version:** 2.4.0
**Current Version:** 2.5.0
**Recent Updates (v2.4.0):**
**Recent Updates (v2.5.0):**
- **First Addon migrated:** CUPS Printing — install/reconfigure/complete uninstall, in `./install.sh`. Genuinely mutates real system state (`apt install`/`remove --purge`, `/etc/cups`, `ufw`) at fixed paths CUPS itself doesn't let us relocate, so every test uses full command-level `sudo` stubbing rather than the scratch-directory approach used for this project's own files.
- **Menu restructured:** `install.sh`'s top level is now grouped Core Settings / Addons / Advanced, matching the legacy tool, instead of one flat list — done now while it's cheap, ahead of the list getting unwieldy.
- **Bug fix:** a "wait for service to start" retry loop used a bare `cmd1 && cmd2 && break` as its body — that's not made safe by being inside a loop; a bare `&&`/`||` list used as a standalone statement is fully subject to `set -e`, and the first command failing on an early iteration (near-certain right after a fresh install) would have killed the whole session. Restored the `if cmd1 && cmd2; then break; fi` form.
- **Resolved:** real uncertainty about how far `run_menu`'s `handler || true` guard (added in v2.1.0) actually reaches — confirmed with an isolated test that it protects against a failing command no matter how many function calls deep, so the session-crash risk chased since v2.1.0 is already covered end-to-end by that one fix. Per-statement guards still matter for a different reason: without them, a deep failure bubbles past the menu actually responsible for it to wherever the nearest `|| true` happens to catch it.
**Previous (v2.4.0):**
- **Diagnostics migrated** — system status, log viewing (Electron/LightDM/journal), an 8-step audio diagnostic, and a ping+DNS network test, from the legacy Advanced menu. A change of pace: everything here is read-only, no destructive-action risk to manage.
- **Bug fix (set -e safety):** every diagnostic whose failure is the expected case — no lightdm running, no audio hardware, no network, missing logs, `ping`/`nslookup` not even installed — was a bare unguarded statement that would have crashed the whole session instead of reporting "not found" and moving on. Fixed throughout; a diagnostics tool has to survive exactly the broken states it exists to diagnose.
- Manual Electron Update, Factory Reset, Export/Import Settings, Emergency Hotspot, and Fix Blank Screen are staying in the legacy script for now — destructive/mutating, and some share Upgrade's coupling to the legacy script's self-extraction mechanism (see v2.3.0 notes).
+40 -10
View File
@@ -12,12 +12,13 @@
# at a time, so a change to (say) the Sites menu can't accidentally break
# WiFi setup or the uninstaller three thousand lines away.
#
# Migrated so far: Sites & Page Timing (menus/sites.sh), Display &
# Interaction (menus/display.sh), Timezone (menus/timezone.sh), Hidden
# Site PIN (menus/hidden_pin.sh), Password Protection & Lockout
# (menus/lockout.sh), WiFi (menus/wifi.sh), Power/Display/Quiet Hours
# (menus/power_schedule.sh), Diagnostics (menus/diagnostics.sh - system
# status/logs/audio/network from the legacy Advanced menu).
# Migrated so far, grouped the same way the legacy menu groups them:
# Core Settings: Sites & Page Timing, Display & Interaction, Timezone,
# Hidden Site PIN, Password Protection & Lockout, WiFi,
# Power/Display/Quiet Hours.
# Addons: CUPS Printing (menus/addon_cups.sh).
# Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/
# audio/network).
#
# Usage (once the kiosk has already been installed):
# git clone <repo>
@@ -49,6 +50,8 @@ source "$SCRIPT_DIR/menus/wifi.sh"
source "$SCRIPT_DIR/menus/power_schedule.sh"
# shellcheck source=menus/diagnostics.sh
source "$SCRIPT_DIR/menus/diagnostics.sh"
# shellcheck source=menus/addon_cups.sh
source "$SCRIPT_DIR/menus/addon_cups.sh"
################################################################################
# Preflight
@@ -81,10 +84,12 @@ if ! is_kiosk_installed; then
fi
################################################################################
# Top-level menu
# Top-level menu - grouped the same way the legacy menu groups them
# (Core Settings / Addons / Advanced), so the structure stays familiar
# and the flat list doesn't grow unwieldy as more menus migrate in.
################################################################################
main_menu_builder() {
core_settings_menu_builder() {
MENU_LABELS=(
"Sites & Page Timing"
"Display & Interaction"
@@ -93,7 +98,6 @@ main_menu_builder() {
"Password Protection & Lockout"
"WiFi"
"Power/Display/Quiet Hours"
"Diagnostics"
)
MENU_HANDLERS=(
sites_menu
@@ -103,10 +107,36 @@ main_menu_builder() {
lockout_menu
wifi_menu
power_schedule_menu
diagnostics_menu
)
}
core_settings_menu() {
run_menu "CORE SETTINGS" core_settings_menu_builder
}
addons_menu_builder() {
MENU_LABELS=("CUPS Printing")
MENU_HANDLERS=(addon_cups_menu)
}
addons_menu() {
run_menu "ADDONS" addons_menu_builder
}
advanced_menu_builder() {
MENU_LABELS=("Diagnostics")
MENU_HANDLERS=(diagnostics_menu)
}
advanced_menu() {
run_menu "ADVANCED" advanced_menu_builder
}
main_menu_builder() {
MENU_LABELS=("Core Settings" "Addons" "Advanced")
MENU_HANDLERS=(core_settings_menu addons_menu advanced_menu)
}
main_menu_status() {
echo "Managing kiosk at: ${KIOSK_DIR}"
}
+6
View File
@@ -29,6 +29,12 @@
: "${CRON_D_DIR:=/etc/cron.d}"
: "${BIN_DIR:=/usr/local/bin}"
: "${NETPLAN_DIR:=/etc/netplan}"
: "${POLKIT_DIR:=/etc/polkit-1/localauthority/50-local.d}"
# The admin account actually running this tool (as opposed to $KIOSK_USER,
# the kiosk's own restricted account) - used where an addon needs to grant
# *this* user a group membership (e.g. lpadmin for CUPS).
: "${BUILD_USER:=${SUDO_USER:-$(whoami)}}"
# Site/tab arrays
declare -a URLS=()
+156
View File
@@ -0,0 +1,156 @@
#!/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"
}
+40 -2
View File
@@ -1,8 +1,46 @@
#!/bin/bash
################################################################################
### Ubuntu Based Kiosk v2.4.0 ###
### Ubuntu Based Kiosk v2.5.0 ###
################################################################################
#
# RELEASE v2.5.0 - First Addon Migrated (CUPS), Menu Restructured
# - New in ./install.sh: CUPS Printing (menus/addon_cups.sh) - the first
# Addon migrated. Install/reconfigure/complete uninstall (purge),
# genuinely mutating 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 controls, there is no
# scratch equivalent for a real apt-managed subsystem's own file
# layout, so every test uses full command-level `sudo` stubbing
# instead. Only the polkit rule's directory is parameterized
# ($POLKIT_DIR, since that one is ours to place).
# - install.sh's top-level menu is now grouped the same way the legacy
# menu groups things - Core Settings / Addons / Advanced - instead of
# one flat list, ahead of that list getting unwieldy as more Addons
# and Advanced items migrate in.
# - Two bugs caught and fixed before they ever shipped, both instructive
# beyond this one file:
# - A "wait for service to start" retry loop used a bare `cmd1 &&
# cmd2 && break` as its body. That's not safe merely because it's
# inside a loop - a bare &&/|| list used as a standalone statement
# (not the condition of if/while/until) is fully subject to set -e,
# and cmd1 failing on an early iteration (near-certain right after
# a fresh install) would have killed the whole session. Restored
# the `if cmd1 && cmd2; then break; fi` form the legacy script
# already used correctly, rather than "simplifying" it away.
# - Resolved real uncertainty about how far run_menu's `handler ||
# true` guard (added in v2.1.0) actually reaches: verified with a
# minimal isolated test that it protects against a bare failing
# command no matter how many function calls deep it occurs - bash's
# errexit exemption for the left side of `||` covers the entire
# evaluation, not just the immediately-called 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 matter for a different reason:
# without them a deep failure silently bubbles up past the menu
# that's actually responsible for it to wherever the nearest `||
# true` happens to catch it, which may be several menu levels
# higher than where the user actually was.
#
# RELEASE v2.4.0 - Diagnostics Migrated
# - New in ./install.sh: Diagnostics (menus/diagnostics.sh) - system
# status, log viewing (Electron/LightDM/journal), an 8-step audio
@@ -188,7 +226,7 @@ set -euo pipefail
### SECTION 1: CONSTANTS & GLOBALS
################################################################################
SCRIPT_VERSION="2.4.0"
SCRIPT_VERSION="2.5.0"
# Resolve the real path to this script file.
# When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,