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:
@@ -0,0 +1,10 @@
|
||||
Section "ServerFlags"
|
||||
# Disable Ctrl+Alt+Backspace (X server kill)
|
||||
Option "DontZap" "true"
|
||||
|
||||
# Disable VT switching (Ctrl+Alt+F1-F12)
|
||||
Option "DontVTSwitch" "true"
|
||||
|
||||
# Don't allow clients to disconnect on exit
|
||||
Option "AllowClosedownGrabs" "false"
|
||||
EndSection
|
||||
@@ -0,0 +1,7 @@
|
||||
Section "Device"
|
||||
Identifier "Intel Graphics"
|
||||
Driver "intel"
|
||||
Option "AccelMethod" "sna"
|
||||
Option "TearFree" "true"
|
||||
Option "DRI" "3"
|
||||
EndSection
|
||||
@@ -0,0 +1,5 @@
|
||||
Section "InputClass"
|
||||
Identifier "Touch screen use libinput"
|
||||
MatchIsTouchscreen "on"
|
||||
Driver "libinput"
|
||||
EndSection
|
||||
@@ -0,0 +1,2 @@
|
||||
event=button/power.*
|
||||
action=/usr/local/bin/kiosk-power-button.sh
|
||||
@@ -0,0 +1,2 @@
|
||||
event=button/power PBTN
|
||||
action=/usr/local/bin/kiosk-power-button.sh
|
||||
@@ -0,0 +1,2 @@
|
||||
event=button/power PWRF
|
||||
action=/usr/local/bin/kiosk-power-button.sh
|
||||
@@ -0,0 +1,6 @@
|
||||
[Allow kiosk power operations]
|
||||
Identity=unix-user:kiosk
|
||||
Action=org.freedesktop.login1.power-off;org.freedesktop.login1.power-off-multiple-sessions;org.freedesktop.login1.reboot;org.freedesktop.login1.reboot-multiple-sessions;org.freedesktop.login1.suspend;org.freedesktop.login1.suspend-multiple-sessions
|
||||
ResultAny=yes
|
||||
ResultInactive=yes
|
||||
ResultActive=yes
|
||||
@@ -0,0 +1,6 @@
|
||||
[Login]
|
||||
HandlePowerKey=ignore
|
||||
HandlePowerKeyLongPress=poweroff
|
||||
HandleSuspendKey=ignore
|
||||
HandleHibernateKey=ignore
|
||||
HandleLidSwitch=ignore
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Kiosk Display Hotplug Handler
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/kiosk-hotplug.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
@@ -0,0 +1 @@
|
||||
SUBSYSTEM=="drm", ACTION=="change", TAG+="systemd", ENV{SYSTEMD_WANTS}="kiosk-hotplug.service"
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Mirror any connected external display (e.g. HDMI-out to a monitor/TV) onto
|
||||
# the primary display, forcing it to the primary's exact resolution.
|
||||
/usr/local/bin/kiosk-mirror-display.sh
|
||||
|
||||
# AGGRESSIVE DPMS disable - multiple methods
|
||||
xset s off
|
||||
xset s noblank
|
||||
xset -dpms
|
||||
xset s 0 0
|
||||
xset dpms 0 0 0
|
||||
xset dpms force on
|
||||
|
||||
# Keep screen on forever - watchdog (schedule-aware)
|
||||
(
|
||||
while true; do
|
||||
sleep 300 # Every 5 minutes
|
||||
|
||||
# Check if display schedule is active
|
||||
schedule_active=false
|
||||
if systemctl is-active --quiet kiosk-display-off.timer && systemctl is-active --quiet kiosk-display-on.timer; then
|
||||
# Get display off and on times from systemd timers
|
||||
doff=$(systemctl cat kiosk-display-off.timer 2>/dev/null | grep "^OnCalendar=" | sed 's/.*\*-\*-\* //' | sed 's/:00$//')
|
||||
don=$(systemctl cat kiosk-display-on.timer 2>/dev/null | grep "^OnCalendar=" | sed 's/.*\*-\*-\* //' | sed 's/:00$//')
|
||||
|
||||
if [ -n "$doff" ] && [ -n "$don" ]; then
|
||||
# Get current time in HH:MM format
|
||||
current_time=$(date +%H:%M)
|
||||
|
||||
# Convert times to minutes since midnight for comparison
|
||||
# Using 10# prefix to force decimal interpretation (fixes octal bug for 08:xx and 09:xx times)
|
||||
current_mins=$(( 10#$(date +%H) * 60 + 10#$(date +%M) ))
|
||||
off_mins=$(( 10#$(echo "$doff" | cut -d: -f1) * 60 + 10#$(echo "$doff" | cut -d: -f2) ))
|
||||
on_mins=$(( 10#$(echo "$don" | cut -d: -f1) * 60 + 10#$(echo "$don" | cut -d: -f2) ))
|
||||
|
||||
# Check if we're in the "display off" window
|
||||
if [ "$off_mins" -lt "$on_mins" ]; then
|
||||
# Normal case: off time is before on time (e.g., 22:00 to 06:00 next day)
|
||||
if [ "$current_mins" -ge "$off_mins" ] && [ "$current_mins" -lt "$on_mins" ]; then
|
||||
schedule_active=true
|
||||
fi
|
||||
else
|
||||
# Overnight case: off time is after on time (e.g., 06:00 to 22:00)
|
||||
if [ "$current_mins" -ge "$off_mins" ] || [ "$current_mins" -lt "$on_mins" ]; then
|
||||
schedule_active=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only force display on if NOT in scheduled off period
|
||||
if [ "$schedule_active" = "false" ]; then
|
||||
xset s reset 2>/dev/null
|
||||
xset dpms force on 2>/dev/null
|
||||
fi
|
||||
done
|
||||
) &
|
||||
|
||||
# Start PipeWire user services
|
||||
systemctl --user start pipewire pipewire-pulse wireplumber
|
||||
sleep 3
|
||||
|
||||
# Wait for PipeWire to be ready
|
||||
for i in {1..15}; do
|
||||
pactl info >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait for ALSA devices
|
||||
for i in {1..10}; do
|
||||
pactl list sinks short | grep -q alsa && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Route audio to HDMI if an external display is connected/mirrored, else built-in
|
||||
/usr/local/bin/kiosk-audio-route.sh
|
||||
|
||||
# Set audio levels (speakers 100%, mic 100%, mic unmuted)
|
||||
pactl set-sink-volume @DEFAULT_SINK@ 100%
|
||||
pactl set-source-volume @DEFAULT_SOURCE@ 100%
|
||||
pactl set-source-mute @DEFAULT_SOURCE@ 0
|
||||
|
||||
# Audio watchdog - checks every 30 seconds (quiet hours aware)
|
||||
(
|
||||
while true; do
|
||||
sleep 30
|
||||
|
||||
# Check if quiet hours is active
|
||||
quiet_active=false
|
||||
if systemctl is-active --quiet kiosk-quiet-start.timer && systemctl is-active --quiet kiosk-quiet-end.timer; then
|
||||
# Get quiet hours start and end times from systemd timers
|
||||
qstart=$(systemctl cat kiosk-quiet-start.timer 2>/dev/null | grep "^OnCalendar=" | sed 's/.*\*-\*-\* //' | sed 's/:00$//')
|
||||
qend=$(systemctl cat kiosk-quiet-end.timer 2>/dev/null | grep "^OnCalendar=" | sed 's/.*\*-\*-\* //' | sed 's/:00$//')
|
||||
|
||||
if [ -n "$qstart" ] && [ -n "$qend" ]; then
|
||||
# Convert times to minutes since midnight for comparison
|
||||
# Using 10# prefix to force decimal interpretation (fixes octal bug for 08:xx and 09:xx times)
|
||||
current_mins=$(( 10#$(date +%H) * 60 + 10#$(date +%M) ))
|
||||
start_mins=$(( 10#$(echo "$qstart" | cut -d: -f1) * 60 + 10#$(echo "$qstart" | cut -d: -f2) ))
|
||||
end_mins=$(( 10#$(echo "$qend" | cut -d: -f1) * 60 + 10#$(echo "$qend" | cut -d: -f2) ))
|
||||
|
||||
# Check if we're in quiet hours window
|
||||
if [ "$start_mins" -lt "$end_mins" ]; then
|
||||
# Normal case: start time is before end time (e.g., 08:00 to 17:00)
|
||||
if [ "$current_mins" -ge "$start_mins" ] && [ "$current_mins" -lt "$end_mins" ]; then
|
||||
quiet_active=true
|
||||
fi
|
||||
else
|
||||
# Overnight case: start time is after end time (e.g., 22:00 to 07:00)
|
||||
if [ "$current_mins" -ge "$start_mins" ] || [ "$current_mins" -lt "$end_mins" ]; then
|
||||
quiet_active=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if PipeWire is running
|
||||
if ! pactl info >/dev/null 2>&1; then
|
||||
logger "KIOSK: Audio dead, restarting PipeWire"
|
||||
systemctl --user restart pipewire pipewire-pulse wireplumber
|
||||
sleep 5
|
||||
# Only restore audio levels if NOT in quiet hours
|
||||
if [ "$quiet_active" = "false" ]; then
|
||||
pactl set-sink-volume @DEFAULT_SINK@ 100%
|
||||
pactl set-source-volume @DEFAULT_SOURCE@ 100%
|
||||
pactl set-source-mute @DEFAULT_SOURCE@ 0
|
||||
fi
|
||||
fi
|
||||
|
||||
done
|
||||
) &
|
||||
|
||||
# Other services
|
||||
unclutter -idle 0.1 -root &
|
||||
XDG_RUNTIME_DIR=/run/user/$(id -u) xbindkeys &
|
||||
|
||||
# Create boot flag for password requirement on boot
|
||||
touch /home/kiosk/kiosk-app/.boot-flag
|
||||
|
||||
# Start kiosk app AFTER audio is ready
|
||||
sleep 2
|
||||
/home/kiosk/kiosk-app/start.sh &
|
||||
@@ -0,0 +1,33 @@
|
||||
# PipeWire noise cancellation configuration for kiosk microphone
|
||||
# This creates a virtual source with echo cancellation and noise suppression
|
||||
|
||||
context.modules = [
|
||||
{ name = libpipewire-module-echo-cancel
|
||||
args = {
|
||||
# audio.channels = 1
|
||||
# capture.props = {
|
||||
# node.name = "Echo Cancellation Capture"
|
||||
# }
|
||||
# source.props = {
|
||||
# node.name = "Echo Cancellation Source"
|
||||
# node.description = "Noise-Cancelled Microphone"
|
||||
# }
|
||||
# sink.props = {
|
||||
# node.name = "Echo Cancellation Sink"
|
||||
# }
|
||||
# playback.props = {
|
||||
# node.name = "Echo Cancellation Playback"
|
||||
# }
|
||||
aec.method = webrtc
|
||||
aec.args = {
|
||||
# WebRTC audio processing settings
|
||||
webrtc.gain_control = true
|
||||
webrtc.extended_filter = true
|
||||
webrtc.high_pass_filter = true
|
||||
webrtc.noise_suppression = true
|
||||
webrtc.noise_suppression_level = 3
|
||||
webrtc.voice_detection = true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+36
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
Reference in New Issue
Block a user