Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bf4efe2b1 | ||
|
|
0454a259fa |
@@ -1,6 +1,6 @@
|
|||||||
# Ubuntu Based Kiosk
|
# Ubuntu Based Kiosk
|
||||||
|
|
||||||
**Current Version:** 2.7.0 (check script header for latest version)
|
**Current Version:** 2.9.0 (check script header for latest version)
|
||||||
**Built with Claude Sonnet 4.6 AI assistance**
|
**Built with Claude Sonnet 4.6 AI assistance**
|
||||||
**License:** GPL v3 - Keep derivatives open source
|
**License:** GPL v3 - Keep derivatives open source
|
||||||
**Repository:** https://github.com/outis1one/ubuntu-based-kiosk/
|
**Repository:** https://github.com/outis1one/ubuntu-based-kiosk/
|
||||||
@@ -1212,6 +1212,15 @@ terminal menu and the web UI, so they can't drift apart).
|
|||||||
- `menus/addon_authelia.sh` — **Authelia Auto-Login** (Addons):
|
- `menus/addon_authelia.sh` — **Authelia Auto-Login** (Addons):
|
||||||
encrypted SSO credentials plus the server-side setup instructions.
|
encrypted SSO credentials plus the server-side setup instructions.
|
||||||
Prompted the `save_config` merge fix above.
|
Prompted the `save_config` merge fix above.
|
||||||
|
- `menus/addon_remote_access.sh` — **Remote Access** (Addons): VNC,
|
||||||
|
WireGuard, Tailscale, Netbird. The biggest Addon so far.
|
||||||
|
- `menus/addon_lms_squeezelite.sh` — **LMS Server / Squeezelite Player**
|
||||||
|
(Addons): install/reconfigure/uninstall for an LMS (Lyrion/Logitech
|
||||||
|
Media Server) server the kiosk can host, and a Squeezelite player the
|
||||||
|
kiosk can run against any LMS server on the LAN. Squeezelite's own
|
||||||
|
start script and systemd unit go through `$BIN_DIR`/`$SYSTEMD_DIR`
|
||||||
|
like every other addon; LMS's own apt repo/GPG key/ufw rules stay at
|
||||||
|
their real fixed system paths, same as CUPS.
|
||||||
- `install.sh` — entry point for the modular tool, now grouped **Core
|
- `install.sh` — entry point for the modular tool, now grouped **Core
|
||||||
Settings / Addons / Advanced** like the legacy menu. Run it against an
|
Settings / Addons / Advanced** like the legacy menu. Run it against an
|
||||||
*already-installed* kiosk:
|
*already-installed* kiosk:
|
||||||
@@ -1224,14 +1233,25 @@ terminal menu and the web UI, so they can't drift apart).
|
|||||||
**Honest status:** this does not yet replace first-time installation, or
|
**Honest status:** this does not yet replace first-time installation, or
|
||||||
most of the old installer. `ubuntu-based-kiosk.sh` is still ~12,000
|
most of the old installer. `ubuntu-based-kiosk.sh` is still ~12,000
|
||||||
lines and still contains its own unremoved, unmodified copies of every
|
lines and still contains its own unremoved, unmodified copies of every
|
||||||
menu above (plus Upgrade, Reinstall, Uninstall, 3 more Addons, and the
|
menu above (plus Upgrade, Reinstall, Uninstall, 1 more Addon — Easy
|
||||||
other 8 Advanced items — none of that has moved yet). Both copies
|
Asterisk Intercom — and the other 8 Advanced items — none of that has
|
||||||
coexist deliberately: the old ones stay until enough
|
moved yet). Both copies coexist deliberately: the old ones stay until
|
||||||
of Core Settings/Addons/Advanced is migrated to
|
enough of Core Settings/Addons/Advanced is migrated to retire them in
|
||||||
retire them in one pass, rather than leaving the legacy menu half-wired.
|
one pass, rather than leaving the legacy menu half-wired. Migration
|
||||||
Migration continues one `menus/*.sh` file at a time; first-time
|
continues one `menus/*.sh` file at a time; first-time installation
|
||||||
installation itself is the last and largest piece to move, if it moves
|
itself is the last and largest piece to move, if it moves at all.
|
||||||
at all.
|
|
||||||
|
**Resolved (v2.9.0):** `is_service_enabled()` — shared by both scripts
|
||||||
|
— had a pre-check (`systemctl list-unit-files | grep -q "^${service}\s"`)
|
||||||
|
that never actually matched, since every call site passes a bare
|
||||||
|
service name while `list-unit-files` lines start with
|
||||||
|
`"$service.service"`. The function always fell through to `return 1`
|
||||||
|
regardless of the real enabled state — under-reporting "enabled but not
|
||||||
|
currently running" as "not installed" everywhere it's used, including
|
||||||
|
LMS/Squeezelite's own status detection. Fixed in both `lib/config.sh`
|
||||||
|
and `ubuntu-based-kiosk.sh` by dropping the dead pre-check —
|
||||||
|
`systemctl is-enabled` already reports "not found" as a failure on its
|
||||||
|
own.
|
||||||
|
|
||||||
**Resolved (v2.7.0):** the config-clobbering bug fixed in `lib/config.sh`
|
**Resolved (v2.7.0):** the config-clobbering bug fixed in `lib/config.sh`
|
||||||
(v2.6.0 — `save_config` silently deleting fields it doesn't know about,
|
(v2.6.0 — `save_config` silently deleting fields it doesn't know about,
|
||||||
@@ -1246,9 +1266,19 @@ full migration pass.
|
|||||||
|
|
||||||
## Project Status & Future Plans
|
## Project Status & Future Plans
|
||||||
|
|
||||||
**Current Version:** 2.7.0
|
**Current Version:** 2.9.0
|
||||||
|
|
||||||
**Recent Updates (v2.7.0):**
|
**Recent Updates (v2.9.0):**
|
||||||
|
- **LMS Server / Squeezelite Player migrated** — install/reconfigure/uninstall for both, in `./install.sh`. Squeezelite's own start script and systemd unit now go through `$BIN_DIR`/`$SYSTEMD_DIR` like every other addon instead of hardcoded `/usr/local/bin`/`/etc/systemd/system`; LMS's own apt repo/GPG key/ufw rules stay at their real fixed system paths, same approach as CUPS.
|
||||||
|
- **Bug fix:** the legacy `install_lms()` enabled/started the detected service via `sudo systemctl enable "$service_name" 2>&1 | tee /tmp/lms-enable.log` — piped through `tee`, the statement's exit status reflected `tee` (always 0), not `systemctl enable`, so a real enable/start failure was silently swallowed instead of falling through to a warning. Now uses the shared `enable_and_start_units()` helper.
|
||||||
|
- **Bug fix (shared, backported to the legacy script too):** `is_service_enabled()`'s pre-check never matched a bare service name against `list-unit-files`' `"$service.service"` lines, so it always reported "not enabled" regardless of the real state. Dropped the dead pre-check — see "Modular Management" below.
|
||||||
|
|
||||||
|
**Previous (v2.8.0):**
|
||||||
|
- **Remote Access migrated** — VNC, WireGuard, Tailscale, and Netbird, each with its own install/connect/status/uninstall flow. The biggest Addon so far. Tailscale/Netbird install via the vendors' own `curl | sh` method, preserved as-is.
|
||||||
|
- **Important framework-level bug found and fixed:** `run_menu()`'s *handler* call has been crash-guarded since v2.1.0, but its *status function* call was still completely bare. A status function is meant to be read-only display, but a pipeline whose `grep` matches nothing (which `pipefail` turns into a failure even though the actual last command succeeds) would crash the **entire session**, not just fail to show status. Found while building `wireguard_status()` and verifying its exact failure mode rather than assuming it was covered. Fixed once, in the framework, protecting every status function across every menu — present and future. Also audited every existing status function for the same shape and fixed one real instance in `power_schedule_status()`.
|
||||||
|
- Deduplicated: promoted `power_schedule.sh`'s `enable_and_start_timers()` to a shared `enable_and_start_units()` in `lib/menu.sh` (works for services now, not just timers) rather than writing the same helper a second time for VNC/WireGuard.
|
||||||
|
|
||||||
|
**Previous (v2.7.0):**
|
||||||
- **Backported fix:** `ubuntu-based-kiosk.sh`'s own `save_config()` had the identical config-clobbering bug fixed in `lib/config.sh` under v2.6.0 — it silently deleted Authelia credentials (or any field it doesn't explicitly know about) the next time Sites, Touch Controls, Navigation, or Password Protection saved. This was a real, currently-shipping credential-loss bug, so it's fixed directly in the legacy script now rather than waiting for those menus to be migrated. Verified in isolation against the exact extracted function before touching the shipping copy. Nothing else about those menus changed.
|
- **Backported fix:** `ubuntu-based-kiosk.sh`'s own `save_config()` had the identical config-clobbering bug fixed in `lib/config.sh` under v2.6.0 — it silently deleted Authelia credentials (or any field it doesn't explicitly know about) the next time Sites, Touch Controls, Navigation, or Password Protection saved. This was a real, currently-shipping credential-loss bug, so it's fixed directly in the legacy script now rather than waiting for those menus to be migrated. Verified in isolation against the exact extracted function before touching the shipping copy. Nothing else about those menus changed.
|
||||||
|
|
||||||
**Previous (v2.6.0):**
|
**Previous (v2.6.0):**
|
||||||
|
|||||||
+9
-3
@@ -17,7 +17,9 @@
|
|||||||
# Hidden Site PIN, Password Protection & Lockout, WiFi,
|
# Hidden Site PIN, Password Protection & Lockout, WiFi,
|
||||||
# Power/Display/Quiet Hours.
|
# Power/Display/Quiet Hours.
|
||||||
# Addons: CUPS Printing (menus/addon_cups.sh), Authelia Auto-Login
|
# Addons: CUPS Printing (menus/addon_cups.sh), Authelia Auto-Login
|
||||||
# (menus/addon_authelia.sh).
|
# (menus/addon_authelia.sh), Remote Access - VNC/WireGuard/
|
||||||
|
# Tailscale/Netbird (menus/addon_remote_access.sh), LMS Server /
|
||||||
|
# Squeezelite Player (menus/addon_lms_squeezelite.sh).
|
||||||
# Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/
|
# Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/
|
||||||
# audio/network).
|
# audio/network).
|
||||||
#
|
#
|
||||||
@@ -55,6 +57,10 @@ source "$SCRIPT_DIR/menus/diagnostics.sh"
|
|||||||
source "$SCRIPT_DIR/menus/addon_cups.sh"
|
source "$SCRIPT_DIR/menus/addon_cups.sh"
|
||||||
# shellcheck source=menus/addon_authelia.sh
|
# shellcheck source=menus/addon_authelia.sh
|
||||||
source "$SCRIPT_DIR/menus/addon_authelia.sh"
|
source "$SCRIPT_DIR/menus/addon_authelia.sh"
|
||||||
|
# shellcheck source=menus/addon_remote_access.sh
|
||||||
|
source "$SCRIPT_DIR/menus/addon_remote_access.sh"
|
||||||
|
# shellcheck source=menus/addon_lms_squeezelite.sh
|
||||||
|
source "$SCRIPT_DIR/menus/addon_lms_squeezelite.sh"
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Preflight
|
# Preflight
|
||||||
@@ -118,8 +124,8 @@ core_settings_menu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addons_menu_builder() {
|
addons_menu_builder() {
|
||||||
MENU_LABELS=("CUPS Printing" "Authelia Auto-Login")
|
MENU_LABELS=("CUPS Printing" "Authelia Auto-Login" "Remote Access" "LMS Server / Squeezelite Player")
|
||||||
MENU_HANDLERS=(addon_cups_menu addon_authelia_menu)
|
MENU_HANDLERS=(addon_cups_menu addon_authelia_menu remote_access_menu addon_lms_squeezelite_menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
addons_menu() {
|
addons_menu() {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
: "${BIN_DIR:=/usr/local/bin}"
|
: "${BIN_DIR:=/usr/local/bin}"
|
||||||
: "${NETPLAN_DIR:=/etc/netplan}"
|
: "${NETPLAN_DIR:=/etc/netplan}"
|
||||||
: "${POLKIT_DIR:=/etc/polkit-1/localauthority/50-local.d}"
|
: "${POLKIT_DIR:=/etc/polkit-1/localauthority/50-local.d}"
|
||||||
|
: "${WIREGUARD_DIR:=/etc/wireguard}"
|
||||||
|
|
||||||
# The admin account actually running this tool (as opposed to $KIOSK_USER,
|
# The admin account actually running this tool (as opposed to $KIOSK_USER,
|
||||||
# the kiosk's own restricted account) - used where an addon needs to grant
|
# the kiosk's own restricted account) - used where an addon needs to grant
|
||||||
@@ -77,6 +78,20 @@ is_service_active() {
|
|||||||
systemctl is-active --quiet "$service" 2>/dev/null
|
systemctl is-active --quiet "$service" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Whether a service is enabled (would start on boot), regardless of
|
||||||
|
# whether it's currently running. The legacy script's version of this
|
||||||
|
# pre-checked `systemctl list-unit-files | grep -q "^${service}\s"`
|
||||||
|
# before calling is-enabled - but every call site passes a bare service
|
||||||
|
# name (e.g. "squeezelite"), while list-unit-files lines start with
|
||||||
|
# "squeezelite.service", so that regex never matched and the legacy
|
||||||
|
# function always fell through to `return 1` no matter the real state.
|
||||||
|
# `systemctl is-enabled` already reports "not found" as a failure on its
|
||||||
|
# own, so the pre-check was both broken and unnecessary - dropped here.
|
||||||
|
is_service_enabled() {
|
||||||
|
local service="$1"
|
||||||
|
systemctl is-enabled --quiet "$service" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
# Load every setting config.json has into the bash globals above.
|
# Load every setting config.json has into the bash globals above.
|
||||||
# Safe to call with no existing config file - leaves script defaults in place.
|
# Safe to call with no existing config file - leaves script defaults in place.
|
||||||
load_existing_config() {
|
load_existing_config() {
|
||||||
|
|||||||
+20
-1
@@ -84,6 +84,18 @@ get_vpn_ips() {
|
|||||||
[[ -n "$vpn_info" ]] && echo "$vpn_info" || echo "None"
|
[[ -n "$vpn_info" ]] && echo "$vpn_info" || echo "None"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# enable_and_start_units UNIT [UNIT...]
|
||||||
|
# Reloads systemd and enables+starts the given unit(s) - services or
|
||||||
|
# timers - returning non-zero if enable or start fails (e.g. systemd/
|
||||||
|
# D-Bus unreachable, or a real failure on real hardware). Always call
|
||||||
|
# this from an `if`/`&&`/`||` context: this whole tool runs under
|
||||||
|
# set -e, so a bare, unguarded call whose last command fails would take
|
||||||
|
# down the entire session instead of just this one action.
|
||||||
|
enable_and_start_units() {
|
||||||
|
sudo systemctl daemon-reload 2>/dev/null || true
|
||||||
|
sudo systemctl enable "$@" 2>/dev/null && sudo systemctl start "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
read -r -p "Press Enter to continue..."
|
read -r -p "Press Enter to continue..."
|
||||||
}
|
}
|
||||||
@@ -279,7 +291,14 @@ run_menu() {
|
|||||||
print_menu_header "$title"
|
print_menu_header "$title"
|
||||||
|
|
||||||
if [[ -n "$status_func" ]]; then
|
if [[ -n "$status_func" ]]; then
|
||||||
"$status_func"
|
# `|| true`: same reasoning as the handler call below - a
|
||||||
|
# status function's job is read-only display, and a
|
||||||
|
# legitimately failing command inside it (e.g. a pipeline
|
||||||
|
# whose grep matches nothing, which pipefail turns into a
|
||||||
|
# pipeline failure even though the actual last command
|
||||||
|
# succeeded) must not be allowed to kill the whole session
|
||||||
|
# over what should be, at worst, incomplete status text.
|
||||||
|
"$status_func" || true
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# menus/addon_lms_squeezelite.sh - "LMS Server / Squeezelite Player" addon.
|
||||||
|
#
|
||||||
|
# Two independent pieces sharing one menu, same as the legacy code: an LMS
|
||||||
|
# (Lyrion/Logitech Media Server) server the kiosk can host, and a
|
||||||
|
# Squeezelite player the kiosk can run to play music from any LMS server
|
||||||
|
# (this one or another one on the LAN). LMS itself is a real apt-managed
|
||||||
|
# subsystem with its own fixed paths (repo file, GPG keyring, ufw rules,
|
||||||
|
# /etc/squeezeboxserver) - like CUPS, those get full command-level `sudo`/
|
||||||
|
# `wget`/`apt` stubbing in tests rather than relocation. Squeezelite's own
|
||||||
|
# start script and systemd unit are ours to place, so - like power_schedule
|
||||||
|
# and the other addons - they go through $BIN_DIR/$SYSTEMD_DIR (lib/
|
||||||
|
# config.sh) instead of hardcoded /usr/local/bin and /etc/systemd/system,
|
||||||
|
# so tests can point them at a scratch directory.
|
||||||
|
#
|
||||||
|
# LMS ships under two package/service names depending on version -
|
||||||
|
# "logitechmediaserver" (older) and "lyrionmusicserver" (the project's
|
||||||
|
# current name after its rename) - so detection and every service call
|
||||||
|
# has to check both.
|
||||||
|
#
|
||||||
|
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
lms_service_name() {
|
||||||
|
if systemctl list-unit-files 2>/dev/null | grep -q "lyrionmusicserver.service"; then
|
||||||
|
echo "lyrionmusicserver"
|
||||||
|
elif systemctl list-unit-files 2>/dev/null | grep -q "logitechmediaserver.service"; then
|
||||||
|
echo "logitechmediaserver"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
lms_is_installed() {
|
||||||
|
is_service_active logitechmediaserver || is_service_enabled logitechmediaserver || \
|
||||||
|
is_service_active lyrionmusicserver || is_service_enabled lyrionmusicserver
|
||||||
|
}
|
||||||
|
|
||||||
|
lms_is_running() {
|
||||||
|
is_service_active logitechmediaserver || is_service_active lyrionmusicserver
|
||||||
|
}
|
||||||
|
|
||||||
|
squeezelite_is_installed() {
|
||||||
|
is_service_active squeezelite || is_service_enabled squeezelite
|
||||||
|
}
|
||||||
|
|
||||||
|
addon_lms_squeezelite_status() {
|
||||||
|
if lms_is_installed; then
|
||||||
|
echo "LMS Server: Installed"
|
||||||
|
if lms_is_running; then
|
||||||
|
echo " Status: Running"
|
||||||
|
else
|
||||||
|
echo " Status: Stopped"
|
||||||
|
fi
|
||||||
|
echo " Web: http://$(get_ip_address):9000"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if squeezelite_is_installed; then
|
||||||
|
local player_name="Unknown"
|
||||||
|
if [[ -f "$BIN_DIR/squeezelite-start.sh" ]]; then
|
||||||
|
player_name=$(grep '^PLAYER_NAME=' "$BIN_DIR/squeezelite-start.sh" 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "Unknown")
|
||||||
|
fi
|
||||||
|
echo "Squeezelite Player: Installed"
|
||||||
|
if is_service_active squeezelite; then
|
||||||
|
echo " Status: Running"
|
||||||
|
else
|
||||||
|
echo " Status: Stopped"
|
||||||
|
fi
|
||||||
|
echo " Name: $player_name"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ℹ A server is needed to stream music. The kiosk can run the"
|
||||||
|
echo " server (if sufficient resources) or connect to another server."
|
||||||
|
}
|
||||||
|
|
||||||
|
addon_lms_squeezelite_menu_builder() {
|
||||||
|
MENU_LABELS=("Install/Configure LMS Server" "Install/Configure Squeezelite Player")
|
||||||
|
MENU_HANDLERS=(action_install_lms action_install_squeezelite)
|
||||||
|
|
||||||
|
if lms_is_installed; then
|
||||||
|
MENU_LABELS+=("Uninstall LMS Server")
|
||||||
|
MENU_HANDLERS+=(action_uninstall_lms)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if squeezelite_is_installed; then
|
||||||
|
MENU_LABELS+=("Uninstall Squeezelite Player")
|
||||||
|
MENU_HANDLERS+=(action_uninstall_squeezelite)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
addon_lms_squeezelite_menu() {
|
||||||
|
run_menu "LMS SERVER / SQUEEZELITE PLAYER" addon_lms_squeezelite_menu_builder addon_lms_squeezelite_status
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Actions - LMS Server
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
action_install_lms() {
|
||||||
|
echo
|
||||||
|
if lms_is_installed; then
|
||||||
|
echo "LMS is already installed."
|
||||||
|
if ask_yes_no "Reconfigure port?" "n"; then
|
||||||
|
local new_port
|
||||||
|
new_port=$(ask_integer "New HTTP port" 9000 1 65535)
|
||||||
|
sudo sed -i "s/httpport:.*/httpport: $new_port/" /etc/squeezeboxserver/prefs/server.prefs 2>/dev/null || true
|
||||||
|
sudo systemctl restart lyrionmusicserver 2>/dev/null || sudo systemctl restart logitechmediaserver 2>/dev/null || true
|
||||||
|
log_success "LMS reconfigured on port $new_port"
|
||||||
|
fi
|
||||||
|
pause
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing Lyrion Music Server..."
|
||||||
|
|
||||||
|
# Try the repository method first.
|
||||||
|
if wget -qO - https://debian.slimdevices.com/debian/squeezebox-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/lms-keyring.gpg 2>/dev/null; then
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/lms-keyring.gpg] http://debian.slimdevices.com/debian stable main" | sudo tee /etc/apt/sources.list.d/lms.list
|
||||||
|
# `|| true`: a bare, unguarded `apt update` failing here (bad
|
||||||
|
# mirror, no network) would otherwise crash the whole session
|
||||||
|
# under set -e instead of falling through to the direct-download
|
||||||
|
# fallback below, which is exactly the degrade path this is
|
||||||
|
# supposed to hit when the repository route doesn't work.
|
||||||
|
sudo apt update 2>/dev/null || true
|
||||||
|
if sudo apt install -y logitechmediaserver 2>/dev/null; then
|
||||||
|
log_success "LMS installed via repository"
|
||||||
|
else
|
||||||
|
log_warning "Repository install failed, trying direct download..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fall back to a direct .deb download if the repository didn't produce
|
||||||
|
# either possible package.
|
||||||
|
if ! command -v logitechmediaserver &>/dev/null && ! command -v lyrionmusicserver &>/dev/null; then
|
||||||
|
local lms_deb="/tmp/lms.deb"
|
||||||
|
echo "Downloading LMS v9.0.3..."
|
||||||
|
if wget -q https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver_9.0.3_amd64.deb -O "$lms_deb"; then
|
||||||
|
echo "Installing LMS package..."
|
||||||
|
if sudo apt install -y "$lms_deb"; then
|
||||||
|
log_success "LMS installed via direct download"
|
||||||
|
else
|
||||||
|
log_error "Failed to install LMS package"
|
||||||
|
rm -f "$lms_deb"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$lms_deb"
|
||||||
|
else
|
||||||
|
log_error "Failed to download LMS from lms-community.org"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local service_name
|
||||||
|
service_name=$(lms_service_name)
|
||||||
|
|
||||||
|
if [[ -z "$service_name" ]]; then
|
||||||
|
log_warning "Service file not found, checking installed files..."
|
||||||
|
service_name=$(dpkg -L lyrionmusicserver logitechmediaserver 2>/dev/null | grep -m1 '\.service$' | xargs -r basename | sed 's/\.service$//' || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$service_name" ]]; then
|
||||||
|
log_error "Could not detect LMS service name"
|
||||||
|
echo "Manual steps:"
|
||||||
|
echo " 1. Find service: systemctl list-unit-files | grep -i lms"
|
||||||
|
echo " 2. Enable: sudo systemctl enable SERVICE_NAME"
|
||||||
|
echo " 3. Start: sudo systemctl start SERVICE_NAME"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Using service: $service_name"
|
||||||
|
# enable_and_start_units, not a bare `sudo systemctl enable ... | tee`
|
||||||
|
# pipe: the legacy version's `2>&1 | tee /tmp/lms-enable.log` made the
|
||||||
|
# whole statement's exit status depend on `tee` (always 0) rather than
|
||||||
|
# `systemctl enable`, so a real enable/start failure was silently
|
||||||
|
# swallowed instead of falling through to the warning below.
|
||||||
|
if enable_and_start_units "$service_name"; then
|
||||||
|
sudo ufw allow 9000/tcp comment 'LMS-HTTP' 2>/dev/null || true
|
||||||
|
sudo ufw allow 3483/tcp comment 'LMS-SlimProto' 2>/dev/null || true
|
||||||
|
sudo ufw allow 3483/udp comment 'LMS-Discovery' 2>/dev/null || true
|
||||||
|
|
||||||
|
log_success "LMS installed"
|
||||||
|
echo " Web interface: http://$(get_ip_address):9000"
|
||||||
|
else
|
||||||
|
log_warning "LMS installed, but systemctl enable/start failed - check 'systemctl status $service_name'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_uninstall_lms() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Remove LMS Server?" "n" || { echo "Cancelled"; pause; return; }
|
||||||
|
|
||||||
|
local service_name
|
||||||
|
service_name=$(lms_service_name)
|
||||||
|
|
||||||
|
if [[ -n "$service_name" ]]; then
|
||||||
|
echo "Stopping $service_name..."
|
||||||
|
sudo systemctl stop "$service_name" 2>/dev/null || true
|
||||||
|
sudo systemctl disable "$service_name" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to remove both possible package names - only one will actually
|
||||||
|
# be installed, the other is a harmless no-op.
|
||||||
|
sudo apt remove -y lyrionmusicserver 2>/dev/null || true
|
||||||
|
sudo apt remove -y logitechmediaserver 2>/dev/null || true
|
||||||
|
|
||||||
|
sudo rm -f /etc/apt/sources.list.d/lms.list
|
||||||
|
sudo rm -f /usr/share/keyrings/lms-keyring.gpg
|
||||||
|
|
||||||
|
if ask_yes_no "Remove LMS data and configuration?" "n"; then
|
||||||
|
sudo rm -rf /var/lib/squeezeboxserver
|
||||||
|
sudo rm -rf /etc/squeezeboxserver
|
||||||
|
log_success "LMS and data removed"
|
||||||
|
else
|
||||||
|
log_success "LMS removed (data preserved)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Actions - Squeezelite Player
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
action_install_squeezelite() {
|
||||||
|
echo
|
||||||
|
if squeezelite_is_installed; then
|
||||||
|
echo "Squeezelite is already installed."
|
||||||
|
ask_yes_no "Reconfigure?" "n" || { pause; return; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v squeezelite &>/dev/null; then
|
||||||
|
if ! sudo apt install -y squeezelite; then
|
||||||
|
log_error "squeezelite package installation failed"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local player_name
|
||||||
|
player_name=$(ask_text "Player name" "Kiosk")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "LMS Server Configuration:"
|
||||||
|
echo " Enter IP:PORT of your LMS server"
|
||||||
|
echo " Leave blank for auto-discovery on LAN"
|
||||||
|
echo
|
||||||
|
local lms_server
|
||||||
|
lms_server=$(ask_text "LMS Server (e.g., 192.168.1.100:3483)" "")
|
||||||
|
|
||||||
|
sudo tee "$BIN_DIR/squeezelite-start.sh" > /dev/null <<SQSTART
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PLAYER_NAME="$player_name"
|
||||||
|
LMS_SERVER="$lms_server"
|
||||||
|
|
||||||
|
for i in {1..20}; do
|
||||||
|
pactl info >/dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! pactl info >/dev/null 2>&1; then
|
||||||
|
logger "ERROR: Squeezelite - PipeWire not available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "\$LMS_SERVER" ]]; then
|
||||||
|
exec /usr/bin/squeezelite -n "\$PLAYER_NAME" -s "\$LMS_SERVER" -o pulse -a 80:4:: -b 512:1024 -C 5
|
||||||
|
else
|
||||||
|
exec /usr/bin/squeezelite -n "\$PLAYER_NAME" -o pulse -a 80:4:: -b 512:1024 -C 5
|
||||||
|
fi
|
||||||
|
SQSTART
|
||||||
|
|
||||||
|
sudo chmod +x "$BIN_DIR/squeezelite-start.sh"
|
||||||
|
|
||||||
|
local kiosk_uid
|
||||||
|
kiosk_uid=$(id -u "$KIOSK_USER")
|
||||||
|
|
||||||
|
sudo tee "$SYSTEMD_DIR/squeezelite.service" > /dev/null <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Squeezelite
|
||||||
|
After=sound.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$KIOSK_USER
|
||||||
|
Environment="XDG_RUNTIME_DIR=/run/user/$kiosk_uid"
|
||||||
|
ExecStartPre=/bin/sleep 10
|
||||||
|
ExecStart=${BIN_DIR}/squeezelite-start.sh
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload 2>/dev/null || true
|
||||||
|
# Enable only, not start: squeezelite needs the kiosk user's real
|
||||||
|
# session (PipeWire, XDG_RUNTIME_DIR) up first, which is why a reboot
|
||||||
|
# is required below rather than starting it immediately.
|
||||||
|
if ! sudo systemctl enable squeezelite 2>/dev/null; then
|
||||||
|
log_warning "Squeezelite files written, but 'systemctl enable' failed - check 'systemctl status squeezelite'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Squeezelite installed: $player_name"
|
||||||
|
if [[ -n "$lms_server" ]]; then
|
||||||
|
echo " Server: $lms_server"
|
||||||
|
else
|
||||||
|
echo " Server: Auto-discovery"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "⚠️ IMPORTANT: Squeezelite requires a reboot to work properly"
|
||||||
|
echo
|
||||||
|
if ask_yes_no "Reboot now?" "n"; then
|
||||||
|
echo "Rebooting in 5 seconds..."
|
||||||
|
sleep 5
|
||||||
|
sudo reboot
|
||||||
|
else
|
||||||
|
echo "⚠️ Remember to reboot before using Squeezelite"
|
||||||
|
echo " Command: sudo reboot"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_uninstall_squeezelite() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Remove Squeezelite Player?" "n" || { echo "Cancelled"; pause; return; }
|
||||||
|
|
||||||
|
sudo systemctl stop squeezelite 2>/dev/null || true
|
||||||
|
sudo systemctl disable squeezelite 2>/dev/null || true
|
||||||
|
sudo rm -f "$SYSTEMD_DIR/squeezelite.service"
|
||||||
|
sudo rm -f "$BIN_DIR/squeezelite-start.sh"
|
||||||
|
sudo apt remove -y squeezelite 2>/dev/null || true
|
||||||
|
log_success "Squeezelite removed"
|
||||||
|
|
||||||
|
pause
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# menus/addon_remote_access.sh - "Remote Access" addon (VNC, WireGuard,
|
||||||
|
# Tailscale, Netbird).
|
||||||
|
#
|
||||||
|
# Third Addon migrated, and the biggest so far in scope (4 sub-areas).
|
||||||
|
# All four genuinely mutate real system state at fixed paths this project
|
||||||
|
# doesn't own the layout of (apt packages, /etc/wireguard, real VPN
|
||||||
|
# client CLIs) - same risk class as CUPS. Only $WIREGUARD_DIR and
|
||||||
|
# $SYSTEMD_DIR (lib/config.sh) are parameterized, since those are the
|
||||||
|
# only paths this file itself writes to; every command (apt, systemctl,
|
||||||
|
# wg, tailscale, netbird, x11vnc) gets full stubbing in every test.
|
||||||
|
#
|
||||||
|
# Tailscale and Netbird install themselves via `curl -fsSL <vendor
|
||||||
|
# url> | sh` - the vendors' own documented install method, preserved as-
|
||||||
|
# is rather than redesigned. This is NEVER allowed to run for real in
|
||||||
|
# any test: curl itself is stubbed, not just sudo, so there is no path
|
||||||
|
# by which a test could reach the network.
|
||||||
|
#
|
||||||
|
# None of x11vnc/wg/tailscale/netbird are installed in a fresh
|
||||||
|
# environment, so their "not installed" detection is real/unstubbed and
|
||||||
|
# safe to exercise end-to-end - only the "install" actions need stubs.
|
||||||
|
#
|
||||||
|
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
remote_access_status() {
|
||||||
|
echo "VNC: $(is_service_active x11vnc && echo "running" || echo "not installed")"
|
||||||
|
echo "WireGuard: $(wireguard_connected && echo "connected" || (command -v wg &>/dev/null && echo "installed, not connected" || echo "not installed"))"
|
||||||
|
echo "Tailscale: $(command -v tailscale &>/dev/null && echo "installed" || echo "not installed")"
|
||||||
|
echo "Netbird: $(command -v netbird &>/dev/null && echo "installed" || echo "not installed")"
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_access_menu_builder() {
|
||||||
|
MENU_LABELS=("VNC Remote Desktop" "WireGuard VPN" "Tailscale VPN" "Netbird VPN")
|
||||||
|
MENU_HANDLERS=(vnc_menu wireguard_menu tailscale_menu netbird_menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_access_menu() {
|
||||||
|
run_menu "REMOTE ACCESS" remote_access_menu_builder remote_access_status
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# VNC (x11vnc)
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
vnc_status() {
|
||||||
|
if is_service_active x11vnc; then
|
||||||
|
echo "VNC: running - connect to $(get_ip_address):5900"
|
||||||
|
else
|
||||||
|
echo "VNC: not installed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
vnc_menu_builder() {
|
||||||
|
if is_service_active x11vnc; then
|
||||||
|
MENU_LABELS=("Reconfigure password" "Uninstall")
|
||||||
|
MENU_HANDLERS=(action_vnc_change_password action_vnc_uninstall)
|
||||||
|
else
|
||||||
|
MENU_LABELS=("Install x11vnc")
|
||||||
|
MENU_HANDLERS=(action_vnc_install)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
vnc_menu() {
|
||||||
|
run_menu "VNC REMOTE DESKTOP" vnc_menu_builder vnc_status
|
||||||
|
}
|
||||||
|
|
||||||
|
action_vnc_install() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Install x11vnc?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
if ! sudo apt install -y x11vnc; then
|
||||||
|
log_error "x11vnc installation failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local vnc_pass
|
||||||
|
read -r -s -p "VNC password: " vnc_pass
|
||||||
|
echo
|
||||||
|
if [[ -z "$vnc_pass" ]]; then
|
||||||
|
log_error "No password provided - cancelled"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -u "$KIOSK_USER" mkdir -p "$KIOSK_HOME/.vnc"
|
||||||
|
sudo -u "$KIOSK_USER" x11vnc -storepasswd "$vnc_pass" "$KIOSK_HOME/.vnc/passwd"
|
||||||
|
|
||||||
|
sudo tee "$SYSTEMD_DIR/x11vnc.service" > /dev/null <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=x11vnc Remote Desktop
|
||||||
|
After=lightdm.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${KIOSK_USER}
|
||||||
|
ExecStart=/usr/bin/x11vnc -display :0 -auth guess -rfbauth ${KIOSK_HOME}/.vnc/passwd -forever -loop -noxdamage -repeat -shared
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if enable_and_start_units x11vnc; then
|
||||||
|
sudo ufw allow 5900/tcp comment 'VNC' 2>/dev/null || true
|
||||||
|
log_success "VNC installed - connect to $(get_ip_address):5900"
|
||||||
|
else
|
||||||
|
log_warning "x11vnc installed but systemctl enable/start failed - check 'systemctl status x11vnc'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_vnc_change_password() {
|
||||||
|
echo
|
||||||
|
local vnc_pass
|
||||||
|
read -r -s -p "New VNC password: " vnc_pass
|
||||||
|
echo
|
||||||
|
if [[ -z "$vnc_pass" ]]; then
|
||||||
|
log_error "No password provided - cancelled"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -u "$KIOSK_USER" x11vnc -storepasswd "$vnc_pass" "$KIOSK_HOME/.vnc/passwd"
|
||||||
|
if sudo systemctl restart x11vnc 2>/dev/null; then
|
||||||
|
log_success "VNC password updated"
|
||||||
|
else
|
||||||
|
log_warning "Password file updated, but restarting x11vnc failed - check 'systemctl status x11vnc'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_vnc_uninstall() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Remove VNC?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
sudo systemctl stop x11vnc 2>/dev/null || true
|
||||||
|
sudo systemctl disable x11vnc 2>/dev/null || true
|
||||||
|
sudo rm -f "$SYSTEMD_DIR/x11vnc.service"
|
||||||
|
sudo apt remove -y x11vnc
|
||||||
|
log_success "VNC removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# WireGuard
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
wireguard_connected() {
|
||||||
|
command -v wg &>/dev/null && sudo wg show 2>/dev/null | grep -q interface
|
||||||
|
}
|
||||||
|
|
||||||
|
wireguard_status() {
|
||||||
|
if wireguard_connected; then
|
||||||
|
echo "WireGuard: connected"
|
||||||
|
sudo wg show 2>/dev/null | grep -E "interface:|endpoint:|allowed ips:" | sed 's/^/ /' || true
|
||||||
|
elif command -v wg &>/dev/null; then
|
||||||
|
echo "WireGuard: installed, not connected"
|
||||||
|
else
|
||||||
|
echo "WireGuard: not installed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wireguard_menu_builder() {
|
||||||
|
if wireguard_connected; then
|
||||||
|
MENU_LABELS=("Show full config" "Paste new config" "Uninstall")
|
||||||
|
MENU_HANDLERS=(action_wireguard_show_config action_wireguard_paste_config action_wireguard_uninstall)
|
||||||
|
elif command -v wg &>/dev/null; then
|
||||||
|
MENU_LABELS=("Paste config" "Uninstall")
|
||||||
|
MENU_HANDLERS=(action_wireguard_paste_config action_wireguard_uninstall)
|
||||||
|
else
|
||||||
|
MENU_LABELS=("Install WireGuard")
|
||||||
|
MENU_HANDLERS=(action_wireguard_install)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wireguard_menu() {
|
||||||
|
run_menu "WIREGUARD VPN" wireguard_menu_builder wireguard_status
|
||||||
|
}
|
||||||
|
|
||||||
|
action_wireguard_install() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Install WireGuard?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
if ! sudo apt install -y wireguard wireguard-tools; then
|
||||||
|
log_error "WireGuard installation failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "WireGuard installed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if ask_yes_no "Paste a config now?" "n"; then
|
||||||
|
action_wireguard_paste_config
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_wireguard_show_config() {
|
||||||
|
echo
|
||||||
|
sudo wg show all
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reads a WireGuard config from stdin until EOF (Ctrl+D on a real
|
||||||
|
# terminal) - same as the legacy addon. Writes to $WIREGUARD_DIR rather
|
||||||
|
# than a hardcoded /etc/wireguard, so tests can point it at scratch space
|
||||||
|
# and verify the written content without touching the real directory.
|
||||||
|
action_wireguard_paste_config() {
|
||||||
|
echo
|
||||||
|
echo "Paste your WireGuard config (Ctrl+D when done):"
|
||||||
|
local config
|
||||||
|
config=$(cat)
|
||||||
|
|
||||||
|
if [[ -z "$config" ]]; then
|
||||||
|
log_error "No config provided"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wg_name
|
||||||
|
wg_name=$(ask_text "Config name" "wg0")
|
||||||
|
|
||||||
|
sudo mkdir -p "$WIREGUARD_DIR"
|
||||||
|
echo "$config" | sudo tee "$WIREGUARD_DIR/${wg_name}.conf" > /dev/null
|
||||||
|
sudo chmod 600 "$WIREGUARD_DIR/${wg_name}.conf"
|
||||||
|
|
||||||
|
if enable_and_start_units "wg-quick@${wg_name}"; then
|
||||||
|
log_success "WireGuard configured: $wg_name"
|
||||||
|
else
|
||||||
|
log_warning "Config written, but systemctl enable/start failed - check 'systemctl status wg-quick@${wg_name}'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_wireguard_uninstall() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Remove WireGuard?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
sudo systemctl stop 'wg-quick@*' 2>/dev/null || true
|
||||||
|
sudo systemctl disable 'wg-quick@*' 2>/dev/null || true
|
||||||
|
sudo apt remove -y wireguard wireguard-tools
|
||||||
|
log_success "WireGuard removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Tailscale
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
tailscale_backend_state() {
|
||||||
|
tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"' 2>/dev/null || echo "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
tailscale_status() {
|
||||||
|
if ! command -v tailscale &>/dev/null; then
|
||||||
|
echo "Tailscale: not installed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(tailscale_backend_state)" == "Running" ]]; then
|
||||||
|
echo "Tailscale: connected"
|
||||||
|
echo " Hostname: $(tailscale status --json 2>/dev/null | jq -r '.Self.HostName // "unknown"')"
|
||||||
|
echo " IP: $(tailscale ip -4 2>/dev/null)"
|
||||||
|
else
|
||||||
|
echo "Tailscale: installed, not connected"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tailscale_menu_builder() {
|
||||||
|
if command -v tailscale &>/dev/null; then
|
||||||
|
MENU_LABELS=("Connect (interactive)" "Connect with auth key" "Show status" "Uninstall")
|
||||||
|
MENU_HANDLERS=(action_tailscale_connect_interactive action_tailscale_connect_authkey action_tailscale_show_status action_tailscale_uninstall)
|
||||||
|
else
|
||||||
|
MENU_LABELS=("Install Tailscale")
|
||||||
|
MENU_HANDLERS=(action_tailscale_install)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tailscale_menu() {
|
||||||
|
run_menu "TAILSCALE VPN" tailscale_menu_builder tailscale_status
|
||||||
|
}
|
||||||
|
|
||||||
|
action_tailscale_install() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Install Tailscale?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
if ! curl -fsSL https://tailscale.com/install.sh | sh; then
|
||||||
|
log_error "Tailscale installation failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "Tailscale installed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Options:"
|
||||||
|
echo " 1. Connect now (interactive)"
|
||||||
|
echo " 2. Connect with auth key"
|
||||||
|
echo " 3. Connect later"
|
||||||
|
local choice
|
||||||
|
choice=$(ask_integer "Choose" "3" 1 3)
|
||||||
|
case "$choice" in
|
||||||
|
1) action_tailscale_connect_interactive ;;
|
||||||
|
2) action_tailscale_connect_authkey ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
action_tailscale_connect_interactive() {
|
||||||
|
echo
|
||||||
|
if sudo tailscale up; then
|
||||||
|
log_success "Tailscale connected"
|
||||||
|
else
|
||||||
|
log_error "Tailscale connection failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_tailscale_connect_authkey() {
|
||||||
|
echo
|
||||||
|
echo "Get an auth key from: https://login.tailscale.com/admin/settings/keys"
|
||||||
|
local authkey
|
||||||
|
read -r -p "Enter auth key: " authkey
|
||||||
|
if [[ -z "$authkey" ]]; then
|
||||||
|
echo "Cancelled"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if sudo tailscale up --authkey="$authkey"; then
|
||||||
|
log_success "Tailscale connected"
|
||||||
|
else
|
||||||
|
log_error "Tailscale connection failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_tailscale_show_status() {
|
||||||
|
echo
|
||||||
|
tailscale status
|
||||||
|
}
|
||||||
|
|
||||||
|
action_tailscale_uninstall() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Remove Tailscale?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
sudo tailscale down 2>/dev/null || true
|
||||||
|
sudo apt remove -y tailscale
|
||||||
|
log_success "Tailscale removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Netbird
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
netbird_connected() {
|
||||||
|
[[ "$(netbird status 2>/dev/null | grep "Status:" | awk '{print $2}')" == "Connected" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
netbird_status() {
|
||||||
|
if ! command -v netbird &>/dev/null; then
|
||||||
|
echo "Netbird: not installed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if netbird_connected; then
|
||||||
|
echo "Netbird: connected"
|
||||||
|
netbird status 2>/dev/null | grep -E "NetBird IP:|Public key:" | sed 's/^/ /' || true
|
||||||
|
else
|
||||||
|
echo "Netbird: installed, not connected"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
netbird_menu_builder() {
|
||||||
|
if command -v netbird &>/dev/null; then
|
||||||
|
MENU_LABELS=("Connect with setup key" "Show status" "Uninstall")
|
||||||
|
MENU_HANDLERS=(action_netbird_connect action_netbird_show_status action_netbird_uninstall)
|
||||||
|
else
|
||||||
|
MENU_LABELS=("Install Netbird")
|
||||||
|
MENU_HANDLERS=(action_netbird_install)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
netbird_menu() {
|
||||||
|
run_menu "NETBIRD VPN" netbird_menu_builder netbird_status
|
||||||
|
}
|
||||||
|
|
||||||
|
action_netbird_install() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Install Netbird?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
if ! curl -fsSL https://pkgs.netbird.io/install.sh | sh; then
|
||||||
|
log_error "Netbird installation failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "Netbird installed"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if ask_yes_no "Connect with a setup key now?" "n"; then
|
||||||
|
action_netbird_connect
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_netbird_connect() {
|
||||||
|
echo
|
||||||
|
echo "Get a setup key from the Netbird dashboard"
|
||||||
|
local setup_key
|
||||||
|
read -r -p "Enter setup key: " setup_key
|
||||||
|
if [[ -z "$setup_key" ]]; then
|
||||||
|
echo "Cancelled"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if sudo netbird up --setup-key "$setup_key"; then
|
||||||
|
log_success "Netbird connected"
|
||||||
|
else
|
||||||
|
log_error "Netbird connection failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_netbird_show_status() {
|
||||||
|
echo
|
||||||
|
netbird status
|
||||||
|
}
|
||||||
|
|
||||||
|
action_netbird_uninstall() {
|
||||||
|
echo
|
||||||
|
ask_yes_no "Remove Netbird?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
|
sudo netbird down 2>/dev/null || true
|
||||||
|
sudo apt remove -y netbird
|
||||||
|
log_success "Netbird removed"
|
||||||
|
}
|
||||||
+5
-16
@@ -46,17 +46,6 @@ timer_oncalendar() {
|
|||||||
grep "^OnCalendar=" "$SYSTEMD_DIR/$1" 2>/dev/null | cut -d'=' -f2 | sed 's/\*-\*-\* //' | sed 's/:00$//'
|
grep "^OnCalendar=" "$SYSTEMD_DIR/$1" 2>/dev/null | cut -d'=' -f2 | sed 's/\*-\*-\* //' | sed 's/:00$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
# enable_and_start_timers TIMER [TIMER...]
|
|
||||||
# Reloads systemd and enables+starts the given timer units, returning
|
|
||||||
# non-zero if enable or start fails (e.g. systemd/D-Bus unreachable).
|
|
||||||
# Always call this from an `if`/`&&`/`||` context: this whole tool runs
|
|
||||||
# under set -e, so a bare, unguarded call whose last command fails would
|
|
||||||
# take down the entire session instead of just this one action.
|
|
||||||
enable_and_start_timers() {
|
|
||||||
sudo systemctl daemon-reload 2>/dev/null || true
|
|
||||||
sudo systemctl enable "$@" 2>/dev/null && sudo systemctl start "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Top-level menu
|
# Top-level menu
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -66,7 +55,7 @@ power_schedule_status() {
|
|||||||
|
|
||||||
if timer_exists kiosk-shutdown.timer; then
|
if timer_exists kiosk-shutdown.timer; then
|
||||||
any=true
|
any=true
|
||||||
local t; t=$(timer_oncalendar kiosk-shutdown.timer)
|
local t; t=$(timer_oncalendar kiosk-shutdown.timer) || true
|
||||||
echo "Power: shutdown daily at ${t:-an unknown time}"
|
echo "Power: shutdown daily at ${t:-an unknown time}"
|
||||||
fi
|
fi
|
||||||
if timer_exists kiosk-display-off.timer; then
|
if timer_exists kiosk-display-off.timer; then
|
||||||
@@ -205,7 +194,7 @@ EOF
|
|||||||
log_info "RTC wake cron job created"
|
log_info "RTC wake cron job created"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if enable_and_start_timers kiosk-shutdown.timer; then
|
if enable_and_start_units kiosk-shutdown.timer; then
|
||||||
log_success "Power schedule configured: shutdown at ${shutdown_time}$( [[ -n "$wake_time" ]] && echo ", wake at ${wake_time}")"
|
log_success "Power schedule configured: shutdown at ${shutdown_time}$( [[ -n "$wake_time" ]] && echo ", wake at ${wake_time}")"
|
||||||
else
|
else
|
||||||
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-shutdown.timer'"
|
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-shutdown.timer'"
|
||||||
@@ -342,7 +331,7 @@ Persistent=true
|
|||||||
WantedBy=timers.target
|
WantedBy=timers.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if enable_and_start_timers kiosk-display-off.timer kiosk-display-on.timer; then
|
if enable_and_start_units kiosk-display-off.timer kiosk-display-on.timer; then
|
||||||
log_success "Display schedule configured: off at ${doff}, on at ${don}"
|
log_success "Display schedule configured: off at ${doff}, on at ${don}"
|
||||||
else
|
else
|
||||||
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-display-off.timer'"
|
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-display-off.timer'"
|
||||||
@@ -476,7 +465,7 @@ EOF
|
|||||||
local mode_label="All audio muted"
|
local mode_label="All audio muted"
|
||||||
[[ "$qmode" == "2" ]] && mode_label="Squeezelite stopped"
|
[[ "$qmode" == "2" ]] && mode_label="Squeezelite stopped"
|
||||||
|
|
||||||
if enable_and_start_timers kiosk-quiet-start.timer kiosk-quiet-end.timer; then
|
if enable_and_start_units kiosk-quiet-start.timer kiosk-quiet-end.timer; then
|
||||||
log_success "Quiet hours configured: ${qstart} to ${qend} (${mode_label})"
|
log_success "Quiet hours configured: ${qstart} to ${qend} (${mode_label})"
|
||||||
else
|
else
|
||||||
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-quiet-start.timer'"
|
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-quiet-start.timer'"
|
||||||
@@ -561,7 +550,7 @@ Persistent=true
|
|||||||
WantedBy=timers.target
|
WantedBy=timers.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if enable_and_start_timers kiosk-electron-reload.timer; then
|
if enable_and_start_units kiosk-electron-reload.timer; then
|
||||||
log_success "Electron reload configured: $description"
|
log_success "Electron reload configured: $description"
|
||||||
else
|
else
|
||||||
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-electron-reload.timer'"
|
log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-electron-reload.timer'"
|
||||||
|
|||||||
+70
-7
@@ -1,8 +1,69 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
################################################################################
|
################################################################################
|
||||||
### Ubuntu Based Kiosk v2.7.0 ###
|
### Ubuntu Based Kiosk v2.9.0 ###
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
|
# RELEASE v2.9.0 - LMS Server / Squeezelite Player Migrated;
|
||||||
|
# is_service_enabled() Dead Pre-Check Fixed
|
||||||
|
# - New in ./install.sh: LMS Server / Squeezelite Player
|
||||||
|
# (menus/addon_lms_squeezelite.sh) - install/reconfigure/uninstall for
|
||||||
|
# an LMS (Lyrion/Logitech Media Server) server the kiosk can host, and
|
||||||
|
# a Squeezelite player the kiosk can run against any LMS server on the
|
||||||
|
# LAN. Squeezelite's own start script and systemd unit now go through
|
||||||
|
# $BIN_DIR/$SYSTEMD_DIR (lib/config.sh) instead of hardcoded
|
||||||
|
# /usr/local/bin and /etc/systemd/system, matching every other addon;
|
||||||
|
# LMS's own apt repo/GPG key/ufw rules stay at their real fixed system
|
||||||
|
# paths, same as CUPS.
|
||||||
|
# - Fixed a real unguarded-pipeline bug from the legacy install_lms():
|
||||||
|
# `sudo systemctl enable "$service_name" 2>&1 | tee /tmp/lms-enable.log`
|
||||||
|
# made the whole statement's exit status depend on `tee` (always 0)
|
||||||
|
# instead of `systemctl enable`, so a real enable/start failure was
|
||||||
|
# silently swallowed rather than falling through to a warning. Now
|
||||||
|
# uses the shared enable_and_start_units() helper instead.
|
||||||
|
# - Fixed is_service_enabled() (shared by both scripts): its pre-check
|
||||||
|
# `systemctl list-unit-files | grep -q "^${service}\s"` never matched,
|
||||||
|
# since every call site passes a bare service name (e.g.
|
||||||
|
# "squeezelite") while list-unit-files lines start with
|
||||||
|
# "squeezelite.service" - so the function always fell through to
|
||||||
|
# `return 1` regardless of the real enabled state. `systemctl
|
||||||
|
# is-enabled` already reports "not found" as a failure on its own, so
|
||||||
|
# the dead pre-check is simply dropped. Backported here since it's the
|
||||||
|
# same shared function in both scripts and the fix is low-risk
|
||||||
|
# (behavior-preserving for every state except the one it was silently
|
||||||
|
# getting wrong).
|
||||||
|
#
|
||||||
|
# RELEASE v2.8.0 - Remote Access Migrated (VNC/WireGuard/Tailscale/
|
||||||
|
# Netbird); Framework-Level Status-Function Crash Fixed
|
||||||
|
# - New in ./install.sh: Remote Access (menus/addon_remote_access.sh) -
|
||||||
|
# VNC (x11vnc), WireGuard, Tailscale, and Netbird, each with its own
|
||||||
|
# install/connect/status/uninstall flow. The biggest Addon migrated so
|
||||||
|
# far (4 sub-areas). Tailscale and Netbird install via the vendors'
|
||||||
|
# own documented `curl -fsSL <url> | sh` method, preserved as-is.
|
||||||
|
# - New $WIREGUARD_DIR (lib/config.sh), same pattern as $SYSTEMD_DIR
|
||||||
|
# etc - nothing here hardcodes /etc/wireguard.
|
||||||
|
# - Promoted power_schedule.sh's enable_and_start_timers() to a shared
|
||||||
|
# enable_and_start_units() in lib/menu.sh (works for services now too,
|
||||||
|
# not just timers) - Remote Access needed the identical pattern for
|
||||||
|
# x11vnc and wg-quick@, so this is now fixed and reusable everywhere
|
||||||
|
# instead of being duplicated a second time.
|
||||||
|
# - IMPORTANT framework-level bug found and fixed in lib/menu.sh's
|
||||||
|
# run_menu(): the *handler* call has been `|| true`-guarded since
|
||||||
|
# v2.1.0, but the *status function* call was still bare and completely
|
||||||
|
# unprotected. A status function's job is read-only display, but if
|
||||||
|
# one contains so much as a pipeline whose grep matches nothing (which
|
||||||
|
# pipefail turns into a pipeline failure even though the actual last
|
||||||
|
# command in it succeeds), that bare call would crash the *entire
|
||||||
|
# session* - not just fail to show status. Found while writing
|
||||||
|
# wireguard_status()'s `sudo wg show | grep ... | sed ...` and
|
||||||
|
# confirming its exact failure mode before assuming it was already
|
||||||
|
# covered. Fixed once in run_menu() itself, protecting every status
|
||||||
|
# function across every menu, present and future - same "fix once at
|
||||||
|
# the framework level" pattern as the v2.1.0 handler fix. Also audited
|
||||||
|
# every existing status function across all menus for the same
|
||||||
|
# specific shape (a bare `var=$(...)` assignment from a grep-based
|
||||||
|
# pipeline, not embedded in an echo and not already guarded) and found
|
||||||
|
# one real instance in power_schedule_status(), now fixed too.
|
||||||
|
#
|
||||||
# RELEASE v2.7.0 - Backported Fix: save_config() No Longer Deletes
|
# RELEASE v2.7.0 - Backported Fix: save_config() No Longer Deletes
|
||||||
# Authelia Credentials (or Any Other Untracked Field)
|
# Authelia Credentials (or Any Other Untracked Field)
|
||||||
# - This script's own save_config() had the exact bug described under
|
# - This script's own save_config() had the exact bug described under
|
||||||
@@ -288,7 +349,7 @@ set -euo pipefail
|
|||||||
### SECTION 1: CONSTANTS & GLOBALS
|
### SECTION 1: CONSTANTS & GLOBALS
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
SCRIPT_VERSION="2.7.0"
|
SCRIPT_VERSION="2.9.0"
|
||||||
|
|
||||||
# Resolve the real path to this script file.
|
# Resolve the real path to this script file.
|
||||||
# When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,
|
# When piped (curl|bash or wget|bash), BASH_SOURCE[0] is a pipe descriptor,
|
||||||
@@ -546,12 +607,14 @@ is_service_active() {
|
|||||||
|
|
||||||
is_service_enabled() {
|
is_service_enabled() {
|
||||||
local service="$1"
|
local service="$1"
|
||||||
# Check if service file exists first
|
# `systemctl is-enabled` already reports "not found" as a failure on
|
||||||
if systemctl list-unit-files 2>/dev/null | grep -q "^${service}\s"; then
|
# its own - no need for (and no correct way to write, given every
|
||||||
|
# call site here passes a bare service name while list-unit-files
|
||||||
|
# lines start with "$service.service") a pre-check via
|
||||||
|
# list-unit-files. The previous "^${service}\s" pre-check never
|
||||||
|
# matched, so this function always fell through to `return 1`
|
||||||
|
# regardless of the real enabled state.
|
||||||
systemctl is-enabled --quiet "$service" 2>/dev/null
|
systemctl is-enabled --quiet "$service" 2>/dev/null
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_ip_address() {
|
get_ip_address() {
|
||||||
|
|||||||
Reference in New Issue
Block a user