Offline addons, LAN-aware emergency hotspot, LAN time server
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
This commit is contained in:
@@ -41,9 +41,10 @@ on:
|
|||||||
default: ""
|
default: ""
|
||||||
offline:
|
offline:
|
||||||
description: >-
|
description: >-
|
||||||
Bundle everything install.sh downloads (~420 MB: all packages incl.
|
Bundle everything install.sh downloads (~520 MB: all packages incl.
|
||||||
CUPS printing, Node.js, Electron) so the kiosk installs with no
|
the CUPS, LMS/Squeezelite, Intercom, VNC, WireGuard and hotspot
|
||||||
internet on first boot.
|
addons, Node.js, Electron) so the kiosk and those addons install
|
||||||
|
with no internet.
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
retention_days:
|
retention_days:
|
||||||
|
|||||||
@@ -107,19 +107,21 @@ iso/build-iso.sh --offline # also offers to install apt-utils, python3, gnupg,
|
|||||||
# -> iso/build/ubuntu-26.04.x-kiosk-offline-amd64.iso (~3.2 GB)
|
# -> iso/build/ubuntu-26.04.x-kiosk-offline-amd64.iso (~3.2 GB)
|
||||||
```
|
```
|
||||||
|
|
||||||
This adds a ~420 MB bundle to the ISO:
|
This adds a ~520 MB bundle to the ISO (~3.3 GB total):
|
||||||
- Every Ubuntu package the kiosk install and the **CUPS printing addon**
|
- Every Ubuntu package the kiosk install needs, plus the addons that make
|
||||||
need (all printer drivers included), with their dependencies, as a local
|
sense on a network without internet: **CUPS printing** (all printer
|
||||||
package repository.
|
drivers), **LMS (Lyrion) music server** and **Squeezelite** player,
|
||||||
|
**Asterisk Intercom**, **VNC**, **WireGuard** and the **Emergency
|
||||||
|
Hotspot** - with their dependencies, as a local package repository.
|
||||||
- Node.js 22 (NodeSource).
|
- Node.js 22 (NodeSource).
|
||||||
- Prebuilt Electron and Web UI modules.
|
- Prebuilt Electron and Web UI modules.
|
||||||
|
|
||||||
On first boot it still installs online when there's internet. With none,
|
On first boot it still installs online when there's internet. With none,
|
||||||
it waits 90 seconds and then installs entirely from the bundle. The bundle
|
it waits 90 seconds and then installs entirely from the bundle. The bundle
|
||||||
stays on the kiosk at `/opt/kiosk-offline`, so **Addons → CUPS Printing**
|
stays on the kiosk at `/opt/kiosk-offline`, so those addons install from
|
||||||
(in `./install.sh` or the Web UI) works with no internet too: when it can't
|
the normal menus (`./install.sh` or the Web UI) with no internet too: when
|
||||||
reach the Ubuntu archive, it installs from the bundle by itself and then
|
an install can't reach the Ubuntu archive, it uses the bundle by itself and
|
||||||
switches apt back to normal.
|
then switches apt back to normal.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Build it on an **amd64** Ubuntu/Debian machine, or let the GitHub
|
- Build it on an **amd64** Ubuntu/Debian machine, or let the GitHub
|
||||||
@@ -128,8 +130,12 @@ Notes:
|
|||||||
- Bundled versions are whatever was current when the ISO was built.
|
- Bundled versions are whatever was current when the ISO was built.
|
||||||
Once the kiosk is online, normal updates and Advanced → Upgrade bring it
|
Once the kiosk is online, normal updates and Advanced → Upgrade bring it
|
||||||
current.
|
current.
|
||||||
- Addons that download their own software (LMS/Squeezelite, Tailscale,
|
- Tailscale and Netbird aren't bundled: they install from their vendors'
|
||||||
Netbird, Asterisk Intercom, VNC) still need internet.
|
scripts and only work with their vendors' servers on the internet.
|
||||||
|
- On a network with no internet, set **Core Settings → Time Server (NTP)**
|
||||||
|
to a time server on your network (most routers can serve time), or set
|
||||||
|
the clock by hand there - otherwise the clock slowly drifts, and lockout
|
||||||
|
times, quiet hours and power schedules drift with it.
|
||||||
- `sudo rm -rf /opt/kiosk-offline` frees the space if you don't need it.
|
- `sudo rm -rf /opt/kiosk-offline` frees the space if you don't need it.
|
||||||
|
|
||||||
**No Linux machine handy?** Build it on GitHub instead: **Actions → Build
|
**No Linux machine handy?** Build it on GitHub instead: **Actions → Build
|
||||||
@@ -1105,6 +1111,12 @@ Automatically creates a WiFi access point when internet connectivity is lost.
|
|||||||
- Optional configuration with custom SSID and password
|
- Optional configuration with custom SSID and password
|
||||||
- Can be deferred and configured later
|
- Can be deferred and configured later
|
||||||
|
|
||||||
|
**What counts as "cut off"** (chosen when you set it up; `./install.sh`
|
||||||
|
version): by default, *the local network itself* is unreachable - neither
|
||||||
|
the router nor the internet answers - so a kiosk on a network without
|
||||||
|
internet doesn't start the hotspot on every boot. "No internet" (the old
|
||||||
|
check) and "a specific address doesn't answer" are options too.
|
||||||
|
|
||||||
**How It Works:**
|
**How It Works:**
|
||||||
1. Service monitors internet connectivity every 30 seconds
|
1. Service monitors internet connectivity every 30 seconds
|
||||||
2. When internet is lost, automatically:
|
2. When internet is lost, automatically:
|
||||||
@@ -1477,6 +1489,9 @@ full migration pass.
|
|||||||
- **All-in-one install ISO** (`iso/build-iso.sh`, or Actions → Build kiosk ISO): Ubuntu Server 26.04 plus the kiosk in one USB stick — the Ubuntu install, then `install.sh` automatically on first boot. Interactive, unattended, or fully automatic. See "All-in-One Install ISO" above.
|
- **All-in-one install ISO** (`iso/build-iso.sh`, or Actions → Build kiosk ISO): Ubuntu Server 26.04 plus the kiosk in one USB stick — the Ubuntu install, then `install.sh` automatically on first boot. Interactive, unattended, or fully automatic. See "All-in-One Install ISO" above.
|
||||||
- **Offline installs** (`--offline`): the ISO carries every package (including CUPS printing and all printer drivers), Node.js 22, and prebuilt Electron/Web UI modules, so a kiosk installs with no internet. With internet it still installs current versions online. Addons → CUPS Printing (menu or Web UI) installs from the bundle by itself when offline.
|
- **Offline installs** (`--offline`): the ISO carries every package (including CUPS printing and all printer drivers), Node.js 22, and prebuilt Electron/Web UI modules, so a kiosk installs with no internet. With internet it still installs current versions online. Addons → CUPS Printing (menu or Web UI) installs from the bundle by itself when offline.
|
||||||
- **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI.
|
- **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI.
|
||||||
|
- **Offline addons:** the `--offline` bundle now also carries LMS (Lyrion) + Squeezelite, Asterisk Intercom, VNC, WireGuard and the Emergency Hotspot; their normal installs use it automatically when there's no internet.
|
||||||
|
- **Emergency Hotspot fix:** by default it now starts only when the local network is unreachable (router *and* internet), not merely when there's no internet - on a LAN without internet it used to start on every boot. Existing setups keep the old check until Advanced → Emergency Hotspot → Reconfigure.
|
||||||
|
- **New: Core Settings → Time Server (NTP)** - use a time server on your network, go back to Ubuntu's, or set the clock by hand; for networks without internet.
|
||||||
- **Word documents as sites:** a `.docx` path renders in the kiosk via docx-preview (~1 MB of JavaScript, bundled offline), updating when the file changes. Slideshow folders can mix `.docx` documents and PDFs (rendered with pdf.js) in with images.
|
- **Word documents as sites:** a `.docx` path renders in the kiosk via docx-preview (~1 MB of JavaScript, bundled offline), updating when the file changes. Slideshow folders can mix `.docx` documents and PDFs (rendered with pdf.js) in with images.
|
||||||
- **Electron upgrade:** v42.x → v44.x (42 leaves Electron's support window when 45 ships). Existing kiosks move to it via Advanced → Upgrade.
|
- **Electron upgrade:** v42.x → v44.x (42 leaves Electron's support window when 45 ships). Existing kiosks move to it via Advanced → Upgrade.
|
||||||
- **Fix:** the CUPS menu could report an installed CUPS as "not installed" (`dpkg -l | grep -q` failing under `set -o pipefail`).
|
- **Fix:** the CUPS menu could report an installed CUPS as "not installed" (`dpkg -l | grep -q` failing under `set -o pipefail`).
|
||||||
|
|||||||
+6
-1
@@ -24,7 +24,8 @@
|
|||||||
#
|
#
|
||||||
# Migrated so far, grouped the same way the legacy menu groups them:
|
# Migrated so far, grouped the same way the legacy menu groups them:
|
||||||
# Core Settings: Sites & Page Timing, Display & Interaction, Timezone,
|
# Core Settings: Sites & Page Timing, Display & Interaction, Timezone,
|
||||||
# Hidden Site PIN, Password Protection & Lockout, WiFi,
|
# Time Server (NTP - menus/time_sync.sh, a LAN time server or manual
|
||||||
|
# clock for networks without internet), Hidden Site PIN, Password Protection & Lockout, WiFi,
|
||||||
# Power/Display/Quiet Hours, Complete Uninstall
|
# Power/Display/Quiet Hours, Complete Uninstall
|
||||||
# (menus/complete_uninstall.sh - composed from every addon's own
|
# (menus/complete_uninstall.sh - composed from every addon's own
|
||||||
# uninstall helper rather than re-implementing removal a second time).
|
# uninstall helper rather than re-implementing removal a second time).
|
||||||
@@ -72,6 +73,8 @@ source "$SCRIPT_DIR/menus/sites.sh"
|
|||||||
source "$SCRIPT_DIR/menus/display.sh"
|
source "$SCRIPT_DIR/menus/display.sh"
|
||||||
# shellcheck source=menus/timezone.sh
|
# shellcheck source=menus/timezone.sh
|
||||||
source "$SCRIPT_DIR/menus/timezone.sh"
|
source "$SCRIPT_DIR/menus/timezone.sh"
|
||||||
|
# shellcheck source=menus/time_sync.sh
|
||||||
|
source "$SCRIPT_DIR/menus/time_sync.sh"
|
||||||
# shellcheck source=menus/hidden_pin.sh
|
# shellcheck source=menus/hidden_pin.sh
|
||||||
source "$SCRIPT_DIR/menus/hidden_pin.sh"
|
source "$SCRIPT_DIR/menus/hidden_pin.sh"
|
||||||
# shellcheck source=menus/lockout.sh
|
# shellcheck source=menus/lockout.sh
|
||||||
@@ -151,6 +154,7 @@ core_settings_menu_builder() {
|
|||||||
"Sites & Page Timing"
|
"Sites & Page Timing"
|
||||||
"Display & Interaction"
|
"Display & Interaction"
|
||||||
"Timezone"
|
"Timezone"
|
||||||
|
"Time Server (NTP)"
|
||||||
"Hidden Site PIN"
|
"Hidden Site PIN"
|
||||||
"Password Protection & Lockout"
|
"Password Protection & Lockout"
|
||||||
"WiFi"
|
"WiFi"
|
||||||
@@ -161,6 +165,7 @@ core_settings_menu_builder() {
|
|||||||
sites_menu
|
sites_menu
|
||||||
display_menu
|
display_menu
|
||||||
timezone_menu
|
timezone_menu
|
||||||
|
time_sync_menu
|
||||||
hidden_pin_menu
|
hidden_pin_menu
|
||||||
lockout_menu
|
lockout_menu
|
||||||
wifi_menu
|
wifi_menu
|
||||||
|
|||||||
+3
-2
@@ -70,8 +70,9 @@
|
|||||||
# --repo-url URL git remote recorded in the copied repo, used by
|
# --repo-url URL git remote recorded in the copied repo, used by
|
||||||
# Advanced -> Upgrade (default: this checkout's
|
# Advanced -> Upgrade (default: this checkout's
|
||||||
# origin, else the upstream GitHub repo).
|
# origin, else the upstream GitHub repo).
|
||||||
# --offline Also bundle everything install.sh downloads (~420 MB:
|
# --offline Also bundle everything install.sh downloads (~520 MB:
|
||||||
# every apt package incl. the CUPS printing addon,
|
# every apt package incl. the CUPS, LMS/Squeezelite,
|
||||||
|
# Asterisk Intercom, VNC, WireGuard and hotspot addons,
|
||||||
# Node.js, prebuilt Electron/Web UI node_modules), so
|
# Node.js, prebuilt Electron/Web UI node_modules), so
|
||||||
# first boot can install the kiosk with NO internet.
|
# first boot can install the kiosk with NO internet.
|
||||||
# With internet it still installs online as usual;
|
# With internet it still installs online as usual;
|
||||||
|
|||||||
@@ -8,17 +8,20 @@
|
|||||||
#
|
#
|
||||||
# OUT_DIR ends up as /opt/kiosk-offline on the installed system:
|
# OUT_DIR ends up as /opt/kiosk-offline on the installed system:
|
||||||
# apt/ Local apt repository (flat, [trusted=yes]) with every
|
# apt/ Local apt repository (flat, [trusted=yes]) with every
|
||||||
# package install.sh's provisioning and the CUPS addon
|
# package install.sh's provisioning and the addons ask
|
||||||
# install ask for, NodeSource's nodejs, and all their
|
# for (CUPS, LMS + Squeezelite, Asterisk Intercom, VNC,
|
||||||
|
# WireGuard, Emergency Hotspot - not Tailscale/Netbird,
|
||||||
|
# which need their vendors' servers anyway), Lyrion's
|
||||||
|
# own .deb, NodeSource's nodejs, and all their
|
||||||
# dependencies that a fresh Ubuntu Server install from
|
# dependencies that a fresh Ubuntu Server install from
|
||||||
# SOURCE_ISO doesn't already have.
|
# SOURCE_ISO doesn't already have.
|
||||||
# npm/kiosk-app-node_modules.tar.gz Prebuilt node_modules (Electron
|
# npm/kiosk-app-node_modules.tar.gz Prebuilt node_modules (Electron
|
||||||
# npm/webui-node_modules.tar.gz included) in place of `npm install`.
|
# npm/webui-node_modules.tar.gz included) in place of `npm install`.
|
||||||
# bundle-info.txt What was bundled, from where, when.
|
# bundle-info.txt What was bundled, from where, when.
|
||||||
#
|
#
|
||||||
# Package lists are read from REPO_DIR's own lib/provision.sh and
|
# Package lists are read from REPO_DIR's own lib/provision.sh and menus/
|
||||||
# menus/addon_cups.sh (PROVISION_APT_PACKAGES, PROVISION_INTEL_APT_PACKAGES,
|
# (PROVISION_APT_PACKAGES, CUPS_APT_PACKAGES, SQUEEZELITE_APT_PACKAGES,
|
||||||
# CUPS_APT_PACKAGES), so the bundle always matches what install.sh asks for.
|
# LMS_DEB_URL, ...), so the bundle always matches what install.sh asks for.
|
||||||
#
|
#
|
||||||
# Dependencies are resolved against SOURCE_ISO's own package manifest - a
|
# Dependencies are resolved against SOURCE_ISO's own package manifest - a
|
||||||
# fake dpkg status built from it (fake-dpkg-status.py) - so apt only
|
# fake dpkg status built from it (fake-dpkg-status.py) - so apt only
|
||||||
@@ -127,21 +130,45 @@ python3 "$HERE/fake-dpkg-status.py" "$manifest" "$R/var/lib/apt/lists" > "$R/var
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
# shellcheck disable=SC1091,SC2034
|
# shellcheck disable=SC1091,SC2034
|
||||||
|
# Each list lives next to the code that installs it; lib/config.sh first
|
||||||
|
# for the path defaults those files use at the top level.
|
||||||
mapfile -t packages < <(
|
mapfile -t packages < <(
|
||||||
SCRIPT_DIR="$REPO_DIR"
|
SCRIPT_DIR="$REPO_DIR"
|
||||||
|
source "$REPO_DIR/lib/config.sh"
|
||||||
source "$REPO_DIR/lib/provision.sh"
|
source "$REPO_DIR/lib/provision.sh"
|
||||||
source "$REPO_DIR/menus/addon_cups.sh"
|
source "$REPO_DIR/menus/addon_cups.sh"
|
||||||
|
source "$REPO_DIR/menus/addon_lms_squeezelite.sh"
|
||||||
|
source "$REPO_DIR/menus/addon_asterisk_intercom.sh"
|
||||||
|
source "$REPO_DIR/menus/addon_remote_access.sh"
|
||||||
|
source "$REPO_DIR/menus/advanced_emergency_hotspot.sh"
|
||||||
printf '%s\n' "${PROVISION_APT_PACKAGES[@]}" "${PROVISION_INTEL_APT_PACKAGES[@]}" \
|
printf '%s\n' "${PROVISION_APT_PACKAGES[@]}" "${PROVISION_INTEL_APT_PACKAGES[@]}" \
|
||||||
"${CUPS_APT_PACKAGES[@]}" "${EXTRA_APT_PACKAGES[@]}"
|
"${CUPS_APT_PACKAGES[@]}" "${SQUEEZELITE_APT_PACKAGES[@]}" \
|
||||||
|
"${ASTERISK_INTERCOM_APT_PACKAGES[@]}" "${VNC_APT_PACKAGES[@]}" \
|
||||||
|
"${WIREGUARD_APT_PACKAGES[@]}" "${EMERGENCY_HOTSPOT_APT_PACKAGES[@]}" \
|
||||||
|
"${EXTRA_APT_PACKAGES[@]}"
|
||||||
)
|
)
|
||||||
(( ${#packages[@]} > 20 )) || die "couldn't read the package lists from $REPO_DIR"
|
(( ${#packages[@]} > 20 )) || die "couldn't read the package lists from $REPO_DIR"
|
||||||
|
|
||||||
|
# Lyrion Music Server isn't in Ubuntu's archive: bundle the exact .deb the
|
||||||
|
# LMS addon downloads online, and let apt resolve its dependencies from it
|
||||||
|
# (a local .deb path in the install list).
|
||||||
|
# shellcheck disable=SC1091,SC2034
|
||||||
|
lms_url="$(SCRIPT_DIR="$REPO_DIR"; source "$REPO_DIR/lib/config.sh"; source "$REPO_DIR/menus/addon_lms_squeezelite.sh"; echo "$LMS_DEB_URL")"
|
||||||
|
lms_deb="$CACHE_DIR/$(basename "$lms_url")"
|
||||||
|
if [[ ! -s "$lms_deb" ]]; then
|
||||||
|
info "Downloading $(basename "$lms_url")..."
|
||||||
|
curl -fL --retry 3 -o "$lms_deb.part" "$lms_url" || die "couldn't download $lms_url"
|
||||||
|
mv "$lms_deb.part" "$lms_deb"
|
||||||
|
fi
|
||||||
|
dpkg-deb -I "$lms_deb" >/dev/null 2>&1 || { rm -f "$lms_deb"; die "$lms_deb isn't a valid .deb - deleted, re-run to retry"; }
|
||||||
|
packages+=("$lms_deb")
|
||||||
|
|
||||||
info "Resolving ${#packages[@]} packages (with recommends, as install.sh installs them)..."
|
info "Resolving ${#packages[@]} packages (with recommends, as install.sh installs them)..."
|
||||||
# --print-uris against an empty archive dir: it leaves out anything already
|
# --print-uris against an empty archive dir: it leaves out anything already
|
||||||
# in the cache, which on a rebuild would be everything.
|
# in the cache, which on a rebuild would be everything.
|
||||||
rm -rf "$R/empty-archives"; mkdir -p "$R/empty-archives/partial"
|
rm -rf "$R/empty-archives"; mkdir -p "$R/empty-archives/partial"
|
||||||
mapfile -t debs < <("${APT[@]}" -qq -o "Dir::Cache::archives=$R/empty-archives" \
|
mapfile -t debs < <("${APT[@]}" -qq -o "Dir::Cache::archives=$R/empty-archives" \
|
||||||
install --print-uris -y "${packages[@]}" | awk '{print $2}')
|
install --print-uris -y "${packages[@]}" | awk '$1 !~ /^.file:/ {print $2}') # the local LMS .deb is copied below
|
||||||
(( ${#debs[@]} )) || die "apt resolved nothing to download - check the output above"
|
(( ${#debs[@]} )) || die "apt resolved nothing to download - check the output above"
|
||||||
|
|
||||||
info "Downloading ${#debs[@]} .debs (cached between builds)..."
|
info "Downloading ${#debs[@]} .debs (cached between builds)..."
|
||||||
@@ -150,6 +177,7 @@ info "Downloading ${#debs[@]} .debs (cached between builds)..."
|
|||||||
for f in "${debs[@]}"; do
|
for f in "${debs[@]}"; do
|
||||||
cp "$R/var/cache/apt/archives/$f" "$OUT_DIR/apt/"
|
cp "$R/var/cache/apt/archives/$f" "$OUT_DIR/apt/"
|
||||||
done
|
done
|
||||||
|
cp "$lms_deb" "$OUT_DIR/apt/"
|
||||||
(
|
(
|
||||||
cd "$OUT_DIR/apt"
|
cd "$OUT_DIR/apt"
|
||||||
apt-ftparchive packages . > Packages 2>/dev/null
|
apt-ftparchive packages . > Packages 2>/dev/null
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
: "${CRON_D_DIR:=/etc/cron.d}"
|
: "${CRON_D_DIR:=/etc/cron.d}"
|
||||||
: "${BIN_DIR:=/usr/local/bin}"
|
: "${BIN_DIR:=/usr/local/bin}"
|
||||||
: "${NETPLAN_DIR:=/etc/netplan}"
|
: "${NETPLAN_DIR:=/etc/netplan}"
|
||||||
|
: "${TIMESYNCD_CONF_DIR:=/etc/systemd/timesyncd.conf.d}"
|
||||||
: "${POLKIT_DIR:=/etc/polkit-1/localauthority/50-local.d}"
|
: "${POLKIT_DIR:=/etc/polkit-1/localauthority/50-local.d}"
|
||||||
: "${WIREGUARD_DIR:=/etc/wireguard}"
|
: "${WIREGUARD_DIR:=/etc/wireguard}"
|
||||||
: "${WEBUI_DIR:=/opt/kiosk-webui}"
|
: "${WEBUI_DIR:=/opt/kiosk-webui}"
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
BARESIP_CONFIG_DIR="${KIOSK_HOME}/.baresip"
|
BARESIP_CONFIG_DIR="${KIOSK_HOME}/.baresip"
|
||||||
|
# Also read by iso/offline/build-bundle.sh to bundle them for offline installs.
|
||||||
|
# pulseaudio-utils/pipewire-pulse: pactl, for audio routing.
|
||||||
|
ASTERISK_INTERCOM_APT_PACKAGES=(baresip pulseaudio-utils pipewire-pulse)
|
||||||
BARESIP_USER_SERVICE_DIR="${KIOSK_HOME}/.config/systemd/user"
|
BARESIP_USER_SERVICE_DIR="${KIOSK_HOME}/.config/systemd/user"
|
||||||
|
|
||||||
# Runs `systemctl --user ...` as $KIOSK_USER with the runtime dir/D-Bus
|
# Runs `systemctl --user ...` as $KIOSK_USER with the runtime dir/D-Bus
|
||||||
@@ -169,14 +172,13 @@ action_configure_asterisk_intercom() {
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Installing Baresip..."
|
echo "Installing Baresip..."
|
||||||
if ! command -v baresip &>/dev/null; then
|
if ! command -v baresip &>/dev/null || ! command -v pactl &>/dev/null; then
|
||||||
if ! sudo apt install -y baresip; then
|
if ! run_with_offline_fallback sudo apt install -y "${ASTERISK_INTERCOM_APT_PACKAGES[@]}"; then
|
||||||
log_error "Failed to install baresip package"
|
log_error "Failed to install baresip package"
|
||||||
pause
|
pause
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
sudo apt install -y pulseaudio-utils pipewire-pulse 2>/dev/null || true
|
|
||||||
|
|
||||||
sudo mkdir -p "$BARESIP_CONFIG_DIR"
|
sudo mkdir -p "$BARESIP_CONFIG_DIR"
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,23 @@
|
|||||||
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
|
# Also read by iso/offline/build-bundle.sh, to bundle the same versions
|
||||||
|
# for offline installs (the .deb itself plus its apt dependencies).
|
||||||
|
LMS_DEB_URL="https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver_9.0.3_amd64.deb"
|
||||||
|
SQUEEZELITE_APT_PACKAGES=(squeezelite)
|
||||||
|
|
||||||
|
# True on a kiosk installed from an --offline ISO that can't reach the
|
||||||
|
# internet right now - the one case where LMS comes from the bundle.
|
||||||
|
lms_offline_install_wanted() {
|
||||||
|
grep -q '^Package: lyrionmusicserver$' "$KIOSK_OFFLINE_DIR/apt/Packages" 2>/dev/null && ! have_internet
|
||||||
|
}
|
||||||
|
|
||||||
lms_service_name() {
|
lms_service_name() {
|
||||||
if systemctl list-unit-files 2>/dev/null | grep -q "lyrionmusicserver.service"; then
|
# `systemctl cat`, not `list-unit-files | grep -q`: grep -q exiting
|
||||||
|
# early can SIGPIPE systemctl, failing the pipeline under pipefail.
|
||||||
|
if systemctl cat lyrionmusicserver.service &>/dev/null; then
|
||||||
echo "lyrionmusicserver"
|
echo "lyrionmusicserver"
|
||||||
elif systemctl list-unit-files 2>/dev/null | grep -q "logitechmediaserver.service"; then
|
elif systemctl cat logitechmediaserver.service &>/dev/null; then
|
||||||
echo "logitechmediaserver"
|
echo "logitechmediaserver"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -114,42 +127,53 @@ action_install_lms() {
|
|||||||
|
|
||||||
echo "Installing Lyrion Music Server..."
|
echo "Installing Lyrion Music Server..."
|
||||||
|
|
||||||
# Try the repository method first.
|
if lms_offline_install_wanted; then
|
||||||
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
|
# Kiosk installed from an --offline ISO with no internet right now:
|
||||||
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
|
# the bundle carries the same LMS_DEB_URL .deb as a package.
|
||||||
# `|| true`: a bare, unguarded `apt update` failing here (bad
|
if ! run_with_offline_fallback sudo apt install -y lyrionmusicserver; then
|
||||||
# mirror, no network) would otherwise crash the whole session
|
log_error "Failed to install LMS from the offline bundle"
|
||||||
# under set -e instead of falling through to the direct-download
|
pause
|
||||||
# fallback below, which is exactly the degrade path this is
|
return 1
|
||||||
# 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
|
||||||
fi
|
log_success "LMS installed from the offline bundle"
|
||||||
|
else
|
||||||
# Fall back to a direct .deb download if the repository didn't produce
|
# Try the repository method first.
|
||||||
# either possible package.
|
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
|
||||||
if ! command -v logitechmediaserver &>/dev/null && ! command -v lyrionmusicserver &>/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
|
||||||
local lms_deb="/tmp/lms.deb"
|
# `|| true`: a bare, unguarded `apt update` failing here (bad
|
||||||
echo "Downloading LMS v9.0.3..."
|
# mirror, no network) would otherwise crash the whole session
|
||||||
if wget -q https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver_9.0.3_amd64.deb -O "$lms_deb"; then
|
# under set -e instead of falling through to the direct-download
|
||||||
echo "Installing LMS package..."
|
# fallback below, which is exactly the degrade path this is
|
||||||
if sudo apt install -y "$lms_deb"; then
|
# supposed to hit when the repository route doesn't work.
|
||||||
log_success "LMS installed via direct download"
|
sudo apt update 2>/dev/null || true
|
||||||
|
if sudo apt install -y logitechmediaserver 2>/dev/null; then
|
||||||
|
log_success "LMS installed via repository"
|
||||||
else
|
else
|
||||||
log_error "Failed to install LMS package"
|
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 ($(basename "$LMS_DEB_URL"))..."
|
||||||
|
if wget -q "$LMS_DEB_URL" -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"
|
rm -f "$lms_deb"
|
||||||
|
else
|
||||||
|
log_error "Failed to download LMS from lms-community.org"
|
||||||
pause
|
pause
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
rm -f "$lms_deb"
|
|
||||||
else
|
|
||||||
log_error "Failed to download LMS from lms-community.org"
|
|
||||||
pause
|
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -250,7 +274,7 @@ action_install_squeezelite() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v squeezelite &>/dev/null; then
|
if ! command -v squeezelite &>/dev/null; then
|
||||||
if ! sudo apt install -y squeezelite; then
|
if ! run_with_offline_fallback sudo apt install -y "${SQUEEZELITE_APT_PACKAGES[@]}"; then
|
||||||
log_error "squeezelite package installation failed"
|
log_error "squeezelite package installation failed"
|
||||||
pause
|
pause
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
|
# Also read by iso/offline/build-bundle.sh to bundle them for offline
|
||||||
|
# installs. (Tailscale/Netbird aren't bundled: they install from their
|
||||||
|
# vendors' scripts and need their vendors' servers to work anyway.)
|
||||||
|
VNC_APT_PACKAGES=(x11vnc)
|
||||||
|
WIREGUARD_APT_PACKAGES=(wireguard wireguard-tools)
|
||||||
|
|
||||||
remote_access_status() {
|
remote_access_status() {
|
||||||
echo "VNC: $(is_service_active x11vnc && echo "running" || echo "not installed")"
|
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 "WireGuard: $(wireguard_connected && echo "connected" || (command -v wg &>/dev/null && echo "installed, not connected" || echo "not installed"))"
|
||||||
@@ -70,7 +76,7 @@ action_vnc_install() {
|
|||||||
echo
|
echo
|
||||||
ask_yes_no "Install x11vnc?" "n" || { echo "Cancelled"; return; }
|
ask_yes_no "Install x11vnc?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
if ! sudo apt install -y x11vnc; then
|
if ! run_with_offline_fallback sudo apt install -y "${VNC_APT_PACKAGES[@]}"; then
|
||||||
log_error "x11vnc installation failed"
|
log_error "x11vnc installation failed"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -182,7 +188,7 @@ action_wireguard_install() {
|
|||||||
echo
|
echo
|
||||||
ask_yes_no "Install WireGuard?" "n" || { echo "Cancelled"; return; }
|
ask_yes_no "Install WireGuard?" "n" || { echo "Cancelled"; return; }
|
||||||
|
|
||||||
if ! sudo apt install -y wireguard wireguard-tools; then
|
if ! run_with_offline_fallback sudo apt install -y "${WIREGUARD_APT_PACKAGES[@]}"; then
|
||||||
log_error "WireGuard installation failed"
|
log_error "WireGuard installation failed"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ source "$SCRIPT_DIR/lib/electron.sh"
|
|||||||
source "$SCRIPT_DIR/menus/sites.sh"
|
source "$SCRIPT_DIR/menus/sites.sh"
|
||||||
source "$SCRIPT_DIR/menus/display.sh"
|
source "$SCRIPT_DIR/menus/display.sh"
|
||||||
source "$SCRIPT_DIR/menus/timezone.sh"
|
source "$SCRIPT_DIR/menus/timezone.sh"
|
||||||
|
source "$SCRIPT_DIR/menus/time_sync.sh"
|
||||||
source "$SCRIPT_DIR/menus/hidden_pin.sh"
|
source "$SCRIPT_DIR/menus/hidden_pin.sh"
|
||||||
source "$SCRIPT_DIR/menus/lockout.sh"
|
source "$SCRIPT_DIR/menus/lockout.sh"
|
||||||
source "$SCRIPT_DIR/menus/wifi.sh"
|
source "$SCRIPT_DIR/menus/wifi.sh"
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
################################################################################
|
################################################################################
|
||||||
# menus/advanced_emergency_hotspot.sh - "Emergency Hotspot" (Advanced):
|
# menus/advanced_emergency_hotspot.sh - "Emergency Hotspot" (Advanced):
|
||||||
# auto-starts a WiFi hotspot if no internet is detected 60 seconds after
|
# auto-starts a WiFi hotspot if the kiosk finds itself cut off 60 seconds
|
||||||
# boot, so the kiosk can be reached and reconfigured remotely.
|
# 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)
|
# Writes a standalone runtime script ($BIN_DIR/kiosk-emergency-hotspot)
|
||||||
# plus a oneshot systemd unit ($SYSTEMD_DIR) that runs it at boot - both
|
# plus a oneshot systemd unit ($SYSTEMD_DIR) that runs it at boot - both
|
||||||
@@ -21,6 +28,8 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
EMERGENCY_HOTSPOT_SCRIPT="$BIN_DIR/kiosk-emergency-hotspot"
|
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() {
|
emergency_hotspot_is_configured() {
|
||||||
[[ -f "$EMERGENCY_HOTSPOT_SCRIPT" ]]
|
[[ -f "$EMERGENCY_HOTSPOT_SCRIPT" ]]
|
||||||
@@ -38,8 +47,8 @@ advanced_emergency_hotspot_status() {
|
|||||||
else
|
else
|
||||||
echo "Emergency Hotspot: Not configured"
|
echo "Emergency Hotspot: Not configured"
|
||||||
fi
|
fi
|
||||||
echo "ℹ Auto-starts a WiFi hotspot if no internet is detected 60"
|
echo "ℹ Auto-starts a WiFi hotspot if the kiosk can't reach its network"
|
||||||
echo " seconds after boot, so you can connect and reconfigure remotely."
|
echo " 60 seconds after boot, so you can connect and reconfigure remotely."
|
||||||
}
|
}
|
||||||
|
|
||||||
advanced_emergency_hotspot_menu_builder() {
|
advanced_emergency_hotspot_menu_builder() {
|
||||||
@@ -62,7 +71,7 @@ advanced_emergency_hotspot_menu() {
|
|||||||
|
|
||||||
action_configure_emergency_hotspot() {
|
action_configure_emergency_hotspot() {
|
||||||
echo
|
echo
|
||||||
if ! sudo apt install -y hostapd dnsmasq iptables; then
|
if ! run_with_offline_fallback sudo apt install -y "${EMERGENCY_HOTSPOT_APT_PACKAGES[@]}"; then
|
||||||
log_error "Failed to install hostapd/dnsmasq/iptables"
|
log_error "Failed to install hostapd/dnsmasq/iptables"
|
||||||
pause
|
pause
|
||||||
return 1
|
return 1
|
||||||
@@ -91,6 +100,29 @@ action_configure_emergency_hotspot() {
|
|||||||
[[ ${#hotspot_pass} -lt 8 ]] && log_error "Password must be at least 8 characters"
|
[[ ${#hotspot_pass} -lt 8 ]] && log_error "Password must be at least 8 characters"
|
||||||
done
|
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"
|
local hotspot_ip="192.168.50.1"
|
||||||
|
|
||||||
sudo mkdir -p "$BIN_DIR"
|
sudo mkdir -p "$BIN_DIR"
|
||||||
@@ -98,9 +130,14 @@ action_configure_emergency_hotspot() {
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
################################################################################
|
################################################################################
|
||||||
### KIOSK EMERGENCY HOTSPOT
|
### KIOSK EMERGENCY HOTSPOT
|
||||||
### Auto-starts if no internet connection 60 seconds after boot
|
### 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"
|
WIFI_IFACE="$wifi_iface"
|
||||||
HOTSPOT_SSID="$hotspot_ssid"
|
HOTSPOT_SSID="$hotspot_ssid"
|
||||||
HOTSPOT_PASS="$hotspot_pass"
|
HOTSPOT_PASS="$hotspot_pass"
|
||||||
@@ -110,13 +147,36 @@ KIOSK_USER="$KIOSK_USER"
|
|||||||
# Wait 60 seconds after boot
|
# Wait 60 seconds after boot
|
||||||
sleep 60
|
sleep 60
|
||||||
|
|
||||||
# Check for internet connectivity
|
ping_ok() { ping -c 2 -W 3 "\$1" >/dev/null 2>&1; }
|
||||||
if ping -c 3 -W 5 8.8.8.8 >/dev/null 2>&1; then
|
internet_ok() { ping_ok 8.8.8.8 || ping_ok 1.1.1.1; }
|
||||||
logger "KIOSK: Internet connected - emergency hotspot not needed"
|
# The router counts as there if it answers ping - or, for routers set to
|
||||||
exit 0
|
# ignore ping, if it answered the ARP lookup that ping just triggered.
|
||||||
fi
|
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
|
||||||
|
}
|
||||||
|
|
||||||
logger "KIOSK: No internet detected - starting emergency hotspot"
|
# 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
|
# Stop any conflicting services
|
||||||
systemctl stop wpa_supplicant 2>/dev/null || true
|
systemctl stop wpa_supplicant 2>/dev/null || true
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# tailscale_do_uninstall, netbird_do_uninstall, lms_do_uninstall,
|
# tailscale_do_uninstall, netbird_do_uninstall, lms_do_uninstall,
|
||||||
# squeezelite_do_uninstall, asterisk_intercom_do_uninstall,
|
# squeezelite_do_uninstall, asterisk_intercom_do_uninstall,
|
||||||
# webui_do_uninstall, power_schedule_do_remove_all,
|
# webui_do_uninstall, power_schedule_do_remove_all,
|
||||||
# emergency_hotspot_do_disable) instead of
|
# emergency_hotspot_do_disable, time_sync_do_reset) instead of
|
||||||
# re-implementing removal logic for each addon a second time here - if an
|
# re-implementing removal logic for each addon a second time here - if an
|
||||||
# addon's uninstall logic changes, this picks it up automatically. Only
|
# addon's uninstall logic changes, this picks it up automatically. Only
|
||||||
# the pieces no single addon owns - the kiosk user/files, Node.js/
|
# the pieces no single addon owns - the kiosk user/files, Node.js/
|
||||||
@@ -62,6 +62,7 @@ action_complete_uninstall() {
|
|||||||
echo " • LightDM and Openbox"
|
echo " • LightDM and Openbox"
|
||||||
echo " • All kiosk schedules and services"
|
echo " • All kiosk schedules and services"
|
||||||
echo " • Emergency hotspot configuration"
|
echo " • Emergency hotspot configuration"
|
||||||
|
echo " • LAN time server setting (back to Ubuntu's defaults)"
|
||||||
echo
|
echo
|
||||||
echo "⚠️ This CANNOT be undone!"
|
echo "⚠️ This CANNOT be undone!"
|
||||||
echo
|
echo
|
||||||
@@ -90,9 +91,10 @@ action_complete_uninstall() {
|
|||||||
asterisk_intercom_do_uninstall purge
|
asterisk_intercom_do_uninstall purge
|
||||||
webui_do_uninstall
|
webui_do_uninstall
|
||||||
|
|
||||||
echo "[3/12] Removing schedules and emergency hotspot..."
|
echo "[3/12] Removing schedules, emergency hotspot and time server setting..."
|
||||||
power_schedule_do_remove_all
|
power_schedule_do_remove_all
|
||||||
emergency_hotspot_do_disable
|
emergency_hotspot_do_disable
|
||||||
|
time_sync_do_reset
|
||||||
|
|
||||||
# Must come after every addon teardown above - Asterisk Intercom's
|
# Must come after every addon teardown above - Asterisk Intercom's
|
||||||
# helper still needs this user to resolve its systemd --user session.
|
# helper still needs this user to resolve its systemd --user session.
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# menus/time_sync.sh - "Time Server (NTP)" (Core Settings).
|
||||||
|
#
|
||||||
|
# The kiosk keeps its clock right with systemd-timesyncd, which by default
|
||||||
|
# asks Ubuntu's time servers on the internet. On a network with no
|
||||||
|
# internet that never works and the clock slowly drifts - which matters
|
||||||
|
# here, because lockout times, quiet hours and power schedules all go by
|
||||||
|
# it. This menu points timesyncd at a time server on the local network
|
||||||
|
# instead (most routers, Windows domain controllers and NAS boxes can
|
||||||
|
# serve NTP), or, with no time server at all, sets the clock by hand.
|
||||||
|
#
|
||||||
|
# Writes one drop-in, $TIMESYNCD_CONF_DIR/50-kiosk-ntp.conf - removing it
|
||||||
|
# (the "internet time servers" option, or Complete Uninstall via
|
||||||
|
# time_sync_do_reset) puts Ubuntu's defaults back untouched.
|
||||||
|
#
|
||||||
|
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
TIMESYNC_DROPIN="$TIMESYNCD_CONF_DIR/50-kiosk-ntp.conf"
|
||||||
|
|
||||||
|
# The time server(s) this menu configured, or empty for Ubuntu's defaults.
|
||||||
|
time_sync_servers() {
|
||||||
|
sed -n 's/^NTP=//p' "$TIMESYNC_DROPIN" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
time_sync_status() {
|
||||||
|
local servers synced
|
||||||
|
servers=$(time_sync_servers)
|
||||||
|
synced=$(timedatectl show -p NTPSynchronized --value 2>/dev/null || echo "unknown")
|
||||||
|
echo "Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
echo "Time servers: ${servers:-Ubuntu defaults (internet)}"
|
||||||
|
echo "Synchronized: $([[ "$synced" == "yes" ]] && echo "yes" || echo "no")"
|
||||||
|
}
|
||||||
|
|
||||||
|
time_sync_menu_builder() {
|
||||||
|
MENU_LABELS=(
|
||||||
|
"Use a time server on my network"
|
||||||
|
"Use the internet time servers (Ubuntu default)"
|
||||||
|
"Set the date and time manually"
|
||||||
|
"Show sync details"
|
||||||
|
)
|
||||||
|
MENU_HANDLERS=(
|
||||||
|
action_time_sync_lan
|
||||||
|
action_time_sync_default
|
||||||
|
action_time_sync_manual
|
||||||
|
action_time_sync_details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
time_sync_menu() {
|
||||||
|
run_menu "TIME SERVER (NTP)" time_sync_menu_builder time_sync_status
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate a space-separated list of hostnames / IPv4 / IPv6 addresses.
|
||||||
|
time_sync_valid_servers() {
|
||||||
|
local s
|
||||||
|
[[ -n "$1" ]] || return 1
|
||||||
|
for s in $1; do
|
||||||
|
[[ "$s" =~ ^[A-Za-z0-9.:-]+$ ]] || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
time_sync_apply() {
|
||||||
|
sudo systemctl enable systemd-timesyncd 2>/dev/null || true
|
||||||
|
sudo timedatectl set-ntp true 2>/dev/null || true
|
||||||
|
sudo systemctl restart systemd-timesyncd 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove this menu's drop-in, back to Ubuntu's default time servers.
|
||||||
|
# Also used by Complete Uninstall.
|
||||||
|
time_sync_do_reset() {
|
||||||
|
sudo rm -f "$TIMESYNC_DROPIN"
|
||||||
|
time_sync_apply
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Actions
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
action_time_sync_lan() {
|
||||||
|
echo
|
||||||
|
echo "Enter the address of a time (NTP) server on your network - often"
|
||||||
|
echo "the router (e.g. 192.168.1.1), a Windows domain controller, or a"
|
||||||
|
echo "NAS. Several can be given, separated by spaces."
|
||||||
|
local current servers
|
||||||
|
current=$(time_sync_servers)
|
||||||
|
servers=$(ask_text "Time server(s)" "${current:-$(ip -4 route show default 2>/dev/null | awk '{print $3; exit}')}")
|
||||||
|
if ! time_sync_valid_servers "$servers"; then
|
||||||
|
log_error "Not a valid address list: '$servers'"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo mkdir -p "$TIMESYNCD_CONF_DIR"
|
||||||
|
sudo tee "$TIMESYNC_DROPIN" > /dev/null <<EOF
|
||||||
|
# Written by ubuntu-based-kiosk (Core Settings -> Time Server).
|
||||||
|
# Delete this file to go back to Ubuntu's default time servers.
|
||||||
|
[Time]
|
||||||
|
NTP=$servers
|
||||||
|
EOF
|
||||||
|
time_sync_apply
|
||||||
|
|
||||||
|
echo "Checking the time server (up to 15 seconds)..."
|
||||||
|
local i
|
||||||
|
for i in {1..15}; do
|
||||||
|
if [[ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null)" == "yes" ]]; then
|
||||||
|
log_success "Clock synchronized with $servers"
|
||||||
|
pause
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
log_warning "Saved, but no sync yet. timesyncd keeps retrying in the background."
|
||||||
|
echo "If it never syncs, check the address and that the server answers"
|
||||||
|
echo "NTP (UDP port 123): Core Settings -> Time Server -> Show sync details."
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_time_sync_default() {
|
||||||
|
echo
|
||||||
|
if [[ ! -f "$TIMESYNC_DROPIN" ]]; then
|
||||||
|
echo "Already using Ubuntu's default (internet) time servers."
|
||||||
|
pause
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
time_sync_do_reset
|
||||||
|
log_success "Back on Ubuntu's default (internet) time servers"
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
# For a network with no time server at all: set the clock by hand.
|
||||||
|
# Automatic sync stays on afterwards - it just has nothing to sync with
|
||||||
|
# until a server becomes reachable, and then takes over again.
|
||||||
|
action_time_sync_manual() {
|
||||||
|
echo
|
||||||
|
echo "Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
echo "Enter the correct local date and time (timezone: $(timedatectl show -p Timezone --value 2>/dev/null))."
|
||||||
|
local when
|
||||||
|
when=$(ask_text "Date and time (YYYY-MM-DD HH:MM)" "$(date '+%Y-%m-%d %H:%M')")
|
||||||
|
if [[ ! "$when" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}(:[0-9]{2})?$ ]] || ! date -d "$when" &>/dev/null; then
|
||||||
|
log_error "Not a valid date/time: '$when' (expected e.g. 2026-10-03 14:30)"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[[ "$when" =~ :[0-9]{2}:[0-9]{2}$ ]] || when="$when:00"
|
||||||
|
|
||||||
|
# timedatectl refuses set-time while automatic sync is on.
|
||||||
|
sudo timedatectl set-ntp false 2>/dev/null || true
|
||||||
|
if sudo timedatectl set-time "$when"; then
|
||||||
|
# Keep the hardware clock in step so the time survives a reboot.
|
||||||
|
sudo hwclock --systohc 2>/dev/null || true
|
||||||
|
log_success "Clock set to $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||||
|
else
|
||||||
|
log_error "Couldn't set the clock"
|
||||||
|
fi
|
||||||
|
sudo timedatectl set-ntp true 2>/dev/null || true
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_time_sync_details() {
|
||||||
|
echo
|
||||||
|
timedatectl status 2>/dev/null || true
|
||||||
|
echo
|
||||||
|
timedatectl timesync-status 2>/dev/null || echo "(timesync-status not available)"
|
||||||
|
pause
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user