#!/bin/bash set -euo pipefail # Configure the one explicitly managed church printer. The Ricoh Aficio MP # C3502 supports PCL 6 as standard; Debian Trixie's compressed Foomatic # database supplies this generic color PCL 6/PCL XL path without vendor code. QUEUE="Elmer" LEGACY_QUEUE="Classroom_Color" DEFAULT_URI="socket://10.10.0.155:9100" DRIVER="foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd" DESCRIPTION="Elmer" # Blank on purpose. CUPS shows Location under the queue name in every # print dialog, and the teacher-facing identity of this printer is # simply "Elmer". LOCATION="" MAKE_DEFAULT=true COMMAND_TIMEOUT=10 usage() { cat <<'USAGE' Usage: classroom-printer-setup [--no-default] [PRINTER-IP-OR-URI] Examples: sudo classroom-printer-setup sudo classroom-printer-setup 10.10.0.155 sudo classroom-printer-setup socket://printer.example.org:9100 With no address, configures the church Ricoh as Elmer at socket://10.10.0.155:9100. An IP address or hostname uses JetDirect/AppSocket on port 9100. Explicit socket://, ipp://, ipps:// and lpd:// URIs are also accepted. USAGE } log() { echo "[classroom-printer-setup] $1" } fail() { log "ERROR: $1" >&2 exit 1 } if [[ "${CLASSROOM_PRINTER_SETUP_TESTING:-false}" != "true" && $EUID -ne 0 ]]; then fail "run this command as root: sudo classroom-printer-setup" fi if [[ "${1:-}" == "--no-default" ]]; then MAKE_DEFAULT=false shift fi if [[ $# -gt 1 || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage [[ $# -le 1 ]] && exit 0 exit 2 fi address="${1:-$DEFAULT_URI}" case "$address" in socket://*|ipp://*|ipps://*|lpd://*) uri="$address" ;; *://*) fail "unsupported printer URI scheme: $address" ;; *) if [[ ! "$address" =~ ^[A-Za-z0-9._:-]+$ ]]; then fail "printer address contains unsupported characters: $address" fi uri="socket://${address}:9100" ;; esac for command in timeout lpadmin lpinfo lpoptions lpstat cupsenable cupsaccept; do command -v "$command" >/dev/null 2>&1 || \ fail "$command is missing; reinstall the ekklesia-classroom package" done run_bounded() { timeout \ --signal=TERM \ --kill-after=2s \ "${COMMAND_TIMEOUT}s" \ "$@" } disable_discovery() { command -v systemctl >/dev/null 2>&1 || return 0 # cups-browsed creates local queues for advertised printers. CUPS can also # enumerate DNS-SD destinations through Avahi even without cups-browsed, # so disable both Avahi activation paths on this single-purpose appliance. run_bounded systemctl disable --now cups-browsed.service \ >/dev/null 2>&1 || true run_bounded systemctl disable --now \ avahi-daemon.socket avahi-daemon.service >/dev/null 2>&1 || true run_bounded systemctl mask \ avahi-daemon.socket avahi-daemon.service >/dev/null 2>&1 || true } disable_discovery if ! run_bounded lpstat -r >/dev/null 2>&1; then command -v systemctl >/dev/null 2>&1 && \ run_bounded systemctl start cups.service >/dev/null 2>&1 || true # systemctl can return as the service begins starting. Give the local # scheduler a short, bounded chance to create its socket. for _ in {1..20}; do run_bounded lpstat -r >/dev/null 2>&1 && break sleep 0.25 done fi run_bounded lpstat -r >/dev/null 2>&1 || \ fail "CUPS is not running; check: systemctl status cups" if run_bounded lpstat -p "$LEGACY_QUEUE" >/dev/null 2>&1; then log "removing legacy ${LEGACY_QUEUE} queue" run_bounded lpadmin -x "$LEGACY_QUEUE" || \ fail "CUPS could not remove legacy queue ${LEGACY_QUEUE}" fi device="$(run_bounded lpstat -v "$QUEUE" 2>/dev/null || true)" status="$(run_bounded lpstat -p "$QUEUE" 2>/dev/null || true)" accepting="$(run_bounded lpstat -a "$QUEUE" 2>/dev/null || true)" default="$(run_bounded lpstat -d 2>/dev/null || true)" details="$(run_bounded lpstat -l -p "$QUEUE" 2>/dev/null || true)" options="$(run_bounded lpoptions -p "$QUEUE" -l 2>/dev/null || true)" if grep -Fq ": ${uri}" <<< "$device" \ && grep -Fq "printer ${QUEUE}" <<< "$status" \ && grep -Fq " enabled " <<< " ${status} " \ && grep -Fq "${QUEUE} accepting requests" <<< "$accepting" \ && grep -Fq "system default destination: ${QUEUE}" <<< "$default" \ && grep -Fq "Description: ${DESCRIPTION}" <<< "$details" \ && grep -Eq '^[[:space:]]*Location:[[:space:]]*$' <<< "$details" \ && grep -Eq '^PrintoutMode/.*:.*\*Normal([[:space:]]|$)' <<< "$options" \ && grep -Eq '^ColorModel/.*:.*\*Color([[:space:]]|$)' <<< "$options"; then log "${QUEUE} is already correctly configured." exit 0 fi models="$(run_bounded lpinfo -m)" || \ fail "CUPS could not list installed printer drivers" if ! grep -q "^${DRIVER} " <<< "$models"; then fail "the Debian generic color PCL 6 driver is not installed" fi log "configuring ${QUEUE} at ${uri}" # Repeating this command updates the same named queue; it never creates a # second destination. None of these operations connects to the printer. run_bounded lpadmin \ -p "$QUEUE" \ -v "$uri" \ -m "$DRIVER" \ -D "$DESCRIPTION" \ -L "$LOCATION" || fail "CUPS could not configure ${QUEUE}" # CUPS on Trixie does not reliably retain PPD choices supplied in the same # request that selects the driver. Apply the physically verified defaults in a # second request after the PPD exists. run_bounded lpadmin \ -p "$QUEUE" \ -o PrintoutMode=Normal \ -o ColorModel=Color || fail "CUPS could not set the color defaults for ${QUEUE}" options="$(run_bounded lpoptions -p "$QUEUE" -l 2>/dev/null || true)" grep -Eq '^PrintoutMode/.*:.*\*Normal([[:space:]]|$)' <<< "$options" \ || fail "CUPS did not retain PrintoutMode=Normal for ${QUEUE}" grep -Eq '^ColorModel/.*:.*\*Color([[:space:]]|$)' <<< "$options" \ || fail "CUPS did not retain ColorModel=Color for ${QUEUE}" run_bounded cupsenable "$QUEUE" || fail "CUPS could not enable ${QUEUE}" run_bounded cupsaccept "$QUEUE" || fail "CUPS could not accept jobs for ${QUEUE}" if [[ "$MAKE_DEFAULT" == true ]]; then run_bounded lpadmin -d "$QUEUE" || \ fail "CUPS could not make ${QUEUE} the default" fi log "${QUEUE} is configured, enabled and accepting jobs." if [[ "$MAKE_DEFAULT" == true ]]; then log "${QUEUE} is the system default printer." fi log "Print defaults: PrintoutMode=Normal, ColorModel=Color." log "The printer may remain offline until it is reachable; this does not affect boot."