Offline bundle now carries every addon that works on a network without internet: LMS (Lyrion's own .deb, from the addon's LMS_DEB_URL) + Squeezelite, Asterisk Intercom (baresip), VNC, WireGuard and the Emergency Hotspot packages - each list in a variable next to the code that installs it (SQUEEZELITE_APT_PACKAGES, ASTERISK_INTERCOM_APT_PACKAGES, VNC_APT_PACKAGES, WIREGUARD_APT_PACKAGES, EMERGENCY_HOTSPOT_APT_PACKAGES), read by build-bundle.sh. Each install goes through run_with_offline_fallback like CUPS, so they install offline from the normal menus / Web UI. LMS installs as a bundle package when offline (lms_offline_install_wanted); its online repo/direct-download paths are unchanged. Tailscale/Netbird deliberately not bundled (vendor scripts, vendor servers). Bundle: 524 MB (+87 MB). Emergency hotspot: the boot check pinged only 8.8.8.8, so on a LAN with no internet the hotspot started on every boot. Now chosen at setup: "network" (default - router or internet must answer; a router that ignores ping still counts if it answered ARP), "internet" (old check), or "host" (a specific address). Three tries 10s apart. Existing scripts keep the old check until Reconfigure. New Core Settings -> Time Server (NTP) (menus/time_sync.sh): point systemd-timesyncd at LAN time server(s) via a timesyncd.conf.d drop-in, back to Ubuntu's defaults, or set the clock by hand (and the RTC) for a network with no time server. Complete Uninstall resets it. Also: lms_service_name used `systemctl list-unit-files | grep -q`, the same pipefail/SIGPIPE pattern as the earlier cups_is_installed bug; now `systemctl cat`. Tested in a network-less chroot of the 26.04.1 server filesystem: kiosk base, then action_wireguard_install and action_install_lms (real functions) and the Squeezelite/Intercom/VNC/hotspot package sets all installed from the bundle, apt left on normal sources after each. Hotspot script generated for all three modes (plus an invalid address, which falls back to "network") and its check run against simulated networks: LAN-only with router answering ping or only ARP -> no hotspot in "network" mode; nothing reachable -> hotspot. Time Server actions driven with timedatectl stubbed: drop-in contents, input validation, manual set-time sequence, reset removes the drop-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
367 lines
12 KiB
Bash
367 lines
12 KiB
Bash
#!/bin/bash
|
||
################################################################################
|
||
# menus/advanced_emergency_hotspot.sh - "Emergency Hotspot" (Advanced):
|
||
# auto-starts a WiFi hotspot if the kiosk finds itself cut off 60 seconds
|
||
# after boot, so it can be reached and reconfigured remotely.
|
||
# "Cut off" is chosen at setup (EMERGENCY_HOTSPOT_CHECK_MODE in the
|
||
# generated script): by default the local network itself is unreachable -
|
||
# neither the router nor the internet answers - so a kiosk on a LAN with
|
||
# no internet (an offline install) doesn't start the hotspot on every
|
||
# boot. The old internet-only check (ping 8.8.8.8) is still an option, as
|
||
# is checking one specific address. Scripts generated before this choice
|
||
# existed keep the internet-only check until Reconfigure is run.
|
||
#
|
||
# Writes a standalone runtime script ($BIN_DIR/kiosk-emergency-hotspot)
|
||
# plus a oneshot systemd unit ($SYSTEMD_DIR) that runs it at boot - both
|
||
# of those paths are ours to place, so (like power_schedule and every
|
||
# other addon) they're parameterized instead of hardcoded. hostapd/
|
||
# dnsmasq/iptables themselves are real apt packages with their own fixed
|
||
# config locations, stubbed at the command level in tests like CUPS.
|
||
#
|
||
# The runtime script itself is a template: everything written with `\$`
|
||
# below stays literal and only resolves when the script actually runs at
|
||
# boot (on the real machine, not in this tool); only the un-escaped
|
||
# $wifi_iface/$hotspot_ssid/$hotspot_pass/$hotspot_ip/$KIOSK_USER/
|
||
# $KIOSK_DIR are substituted once, at configuration time.
|
||
#
|
||
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||
################################################################################
|
||
|
||
EMERGENCY_HOTSPOT_SCRIPT="$BIN_DIR/kiosk-emergency-hotspot"
|
||
# Also read by iso/offline/build-bundle.sh to bundle them for offline installs.
|
||
EMERGENCY_HOTSPOT_APT_PACKAGES=(hostapd dnsmasq iptables)
|
||
|
||
emergency_hotspot_is_configured() {
|
||
[[ -f "$EMERGENCY_HOTSPOT_SCRIPT" ]]
|
||
}
|
||
|
||
emergency_hotspot_ssid() {
|
||
grep '^HOTSPOT_SSID=' "$EMERGENCY_HOTSPOT_SCRIPT" 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true
|
||
}
|
||
|
||
advanced_emergency_hotspot_status() {
|
||
if emergency_hotspot_is_configured; then
|
||
local ssid
|
||
ssid=$(emergency_hotspot_ssid)
|
||
echo "Emergency Hotspot: Configured (SSID: ${ssid:-unknown})"
|
||
else
|
||
echo "Emergency Hotspot: Not configured"
|
||
fi
|
||
echo "ℹ Auto-starts a WiFi hotspot if the kiosk can't reach its network"
|
||
echo " 60 seconds after boot, so you can connect and reconfigure remotely."
|
||
}
|
||
|
||
advanced_emergency_hotspot_menu_builder() {
|
||
if emergency_hotspot_is_configured; then
|
||
MENU_LABELS=("Reconfigure" "Disable")
|
||
MENU_HANDLERS=(action_configure_emergency_hotspot action_disable_emergency_hotspot)
|
||
else
|
||
MENU_LABELS=("Enable emergency hotspot")
|
||
MENU_HANDLERS=(action_configure_emergency_hotspot)
|
||
fi
|
||
}
|
||
|
||
advanced_emergency_hotspot_menu() {
|
||
run_menu "EMERGENCY HOTSPOT" advanced_emergency_hotspot_menu_builder advanced_emergency_hotspot_status
|
||
}
|
||
|
||
################################################################################
|
||
# Actions
|
||
################################################################################
|
||
|
||
action_configure_emergency_hotspot() {
|
||
echo
|
||
if ! run_with_offline_fallback sudo apt install -y "${EMERGENCY_HOTSPOT_APT_PACKAGES[@]}"; then
|
||
log_error "Failed to install hostapd/dnsmasq/iptables"
|
||
pause
|
||
return 1
|
||
fi
|
||
|
||
sudo systemctl stop hostapd dnsmasq 2>/dev/null || true
|
||
sudo systemctl disable hostapd dnsmasq 2>/dev/null || true
|
||
|
||
local wifi_iface
|
||
wifi_iface=$(ls /sys/class/net 2>/dev/null | grep -E "^wl" | head -1 || true)
|
||
if [[ -z "$wifi_iface" ]]; then
|
||
log_error "No WiFi interface found"
|
||
pause
|
||
return 1
|
||
fi
|
||
echo "WiFi interface: $wifi_iface"
|
||
echo
|
||
|
||
local hotspot_ssid
|
||
hotspot_ssid=$(ask_text "Hotspot SSID" "Kiosk-Emergency")
|
||
|
||
local hotspot_pass=""
|
||
while [[ ${#hotspot_pass} -lt 8 ]]; do
|
||
read -r -s -p "Hotspot password (8+ chars): " hotspot_pass
|
||
echo
|
||
[[ ${#hotspot_pass} -lt 8 ]] && log_error "Password must be at least 8 characters"
|
||
done
|
||
|
||
echo
|
||
echo "When should the hotspot start? (checked 60 seconds after boot)"
|
||
echo " 1) The local network is unreachable - neither the router nor the"
|
||
echo " internet answers. Recommended; right for networks without"
|
||
echo " internet too."
|
||
echo " 2) There's no internet. On a network without internet this starts"
|
||
echo " the hotspot on every boot."
|
||
echo " 3) A specific address on your network doesn't answer (e.g. a server)."
|
||
local check_mode="network" check_host="" check_choice
|
||
check_choice=$(ask_integer "Choice" 1 1 3)
|
||
case "$check_choice" in
|
||
2) check_mode="internet" ;;
|
||
3)
|
||
check_host=$(ask_text "Address to check (IP or hostname)" "")
|
||
if [[ "$check_host" =~ ^[A-Za-z0-9.:-]+$ ]]; then
|
||
check_mode="host"
|
||
else
|
||
log_warning "Not a valid address - using option 1 instead"
|
||
check_host=""
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
local hotspot_ip="192.168.50.1"
|
||
|
||
sudo mkdir -p "$BIN_DIR"
|
||
sudo tee "$EMERGENCY_HOTSPOT_SCRIPT" > /dev/null <<EOF
|
||
#!/bin/bash
|
||
################################################################################
|
||
### KIOSK EMERGENCY HOTSPOT
|
||
### Auto-starts if the network check fails 60 seconds after boot
|
||
################################################################################
|
||
|
||
# network = neither the default gateway (router) nor the internet answers
|
||
# internet = no internet (pings 8.8.8.8 / 1.1.1.1)
|
||
# host = CHECK_HOST doesn't answer
|
||
CHECK_MODE="$check_mode"
|
||
CHECK_HOST="$check_host"
|
||
WIFI_IFACE="$wifi_iface"
|
||
HOTSPOT_SSID="$hotspot_ssid"
|
||
HOTSPOT_PASS="$hotspot_pass"
|
||
HOTSPOT_IP="$hotspot_ip"
|
||
KIOSK_USER="$KIOSK_USER"
|
||
|
||
# Wait 60 seconds after boot
|
||
sleep 60
|
||
|
||
ping_ok() { ping -c 2 -W 3 "\$1" >/dev/null 2>&1; }
|
||
internet_ok() { ping_ok 8.8.8.8 || ping_ok 1.1.1.1; }
|
||
# The router counts as there if it answers ping - or, for routers set to
|
||
# ignore ping, if it answered the ARP lookup that ping just triggered.
|
||
gateway_ok() {
|
||
local gw
|
||
gw=\$(ip -4 route show default 2>/dev/null | awk '{print \$3; exit}')
|
||
[[ -n "\$gw" ]] || return 1
|
||
ping_ok "\$gw" && return 0
|
||
ip neigh show "\$gw" 2>/dev/null | grep -qE 'REACHABLE|STALE|DELAY|PROBE'
|
||
}
|
||
connected() {
|
||
case "\$CHECK_MODE" in
|
||
internet) internet_ok ;;
|
||
host) ping_ok "\$CHECK_HOST" ;;
|
||
*) gateway_ok || internet_ok ;;
|
||
esac
|
||
}
|
||
|
||
# Three tries, 10 seconds apart, so a slow DHCP lease or WiFi association
|
||
# isn't mistaken for being cut off.
|
||
for attempt in 1 2 3; do
|
||
if connected; then
|
||
logger "KIOSK: Network check (\$CHECK_MODE) passed - emergency hotspot not needed"
|
||
exit 0
|
||
fi
|
||
[[ \$attempt -lt 3 ]] && sleep 10
|
||
done
|
||
|
||
logger "KIOSK: Network check (\$CHECK_MODE) failed - starting emergency hotspot"
|
||
|
||
# Stop any conflicting services
|
||
systemctl stop wpa_supplicant 2>/dev/null || true
|
||
ip link set \$WIFI_IFACE down 2>/dev/null || true
|
||
sleep 2
|
||
|
||
# Configure static IP for hotspot
|
||
ip addr flush dev \$WIFI_IFACE
|
||
ip addr add \${HOTSPOT_IP}/24 dev \$WIFI_IFACE
|
||
ip link set \$WIFI_IFACE up
|
||
|
||
# Configure dnsmasq
|
||
cat > /tmp/dnsmasq-hotspot.conf <<DNSMASQ
|
||
interface=\$WIFI_IFACE
|
||
dhcp-range=192.168.50.10,192.168.50.50,12h
|
||
dhcp-option=3,\$HOTSPOT_IP
|
||
dhcp-option=6,\$HOTSPOT_IP
|
||
server=8.8.8.8
|
||
log-queries
|
||
log-dhcp
|
||
DNSMASQ
|
||
|
||
# Start dnsmasq
|
||
dnsmasq -C /tmp/dnsmasq-hotspot.conf
|
||
|
||
# Configure hostapd
|
||
cat > /tmp/hostapd-hotspot.conf <<HOSTAPD
|
||
interface=\$WIFI_IFACE
|
||
driver=nl80211
|
||
ssid=\$HOTSPOT_SSID
|
||
hw_mode=g
|
||
channel=6
|
||
macaddr_acl=0
|
||
auth_algs=1
|
||
ignore_broadcast_ssid=0
|
||
wpa=2
|
||
wpa_passphrase=\$HOTSPOT_PASS
|
||
wpa_key_mgmt=WPA-PSK
|
||
wpa_pairwise=TKIP
|
||
rsn_pairwise=CCMP
|
||
HOSTAPD
|
||
|
||
# Start hostapd
|
||
hostapd -B /tmp/hostapd-hotspot.conf
|
||
|
||
# Enable IP forwarding (optional - for internet sharing if wired connection exists)
|
||
echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null || true
|
||
|
||
# Show notification on kiosk display
|
||
sudo -u \$KIOSK_USER DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$(id -u \$KIOSK_USER)/bus \\
|
||
notify-send -u critical -t 0 "Emergency Hotspot Active" \\
|
||
"SSID: \$HOTSPOT_SSID\\nPassword: \$HOTSPOT_PASS\\nConnect to: http://\$HOTSPOT_IP" 2>/dev/null || true
|
||
|
||
logger "KIOSK: Emergency hotspot started - SSID: \$HOTSPOT_SSID, IP: \$HOTSPOT_IP"
|
||
|
||
# Create on-screen notification HTML
|
||
sudo -u \$KIOSK_USER tee /tmp/hotspot-notification.html > /dev/null <<'NOTIFY'
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
background: rgba(0,0,0,0.95);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100vh;
|
||
}
|
||
.container {
|
||
text-align: center;
|
||
padding: 40px;
|
||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||
border-radius: 20px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||
max-width: 600px;
|
||
}
|
||
h1 { font-size: 48px; margin-bottom: 20px; }
|
||
.icon { font-size: 72px; margin-bottom: 20px; }
|
||
.info { font-size: 24px; margin: 20px 0; line-height: 1.6; }
|
||
.credential {
|
||
background: rgba(0,0,0,0.3);
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
margin: 10px 0;
|
||
font-family: monospace;
|
||
font-size: 20px;
|
||
}
|
||
.dismiss {
|
||
margin-top: 30px;
|
||
padding: 15px 40px;
|
||
font-size: 18px;
|
||
background: white;
|
||
color: #e74c3c;
|
||
border: none;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
}
|
||
.dismiss:hover { background: #ecf0f1; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="icon">📡</div>
|
||
<h1>Emergency Hotspot Active</h1>
|
||
<div class="info">No internet connection detected<br>Hotspot created for remote access</div>
|
||
<div class="credential">SSID: <strong>\$HOTSPOT_SSID</strong></div>
|
||
<div class="credential">Password: <strong>\$HOTSPOT_PASS</strong></div>
|
||
<div class="credential">Connect to: <strong>http://\$HOTSPOT_IP</strong></div>
|
||
<button class="dismiss" onclick="window.close()">Dismiss</button>
|
||
</div>
|
||
<script>
|
||
// Auto-dismiss after 5 minutes
|
||
setTimeout(() => window.close(), 300000);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
NOTIFY
|
||
|
||
# Show notification window if Electron is running
|
||
if pgrep -f "electron.*main.js" >/dev/null 2>&1; then
|
||
sudo -u \$KIOSK_USER DISPLAY=:0 \\
|
||
"$KIOSK_DIR/node_modules/electron/dist/electron" \\
|
||
/tmp/hotspot-notification.html &
|
||
fi
|
||
|
||
exit 0
|
||
EOF
|
||
|
||
sudo chmod +x "$EMERGENCY_HOTSPOT_SCRIPT"
|
||
|
||
sudo mkdir -p "$SYSTEMD_DIR"
|
||
sudo tee "$SYSTEMD_DIR/kiosk-emergency-hotspot.service" > /dev/null <<UNITEOF
|
||
[Unit]
|
||
Description=Kiosk Emergency Hotspot
|
||
After=network.target lightdm.service
|
||
Wants=network.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=${EMERGENCY_HOTSPOT_SCRIPT}
|
||
RemainAfterExit=yes
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNITEOF
|
||
|
||
sudo systemctl daemon-reload 2>/dev/null || true
|
||
# Enable only, not start: this is a boot-time oneshot that waits 60s
|
||
# and checks connectivity - starting it right now would just run that
|
||
# wait/check immediately, which isn't what "configure" means here.
|
||
if ! sudo systemctl enable kiosk-emergency-hotspot.service 2>/dev/null; then
|
||
log_warning "Hotspot files written, but 'systemctl enable' failed - check 'systemctl status kiosk-emergency-hotspot.service'"
|
||
fi
|
||
|
||
echo
|
||
log_success "Emergency hotspot configured"
|
||
echo " SSID: $hotspot_ssid"
|
||
echo " Password: $hotspot_pass"
|
||
echo " IP: $hotspot_ip"
|
||
echo
|
||
echo "Hotspot auto-starts if no internet is detected 60 seconds after boot."
|
||
|
||
pause
|
||
}
|
||
|
||
action_disable_emergency_hotspot() {
|
||
echo
|
||
ask_yes_no "Disable emergency hotspot?" "n" || { echo "Cancelled"; pause; return; }
|
||
emergency_hotspot_do_disable
|
||
pause
|
||
}
|
||
|
||
# Shared with Complete Uninstall - same reasoning as cups_do_uninstall.
|
||
emergency_hotspot_do_disable() {
|
||
sudo systemctl stop kiosk-emergency-hotspot.service 2>/dev/null || true
|
||
sudo systemctl disable kiosk-emergency-hotspot.service 2>/dev/null || true
|
||
sudo rm -f "$SYSTEMD_DIR/kiosk-emergency-hotspot.service"
|
||
sudo rm -f "$EMERGENCY_HOTSPOT_SCRIPT"
|
||
sudo systemctl daemon-reload 2>/dev/null || true
|
||
log_success "Emergency hotspot disabled"
|
||
}
|