Add real first-time provisioning to install.sh (v2.14.0)

Until now ./install.sh only managed an already-installed kiosk;
ubuntu-based-kiosk.sh was still the only path from a bare Ubuntu Server
box to a running one. install.sh now provisions from scratch too:
packages, kiosk user, Node.js/Electron, LightDM+Openbox autologin,
audio/video/HDMI/power-button hardware setup, and the firewall, then
hands off to the already-migrated Core Settings/Advanced menus for
initial configuration instead of reimplementing that logic again.

- lib/provision.sh: the new provisioning flow, built mostly by calling
  existing menus (core_settings_menu, emergency hotspot, virtual
  consoles) - cuts it to ~300 lines against the legacy script's
  ~4,000-line first_time_install().
- lib/electron.sh: electron_install_binary() extracted out of
  menus/advanced_electron.sh so provisioning and the existing "Fix
  blank screen" action share one implementation.
- kiosk-app/ and provision/files/: the Electron app source and every
  system template file, extracted byte-for-byte out of
  ubuntu-based-kiosk.sh's heredocs into real files.
- Found and fixed a bash set -e gotcha along the way: testing a
  multi-statement function as an if-condition (`if ! some_func; then`)
  silently exempts everything inside that function from set -e for the
  duration of the call. Fixed in the new provisioning code and in
  menus/advanced_electron.sh's pre-existing repair action, which had
  the same shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
This commit is contained in:
Claude
2026-08-19 05:24:32 +00:00
parent 1c9447bc5d
commit 037a62e008
33 changed files with 4209 additions and 99 deletions
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Assumes DISPLAY/XAUTHORITY are set (for xrandr) and pactl already has a
# working PipeWire/pulse socket for the invoking context.
QUERY=$(xrandr --query 2>/dev/null)
[ -z "$QUERY" ] && exit 0
PRIMARY_OUTPUT=$(echo "$QUERY" | awk '/ primary/{print $1; exit}')
[ -z "$PRIMARY_OUTPUT" ] && exit 0
EXTERNAL_CONNECTED=$(echo "$QUERY" | awk -v p="$PRIMARY_OUTPUT" '/ connected/ && $1!=p{f=1} END{print (f==1)?"yes":"no"}')
HDMI_SINK=$(pactl list sinks short 2>/dev/null | awk 'tolower($2) ~ /hdmi/{print $2; exit}')
NON_HDMI_SINK=$(pactl list sinks short 2>/dev/null | awk 'tolower($2) !~ /hdmi/{print $2; exit}')
route_to() {
local sink="$1" label="$2"
if [ -z "$sink" ]; then
logger "KIOSK: no $label audio sink found, leaving routing unchanged"
return
fi
pactl set-default-sink "$sink" 2>/dev/null \
&& logger "KIOSK: audio routed to $label sink ($sink)" \
|| logger "KIOSK: failed to route audio to $label sink ($sink)"
pactl list sink-inputs short 2>/dev/null | awk '{print $1}' | while read -r sid; do
pactl move-sink-input "$sid" "$sink" 2>/dev/null
done
pactl set-sink-volume "$sink" 100% 2>/dev/null
pactl set-sink-mute "$sink" 0 2>/dev/null
}
if [ "$EXTERNAL_CONNECTED" = "yes" ]; then
route_to "$HDMI_SINK" "HDMI"
else
route_to "$NON_HDMI_SINK" "built-in"
fi
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Give X a moment to finish enumerating the output after the hotplug event
sleep 2
sudo -u kiosk DISPLAY=:0 XAUTHORITY=/home/kiosk/.Xauthority /usr/local/bin/kiosk-mirror-display.sh
kiosk_uid=$(id -u kiosk)
sudo -u kiosk DISPLAY=:0 XAUTHORITY=/home/kiosk/.Xauthority XDG_RUNTIME_DIR="/run/user/${kiosk_uid}" /usr/local/bin/kiosk-audio-route.sh
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Assumes it's run with DISPLAY/XAUTHORITY already set for the kiosk user's
# X session (either inherited, as from Openbox autostart, or exported by the
# caller). Mirrors every connected non-primary output at the primary's exact
# current resolution so kiosk content isn't cropped/letterboxed/blank on a
# TV/monitor with a different native resolution than the kiosk panel.
QUERY=$(xrandr --query 2>/dev/null)
[ -z "$QUERY" ] && exit 0
PRIMARY_OUTPUT=$(echo "$QUERY" | awk '/ primary/{print $1; exit}')
[ -z "$PRIMARY_OUTPUT" ] && exit 0
PRIMARY_RES=$(echo "$QUERY" | awk -v p="$PRIMARY_OUTPUT" '$1==p{for(i=1;i<=NF;i++) if ($i ~ /^[0-9]+x[0-9]+\+/){split($i,a,"+"); print a[1]; exit}}')
[ -z "$PRIMARY_RES" ] && exit 0
for OUT in $(echo "$QUERY" | awk '/ connected/{print $1}'); do
[ "$OUT" = "$PRIMARY_OUTPUT" ] && continue
HAS_NATIVE=$(echo "$QUERY" | awk -v out="$OUT" -v res="$PRIMARY_RES" '
$0 ~ "^"out" " {infound=1; next}
/^[^ \t]/ {infound=0}
infound && $1==res {print "yes"; exit}
')
if [ "$HAS_NATIVE" = "yes" ]; then
xrandr --output "$OUT" --mode "$PRIMARY_RES" --same-as "$PRIMARY_OUTPUT" 2>/dev/null \
&& logger "KIOSK: mirrored $OUT at native $PRIMARY_RES" \
|| logger "KIOSK: mirror of $OUT at $PRIMARY_RES failed"
continue
fi
# $OUT doesn't natively list the primary's resolution - force a matching mode
CVT_LINE=$(cvt "${PRIMARY_RES%x*}" "${PRIMARY_RES#*x}" 2>/dev/null | grep Modeline)
MODENAME=$(echo "$CVT_LINE" | sed -n 's/^Modeline "\([^"]*\)".*/\1/p')
TIMINGS=$(echo "$CVT_LINE" | sed -n 's/^Modeline "[^"]*" *//p')
if [ -z "$MODENAME" ] || [ -z "$TIMINGS" ]; then
logger "KIOSK: could not generate a $PRIMARY_RES mode for $OUT (cvt failed or missing)"
continue
fi
xrandr --newmode "$MODENAME" $TIMINGS 2>/dev/null
xrandr --addmode "$OUT" "$MODENAME" 2>/dev/null
xrandr --output "$OUT" --mode "$MODENAME" --same-as "$PRIMARY_OUTPUT" 2>/dev/null \
&& logger "KIOSK: mirrored $OUT at forced $PRIMARY_RES ($MODENAME)" \
|| logger "KIOSK: mirror of $OUT at forced $PRIMARY_RES failed"
done
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Power button handler - sends SIGUSR1 to Electron to show power menu
# This runs as ROOT from acpid, so it can signal any process
logger "KIOSK POWER: Button pressed"
# Find the Electron main process (runs as kiosk user)
PIDS=$(pgrep -u kiosk -f "electron" 2>/dev/null)
if [ -z "$PIDS" ]; then
logger "KIOSK POWER: No Electron process found"
exit 1
fi
# Send SIGUSR1 to all Electron processes (the main one will handle it)
for PID in $PIDS; do
logger "KIOSK POWER: Sending SIGUSR1 to PID $PID"
kill -USR1 $PID 2>/dev/null
done
logger "KIOSK POWER: Signal sent"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
CURRENT=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oE '[0-9]+%' | head -1 | tr -d '%')
NEW=$((CURRENT - 5))
[[ $NEW -lt 0 ]] && NEW=0
pactl set-sink-volume @DEFAULT_SINK@ ${NEW}%
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
CURRENT=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oE '[0-9]+%' | head -1 | tr -d '%')
NEW=$((CURRENT + 5))
[[ $NEW -gt 100 ]] && NEW=100
pactl set-sink-volume @DEFAULT_SINK@ ${NEW}%
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
echo "Testing power button configuration..."
echo
echo "1. Checking acpid service..."
if systemctl is-active --quiet acpid; then
echo " ✓ acpid is running"
else
echo " ✗ acpid is NOT running"
echo " Fix: sudo systemctl enable --now acpid"
fi
echo
echo "2. Checking ACPI event handler..."
if [ -f /etc/acpi/events/kiosk-power-button ]; then
echo " ✓ Event handler exists"
cat /etc/acpi/events/kiosk-power-button
else
echo " ✗ Event handler not found"
fi
echo
echo "3. Checking power button script..."
if [ -x /usr/local/bin/kiosk-power-button.sh ]; then
echo " ✓ Script exists and is executable"
else
echo " ✗ Script not found or not executable"
fi
echo
echo "4. Checking Electron process..."
PIDS=$(pgrep -u kiosk -f "electron" 2>/dev/null)
if [ -n "$PIDS" ]; then
echo " ✓ Found Electron PIDs: $PIDS"
else
echo " ✗ No Electron process found"
fi
echo
echo "5. Testing power button trigger NOW..."
if [ -x /usr/local/bin/kiosk-power-button.sh ]; then
echo " Running: /usr/local/bin/kiosk-power-button.sh"
/usr/local/bin/kiosk-power-button.sh
echo " Check if power menu appeared!"
else
echo " Script not found"
fi
echo
echo "6. To watch ACPI events: sudo acpi_listen"
echo " Then press power button and look for 'button/power' events"