#!/bin/sh
# udev-triggered shim (Doc 1 §6.4, Doc 2 §4.5, backlog M1-08, G1).
#
# It does no work itself: it hands the event to the running agent over the local
# unix socket, so all queue-naming and certification logic lives in one place
# and a udev storm cannot fork a hundred lpadmin calls.
#
# Called as `printer-event <add|remove|vendor> <name>`, from an
# imprion-printer-event@.service or imprion-usb-device@.service instance — a udev worker must never block, and
# `SYSTEMD_WANTS` is also what makes the *coldplug* work: at boot
# systemd-udev-trigger replays the events for devices that were already plugged
# in, so a printer that was never unplugged still gets its queue.
#
# udev passes a *device* name on add (3-1) and an *interface* name on remove
# (5-1:1.0). They are the same device, so the interface suffix is stripped here
# and the agent sees one identifier for both actions (G1).
#
# THE FALLBACK IS THE POINT. "The agent is never a runtime dependency of the
# PoS" (Doc 1 principle 3) has to be true of printing too: if the socket is
# missing, or the agent does not answer within two seconds, this execs the OS's
# inline implementation and the till gets its queue anyway. The two produce the
# same queue name for the same printer — pkg/CUPS/Certified.go and
# printer-event-fallback are one naming rule written twice, not two — so a
# fallback add followed by an agent add converges on one queue rather than two.
# Since F91 that rule is `<model>-<serial suffix>` for a printer that reports a
# serial and `usb_<vid>_<pid>` for one that does not; the `-2` suffix for a
# SECOND printer of one model is the agent's alone, because allocating it needs
# the live queue list and the rest of the bus, which is exactly what the
# fallback does not have when it runs.
#
# imprion-os ships the Phase A implementation at /usr/lib/imprion/printer-event;
# from Phase F the agent package owns that path and the OS implementation moves
# to /usr/lib/imprion/printer-event-fallback (G14, M6-04).
#
# **That handover has landed and is proved.** On qa8 (BIOS,
# /root/imprion-qa8/printer-bios.txt) the terminal carries this shim at
# /usr/lib/imprion/printer-event (3749 bytes, from this file) and the OS
# implementation at /usr/lib/imprion/printer-event-fallback (8816 bytes), and
# both halves of G1 were measured on one plug cycle:
#
#   add: 5-1 handed to the agent                       # the agent path
#   ...  the agent did not answer ... using the fallback
#   add: usb 5-1 04b8:0202 ... serial='QA8TEST123'     # the OS path
#   add: raw queue 'tm-t88v-test123' -> usb://EPSON/TM-T88V?serial=QA8TEST123
#
# and the agent, restarted afterwards, reconciled onto `tm-t88v-test123`
# without renaming it — the same queue name from both implementations, which is
# what pkg/CUPS/Certified.go's transcription of the OS naming exists to
# guarantee. Defect 2 of the README's M3/M4/M5 QA run ("the fallback is shipped
# by nothing") was measured against qa6, before the OS side landed.
#
# The missing-fallback branch below stays regardless. It is not dead code: an
# agent package installed on an older image, or on any host where the OS half
# has not been updated, still has to leave a working till rather than a failed
# unit on every plug.
set -u

SOCKET="${IMPRION_EVENT_SOCKET:-/run/imprion-agent/events.sock}"
FALLBACK="${IMPRION_PRINTER_EVENT_FALLBACK:-/usr/lib/imprion/printer-event-fallback}"
TAG="imprion-printer"

# The socket budget. A udev-triggered unit that waits on a wedged agent is the
# thing this whole path exists to avoid.
TIMEOUT="${IMPRION_EVENT_TIMEOUT:-2}"

log()  { logger -t "$TAG" -p daemon.info -- "$*"; }
warn() { logger -t "$TAG" -p daemon.warning -- "$*"; }

ACTION="${1:-${ACTION:-}}"
DEVICE="${2:-${DEVPATH:-}}"

# A full sysfs path collapses to its last component; 5-1:1.0 -> 5-1.
DEVICE="${DEVICE##*/}"
DEVICE="${DEVICE%%:*}"

if [ -z "$ACTION" ] || [ -z "$DEVICE" ]; then
    warn "usage: printer-event add|remove <usb-device>"
    exit 2
fi

# `vendor` is the third action (F90, README decision 223). It comes from
# 86-imprion-usb-printers.rules, which fires for a device exposing a
# vendor-class (ff) interface — the owner's SNBC 154f:154f, which no udev
# printer rule can ever match and which neither usblp nor CUPS' own usb backend
# will ever bind. It means something weaker than `add` does: `add` comes from a
# rule whose match IS the printer interface class, so udev has already
# established the device is a printer; `vendor` means only that something no
# driver will bind has appeared, and on a till that is a wifi dongle as often as
# it is a printer. The agent decides, against the fleet-wide printer registry.
case "$ACTION" in
    add|remove|vendor) ;;
    *) warn "unknown action ${ACTION}"; exit 2 ;;
esac

# curl, not nc: `netcat-openbsd` is not on the ISO package list and `curl` is
# (imprion-os config/package-lists/pos.list.chroot). curl also gives the
# two-second budget and a non-zero exit on an HTTP error for free, which is
# exactly the "the agent did not answer" test the fallback keys on.
deliver() {
    [ -S "$SOCKET" ] || return 1

    curl --silent --fail --show-error \
         --max-time "$TIMEOUT" \
         --unix-socket "$SOCKET" \
         --request POST \
         --header 'Content-Type: application/json' \
         --data "$(printf '{"action":"%s","device":"%s"}' "$ACTION" "$DEVICE")" \
         http://localhost/events/printer >/dev/null 2>&1
}

if deliver; then
    log "${ACTION}: ${DEVICE} handed to the agent"
    exit 0
fi

# THE VENDOR ACTION HAS NO FALLBACK, AND THAT IS THE CORRECT ANSWER RATHER THAN
# A GAP. The fallback is the OS's inline class-7 implementation: it looks for a
# printer-class interface, resolves a usb:// URI through CUPS' own backend and
# names the queue from the model and serial. It knows nothing about a registry,
# nothing about an `imprion-usb://` URI and nothing about a bulk-OUT endpoint,
# and the device this action exists for has no printer-class interface, no model
# and no serial — so handing it a `vendor` event would at best do nothing and at
# worst create a queue on a URI whose backend cannot reach the device.
#
# The promise the fallback keeps is about the class-7 path and is unchanged: a
# till with a dead agent still gets its queue for any ordinary USB printer. A
# vendor-class device is one that needed a decision nothing but the agent can
# make, so a vendor event the agent did not take is simply logged, and this
# exits 0 — a failed unit on every plug of a terminal whose agent happens to be
# restarting helps nobody.
if [ "$ACTION" = "vendor" ]; then
    warn "vendor: the agent did not answer on ${SOCKET} within ${TIMEOUT}s, nothing done for ${DEVICE}"
    exit 0
fi

warn "${ACTION}: the agent did not answer on ${SOCKET} within ${TIMEOUT}s, using the fallback"

if [ ! -x "$FALLBACK" ]; then
    # Before Phase F the OS still owns /usr/lib/imprion/printer-event and there
    # is no separate fallback binary to reach for. Saying so and exiting zero is
    # right: the alternative is a failed unit on every plug of a terminal whose
    # agent happens to be restarting.
    warn "${ACTION}: no fallback at ${FALLBACK}, nothing done for ${DEVICE}"
    exit 0
fi

exec "$FALLBACK" "$ACTION" "$DEVICE"
