4 Commits
Author SHA1 Message Date
Outis a66b1e6079 Merge pull request #119 from outis1one/claude/happy-knuth-hwnvbn
Claude/happy knuth hwnvbn
2026-10-09 13:26:37 -04:00
Claude 5cd2117a94 WiFi menu: show its errors, detect adapters by type, keep wired config
Reported: Core Settings -> WiFi "just returns to the core settings menu".
run_menu clears the screen the moment a handler returns, and every early
exit in wifi_menu returned without a pause - so whatever went wrong
(no adapter found, interface wouldn't come up, ...) was wiped before it
could be read.

- wifi_menu now runs the wizard (renamed wifi_configure) and always
  pauses afterwards, success or failure.
- Adapter detection by type (/sys/class/net/*/wireless or phy80211),
  falling back to wl* - not every adapter is named wl*. When none is
  found, lists the interfaces that do exist.
- Clears a software rfkill block via sysfs before `ip link set up`
  (rfkill isn't installed on a server); warns on a hard block; shows
  ip's actual error if bringing the link up still fails.
- Writes its own $NETPLAN_DIR/60-kiosk-wifi.yaml instead of overwriting
  the first *.yaml found - on a fresh install that's the installer's
  50-cloud-init.yaml with the wired config, which it silently dropped.
- SSID/password written as YAML single-quoted scalars: a password with
  " or \ produced invalid YAML before.
- Success = the WiFi interface itself has an IPv4 address (waits up to
  30s), not "the machine has some IP". The SSH revert watchdog uses the
  same test instead of pinging 8.8.8.8, which on a network without
  internet reverted every working connection; with no previous WiFi file
  it reverts by removing ours.

Tested with ip/netplan/sudo stubbed and netplan files in a scratch dir:
installer's wired file untouched, own file written, SSID
`Bob's "Café" #2` and password `p@ss"wo'rd\\:#x` round-trip through a
YAML parser exactly, no-address case reports instead of claiming
success, early errors stay on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
2026-10-09 17:14:01 +00:00
Claude f7901a3684 Don't glob or test inside the kiosk user's private home as the admin
Reported on a 26.04 offline install, first-boot step 4:
  chown: cannot dereference '/home/kiosk/kiosk-app/*.js': No such file or directory
(same for *.mjs, *.html). `sudo chown ... "$KIOSK_DIR"/*.js` has the glob
expanded by the calling (admin) shell before sudo runs; the kiosk user's
home is created mode 750, so the admin can't list it, the pattern stays
literal, and chown fails - stopping the install under set -e.

- provision_install_app: chown the copied top-level files with
  `sudo find "$KIOSK_DIR" -maxdepth 1 -type f -exec chown ...` (root does
  the listing). Top level only, so chrome-sandbox inside node_modules
  keeps its root ownership.
- Swept lib/ and menus/ for the same class of bug: Asterisk Intercom's
  baresip_is_installed and status read $KIOSK_HOME/.baresip/accounts
  without sudo, so they always reported "not configured". Now sudo test
  / sudo head. Everything else that touches the kiosk home (config.json,
  kiosk-app checks) already goes through sudo -u kiosk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
2026-10-09 16:59:28 +00:00
Claude b0c0008d44 Create missing groups before adding the kiosk user (no netdev on 26.04)
Reported on a 26.04 offline install, first-boot step 2:
  useradd: group 'netdev' does not exist
useradd refuses to create the user at all when any -G group is missing,
which stopped provisioning. Ubuntu 26.04 Server's base system has audio,
video, input and plugdev but no netdev (checked against the 26.04.1
ISO's /etc/group).

provision_create_kiosk_user now `groupadd -f -r`s each supplementary
group first (a no-op when it exists), then passes the same list to
useradd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
2026-10-09 16:55:11 +00:00
3 changed files with 141 additions and 43 deletions
+16 -2
View File
@@ -94,8 +94,17 @@ provision_install_packages() {
provision_create_kiosk_user() { provision_create_kiosk_user() {
echo "[2/10] Creating kiosk user..." echo "[2/10] Creating kiosk user..."
local kiosk_groups=(audio video input plugdev netdev)
# Not every Ubuntu release ships all of these - 26.04 Server has no
# netdev group - and useradd refuses to create the user at all if any
# -G group is missing. Create missing ones as plain system groups
# (no-op when they exist).
local g
for g in "${kiosk_groups[@]}"; do
sudo groupadd -f -r "$g"
done
if ! id "$KIOSK_USER" &>/dev/null; then if ! id "$KIOSK_USER" &>/dev/null; then
sudo useradd -m -s /bin/bash -G audio,video,input,plugdev,netdev "$KIOSK_USER" sudo useradd -m -s /bin/bash -G "$(IFS=,; echo "${kiosk_groups[*]}")" "$KIOSK_USER"
echo "$KIOSK_USER:kiosk" | sudo chpasswd echo "$KIOSK_USER:kiosk" | sudo chpasswd
log_success "Kiosk user created (default password: kiosk - change it)" log_success "Kiosk user created (default password: kiosk - change it)"
else else
@@ -129,7 +138,12 @@ provision_install_app() {
echo "[4/10] Installing kiosk app..." echo "[4/10] Installing kiosk app..."
# *.mjs: pdf-render.mjs (pdf.js only ships as a JavaScript module). # *.mjs: pdf-render.mjs (pdf.js only ships as a JavaScript module).
sudo cp "$KIOSK_APP_SRC"/*.js "$KIOSK_APP_SRC"/*.mjs "$KIOSK_APP_SRC"/*.html "$KIOSK_APP_SRC/package.json" "$KIOSK_APP_SRC/start.sh" "$KIOSK_DIR/" sudo cp "$KIOSK_APP_SRC"/*.js "$KIOSK_APP_SRC"/*.mjs "$KIOSK_APP_SRC"/*.html "$KIOSK_APP_SRC/package.json" "$KIOSK_APP_SRC/start.sh" "$KIOSK_DIR/"
sudo chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_DIR"/*.js "$KIOSK_DIR"/*.mjs "$KIOSK_DIR"/*.html "$KIOSK_DIR/package.json" "$KIOSK_DIR/start.sh" # Not `chown "$KIOSK_DIR"/*.js ...`: that glob is expanded by *this*
# (admin) shell before sudo runs, and the kiosk user's home is private
# (mode 750 on 26.04), so it can't be listed - the pattern stays literal
# and chown fails. find runs as root. Top level only: node_modules is
# already the kiosk user's, and chrome-sandbox inside it must stay root's.
sudo find "$KIOSK_DIR" -maxdepth 1 -type f -exec chown "$KIOSK_USER:$KIOSK_USER" {} +
sudo chmod +x "$KIOSK_DIR/start.sh" sudo chmod +x "$KIOSK_DIR/start.sh"
if offline_mode_active; then if offline_mode_active; then
+5 -3
View File
@@ -62,7 +62,9 @@ baresip_installed_version() {
# `apt install baresip` with no configured extension isn't something # `apt install baresip` with no configured extension isn't something
# this menu should call done. # this menu should call done.
baresip_is_installed() { baresip_is_installed() {
command -v baresip &>/dev/null && [[ -f "$BARESIP_CONFIG_DIR/accounts" ]] # sudo: it's under the kiosk user's home, which the admin running this
# can't look inside (mode 750) - a plain [[ -f ]] is always false there.
command -v baresip &>/dev/null && sudo test -f "$BARESIP_CONFIG_DIR/accounts"
} }
baresip_is_running() { baresip_is_running() {
@@ -78,9 +80,9 @@ addon_asterisk_intercom_status() {
else else
echo "Asterisk Intercom: Installed (v${ver:-unknown}) - Not running" echo "Asterisk Intercom: Installed (v${ver:-unknown}) - Not running"
fi fi
if [[ -f "$BARESIP_CONFIG_DIR/accounts" ]]; then if sudo test -f "$BARESIP_CONFIG_DIR/accounts"; then
local account local account
account=$(head -1 "$BARESIP_CONFIG_DIR/accounts" 2>/dev/null) account=$(sudo head -1 "$BARESIP_CONFIG_DIR/accounts" 2>/dev/null)
local extension="${account#<sip:}" local extension="${account#<sip:}"
extension="${extension%%@*}" extension="${extension%%@*}"
[[ -n "$extension" ]] && echo " Extension: $extension" [[ -n "$extension" ]] && echo " Extension: $extension"
+118 -36
View File
@@ -20,13 +20,89 @@
# is exercised by hand against real hardware only. # is exercised by hand against real hardware only.
# #
# Unlike the other migrated menus, this one has no sub-options - it's a # Unlike the other migrated menus, this one has no sub-options - it's a
# single linear wizard, same as the legacy configure_wifi - so wifi_menu # single linear wizard, same as the legacy configure_wifi. wifi_menu runs
# IS the action, not a run_menu wrapper. # it (wifi_configure) and then pauses: run_menu clears the screen as soon
# as a handler returns, which used to wipe every error message here
# before it could be read - the menu just seemed to bounce back.
#
# The WiFi config goes in its own file, $NETPLAN_DIR/60-kiosk-wifi.yaml,
# which netplan merges with the rest - not over the first *.yaml it finds,
# which on a fresh install is the installer's wired config.
# #
# Depends on: lib/menu.sh, lib/config.sh being sourced first. # Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################ ################################################################################
WIFI_NETPLAN_FILE="$NETPLAN_DIR/60-kiosk-wifi.yaml"
wifi_menu() { wifi_menu() {
local rc=0
wifi_configure || rc=$?
echo
pause
return "$rc"
}
# The first wireless interface, by what it is (it has a wireless/ or
# phy80211 entry in sysfs) rather than by name - adapters aren't always
# named wl*. Falls back to the wl* naming convention.
wifi_detect_iface() {
local d
for d in /sys/class/net/*; do
if [[ -d "$d/wireless" || -e "$d/phy80211" ]]; then
basename "$d"
return 0
fi
done
local name
for d in /sys/class/net/wl*; do
[[ -e "$d" ]] || continue
name=$(basename "$d")
echo "$name"
return 0
done
return 1
}
# Clear a software WiFi block (the rfkill "soft" switch) - with it set,
# `ip link set up` fails with "RF-kill". The rfkill tool isn't installed
# on a server, so via sysfs. A hard block (physical switch / BIOS) can't
# be cleared from here and is reported instead.
wifi_unblock() {
local r type
for r in /sys/class/rfkill/rfkill*; do
[[ -e "$r/type" ]] || continue
type=$(cat "$r/type" 2>/dev/null)
[[ "$type" == "wlan" ]] || continue
if [[ "$(cat "$r/soft" 2>/dev/null)" == "1" ]]; then
echo "WiFi is switched off in software (rfkill) - switching it on..."
echo 0 | sudo tee "$r/soft" >/dev/null || true
fi
if [[ "$(cat "$r/hard" 2>/dev/null)" == "1" ]]; then
log_warning "WiFi is switched off by a hardware switch or the BIOS/UEFI - turn it on there"
fi
done
}
# YAML single-quoted scalar: the only special character inside is the
# quote itself, doubled. Safe for any SSID/password (", \, #, : ...) -
# double quotes broke on a password containing " or \.
wifi_yaml_quote() {
local v="$1"
printf "'%s'" "${v//\'/\'\'}"
}
# IFACE's first IPv4 address (without the /prefix), or nothing.
wifi_iface_ip() {
ip -4 -o addr show dev "$1" 2>/dev/null \
| awk '{for (i = 1; i < NF; i++) if ($i == "inet") { sub("/.*", "", $(i+1)); print $(i+1); exit }}'
}
# True if IFACE has an IPv4 address.
wifi_iface_has_ip() {
ip -4 addr show dev "$1" 2>/dev/null | grep -q "inet "
}
wifi_configure() {
echo echo
echo " ═══ WIFI CONFIGURATION ═══" echo " ═══ WIFI CONFIGURATION ═══"
echo echo
@@ -55,11 +131,11 @@ wifi_menu() {
fi fi
local wifi_iface local wifi_iface
wifi_iface=$(ls /sys/class/net 2>/dev/null | grep -E "^wl" | head -1) if ! wifi_iface=$(wifi_detect_iface); then
if [[ -z "$wifi_iface" ]]; then
log_warning "No WiFi hardware detected" log_warning "No WiFi hardware detected"
echo "If you have a USB WiFi adapter, ensure it's plugged in." echo "Network interfaces found: $(ls /sys/class/net 2>/dev/null | tr '\n' ' ')"
echo "If you have a USB WiFi adapter, make sure it's plugged in. A built-in"
echo "adapter with no driver shows up in 'lspci -k' / 'lsusb' but not above."
return 1 return 1
fi fi
@@ -74,9 +150,11 @@ wifi_menu() {
ask_yes_no "Configure WiFi?" "n" || return 0 ask_yes_no "Configure WiFi?" "n" || return 0
wifi_unblock
echo "Bringing up interface..." echo "Bringing up interface..."
if ! sudo ip link set "$wifi_iface" up 2>/dev/null; then local link_err
log_error "Failed to bring up interface" if ! link_err=$(sudo ip link set "$wifi_iface" up 2>&1); then
log_error "Failed to bring up $wifi_iface: ${link_err:-unknown error}"
return 1 return 1
fi fi
sleep 3 sleep 3
@@ -161,15 +239,9 @@ apply_wifi_config() {
local ssid="$2" local ssid="$2"
local password="$3" local password="$3"
# `|| true`: under set -e + pipefail (this whole tool runs under both), # Our own file, merged by netplan with the others (wired config etc).
# `ls` matching nothing exits non-zero even with stderr silenced, which local netplan_file="$WIFI_NETPLAN_FILE"
# would abort this function outright instead of falling through to the sudo mkdir -p "$NETPLAN_DIR"
# default filename below. Same class of bug as the run_menu fix in
# lib/menu.sh - masked here in practice because cloud-init almost
# always leaves a *.yaml file behind, but not guaranteed.
local netplan_file
netplan_file=$(ls "$NETPLAN_DIR"/*.yaml 2>/dev/null | head -1) || true
[[ -z "$netplan_file" ]] && netplan_file="$NETPLAN_DIR/50-cloud-init.yaml"
local backup="" local backup=""
if [[ -f "$netplan_file" ]]; then if [[ -f "$netplan_file" ]]; then
@@ -190,29 +262,31 @@ network:
dhcp6: false dhcp6: false
optional: true optional: true
access-points: access-points:
"$ssid": $(wifi_yaml_quote "$ssid"):
password: "$password" password: $(wifi_yaml_quote "$password")
EOF EOF
if [[ -n "${SSH_CONNECTION:-}" ]] && [[ -n "$backup" ]]; then if [[ -n "${SSH_CONNECTION:-}" ]]; then
# Reverts if the WiFi interface still has no address after 60s -
# not "can't ping 8.8.8.8", which on a network without internet
# would undo a perfectly good connection every time. No backup
# means there was no WiFi file before: revert = remove ours.
local watchdog local watchdog
watchdog=$(mktemp --suffix=.sh) watchdog=$(mktemp --suffix=.sh)
cat > "$watchdog" <<'WATCHEOF' cat > "$watchdog" <<'WATCHEOF'
#!/bin/bash #!/bin/bash
# $1 = backup ("" if none), $2 = netplan file, $3 = WiFi interface
sleep 60 sleep 60
if [[ -f "$1" && -f "$2" ]]; then if ! ip -4 addr show dev "$3" 2>/dev/null | grep -q "inet "; then
ip=$(hostname -I | awk '{print $1}') if [[ -n "$1" && -f "$1" ]]; then cp "$1" "$2"; else rm -f "$2"; fi
if [[ -z "$ip" ]] || ! ping -c 2 8.8.8.8 >/dev/null 2>&1; then
cp "$1" "$2"
netplan apply 2>/dev/null netplan apply 2>/dev/null
echo "WiFi config reverted - connection failed" | wall echo "WiFi config reverted - $3 got no address" | wall
fi
fi fi
rm -f "$0" rm -f "$0"
WATCHEOF WATCHEOF
chmod +x "$watchdog" chmod +x "$watchdog"
nohup sudo bash "$watchdog" "$backup" "$netplan_file" >/dev/null 2>&1 & nohup sudo bash "$watchdog" "$backup" "$netplan_file" "$wifi_iface" >/dev/null 2>&1 &
echo "Watchdog started - will revert in 60s if the connection fails" echo "Watchdog started - will revert in 60s if $wifi_iface gets no address"
fi fi
sudo cp "$temp_plan" "$netplan_file" sudo cp "$temp_plan" "$netplan_file"
@@ -224,22 +298,30 @@ WATCHEOF
netplan_log=$(mktemp) netplan_log=$(mktemp)
if sudo netplan apply > "$netplan_log" 2>&1; then if sudo netplan apply > "$netplan_log" 2>&1; then
cat "$netplan_log" cat "$netplan_log"
sleep 10 echo "Waiting for $wifi_iface to connect (up to 30 seconds)..."
local new_ip local _
new_ip=$(get_ip_address) for _ in {1..30}; do
if [[ -n "$new_ip" && "$new_ip" != "No IP" ]]; then wifi_iface_has_ip "$wifi_iface" && break
log_success "Connected: $ssid ($new_ip)" sleep 1
done
if wifi_iface_has_ip "$wifi_iface"; then
log_success "Connected to $ssid: $(wifi_iface_ip "$wifi_iface")"
[[ -n "${SSH_CONNECTION:-}" ]] && echo "Connection successful - watchdog will not revert" [[ -n "${SSH_CONNECTION:-}" ]] && echo "Connection successful - watchdog will not revert"
else else
log_warning "Config applied but no IP yet" log_warning "Config applied but $wifi_iface has no address yet"
echo "Check: sudo journalctl -u systemd-networkd -f" echo "Wrong password or network name is the usual cause. Details:"
echo " sudo journalctl -b -u 'netplan-wpa-*' -u systemd-networkd -n 30 --no-pager"
fi fi
else else
log_error "netplan apply failed" log_error "netplan apply failed"
echo "Error log:" echo "Error log:"
cat "$netplan_log" cat "$netplan_log"
if [[ -n "$backup" ]] && ask_yes_no "Restore backup?" "y"; then if ask_yes_no "Undo this WiFi change?" "y"; then
if [[ -n "$backup" ]]; then
sudo cp "$backup" "$netplan_file" sudo cp "$backup" "$netplan_file"
else
sudo rm -f "$netplan_file"
fi
# Last resort after everything else failed: report, don't crash # Last resort after everything else failed: report, don't crash
# the session if even the restore-and-reapply doesn't work. # the session if even the restore-and-reapply doesn't work.
if sudo netplan apply; then if sudo netplan apply; then