Offline CUPS from the normal menu, local-file sites, build-dep install
- CUPS (Addons -> CUPS Printing, from ./install.sh or the Web UI) now installs from the offline bundle by itself when the kiosk can't reach the Ubuntu archive: lib/config.sh's run_with_offline_fallback switches apt to the bundle (kiosk-offline-apt on) around the install and back afterwards. No-op on manual installs or when online. - Fix cups_is_installed reporting "not installed" for an installed CUPS: `dpkg -l | grep -q` under install.sh's pipefail fails when grep exits early and dpkg takes SIGPIPE. Uses dpkg-query on the one package now. - Sites: an absolute path (/home/kiosk/docs/menu.pdf) is accepted and turned into a percent-encoded file:// URL, identically in the Sites menu (sites_parse_url) and the Web UI (parseUrl, with tests). Electron 42 shows local HTML, PDFs (built-in viewer) and images as-is. - iso/build-iso.sh checks its build tools (plus the --offline ones) and offers to `sudo apt install` what's missing; --install-deps does it without asking. --offline now refuses non-amd64 hosts up front. Tested: offline chroot of the 26.04.1 server filesystem - action_install_cups installed CUPS + printer-driver-all/hplip/gutenprint from the bundle with no network and left apt back on normal sources; webui test suite passes; Electron 42 rendered a file:// PDF and PNG with spaces/# in the path; build-iso.sh stopped with the install command when apt-utils was missing and no TTY, and installed it and built the ISO with --install-deps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
@@ -69,7 +69,6 @@ boot of the installed system it runs `./install.sh` automatically on the
|
||||
console.
|
||||
|
||||
```bash
|
||||
sudo apt install xorriso curl git openssl
|
||||
git clone https://github.com/outis1one/ubuntu-based-kiosk.git
|
||||
cd ubuntu-based-kiosk
|
||||
iso/build-iso.sh # downloads + verifies the latest 26.04.x server ISO
|
||||
@@ -77,6 +76,10 @@ iso/build-iso.sh # downloads + verifies the latest 26.04.x server ISO
|
||||
sudo dd if=iso/build/ubuntu-26.04.*-kiosk-amd64.iso of=/dev/sdX bs=4M status=progress oflag=sync
|
||||
```
|
||||
|
||||
The script checks for its build tools (`xorriso`, `curl`, `git`, `openssl`)
|
||||
and offers to install any that are missing with `sudo apt install`. Pass
|
||||
`--install-deps` to install them without asking.
|
||||
|
||||
By default the installer only asks for **network** (WiFi works here),
|
||||
**disk**, and your **admin username/password** — language, keyboard and the
|
||||
SSH server are answered for you. On first boot, `install.sh` runs
|
||||
@@ -100,8 +103,7 @@ Useful options (`iso/build-iso.sh --help` for all):
|
||||
### Offline installs (`--offline`)
|
||||
|
||||
```bash
|
||||
sudo apt install apt-utils python3 gnupg ubuntu-keyring # once, on top of the tools above
|
||||
iso/build-iso.sh --offline
|
||||
iso/build-iso.sh --offline # also offers to install apt-utils, python3, gnupg, ubuntu-keyring
|
||||
# -> iso/build/ubuntu-26.04.x-kiosk-offline-amd64.iso (~3.2 GB)
|
||||
```
|
||||
|
||||
@@ -114,14 +116,10 @@ This adds a ~420 MB bundle to the ISO:
|
||||
|
||||
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
|
||||
stays on the kiosk at `/opt/kiosk-offline`, so CUPS can be added later
|
||||
with no internet too:
|
||||
|
||||
```bash
|
||||
sudo kiosk-offline-apt on # apt uses the bundle only
|
||||
./install.sh # Addons -> CUPS Printing
|
||||
sudo kiosk-offline-apt off # back to normal updates
|
||||
```
|
||||
stays on the kiosk at `/opt/kiosk-offline`, so **Addons → CUPS Printing**
|
||||
(in `./install.sh` or the Web UI) works with no internet too: when it can't
|
||||
reach the Ubuntu archive, it installs from the bundle by itself and then
|
||||
switches apt back to normal.
|
||||
|
||||
Notes:
|
||||
- Build it on an **amd64** Ubuntu/Debian machine, or let the GitHub
|
||||
@@ -195,6 +193,10 @@ The kiosk machine still needs a working internet connection (ethernet, or WiFi c
|
||||
- **Home URL** - Auto-return after inactivity on manual or hidden sites
|
||||
- **Pause functionality** - Temporarily pause rotation (configurable per-site)
|
||||
- **Navigation menu** - Quick access to all sites via key icon (top-left hot corner)
|
||||
- **Local files** - Enter an absolute path (e.g. `/home/kiosk/docs/menu.pdf`)
|
||||
instead of a web address and it's shown from the kiosk's own disk:
|
||||
HTML pages, PDFs (built-in viewer), and images. Works offline. The file
|
||||
must be readable by the `kiosk` user.
|
||||
|
||||
### Touch Controls
|
||||
- **2-finger horizontal swipe** - Switch between sites
|
||||
|
||||
+35
-5
@@ -24,7 +24,9 @@
|
||||
# (menu title, plus the `autoinstall` kernel argument with --no-confirm);
|
||||
# boot records (BIOS + UEFI) are replayed from the original as-is.
|
||||
#
|
||||
# Requirements (on the build machine, any recent Ubuntu/Debian):
|
||||
# Requirements (on the build machine, any recent Ubuntu/Debian): the
|
||||
# script checks for its build tools and offers to install whatever's
|
||||
# missing with apt (or pass --install-deps to just do it). By hand:
|
||||
# sudo apt install xorriso curl git openssl
|
||||
# plus, for --offline (amd64 build machine only):
|
||||
# sudo apt install apt-utils python3 gnupg ubuntu-keyring
|
||||
@@ -33,6 +35,7 @@
|
||||
# iso/build-iso.sh [options]
|
||||
#
|
||||
# --iso PATH Use this Ubuntu Server ISO instead of downloading.
|
||||
# --install-deps Install missing build tools with apt without asking.
|
||||
# --release VER Ubuntu release to download (default: 26.04 - picks
|
||||
# the newest 26.04.x live-server-amd64 point release).
|
||||
# --output PATH Output ISO (default: iso/build/ubuntu-<ver>-kiosk-amd64.iso)
|
||||
@@ -74,8 +77,8 @@
|
||||
# With internet it still installs online as usual;
|
||||
# offline it waits 90s, then uses the bundle. The
|
||||
# bundle stays on the kiosk at /opt/kiosk-offline
|
||||
# (see iso/firstboot/kiosk-offline-apt for using it
|
||||
# later, e.g. adding CUPS while offline).
|
||||
# - Addons -> CUPS Printing then installs from it too
|
||||
# whenever the kiosk is offline.
|
||||
#
|
||||
# The repo copied onto the ISO is a clone of this checkout's current HEAD
|
||||
# (committed changes only - uncommitted edits are NOT included).
|
||||
@@ -103,6 +106,7 @@ AUTO_NETWORK=0
|
||||
NO_CONFIRM=0
|
||||
UNATTENDED=0
|
||||
OFFLINE=0
|
||||
INSTALL_DEPS=0
|
||||
REPO_URL=""
|
||||
|
||||
# Usernames the Ubuntu installer rejects (system users/groups) - copied
|
||||
@@ -149,6 +153,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--fully-automatic) AUTO_STORAGE=1; AUTO_NETWORK=1; NO_CONFIRM=1; UNATTENDED=1; shift ;;
|
||||
--repo-url) REPO_URL="$2"; shift 2 ;;
|
||||
--offline) OFFLINE=1; shift ;;
|
||||
--install-deps) INSTALL_DEPS=1; shift ;;
|
||||
-h|--help) usage 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage 1 ;;
|
||||
esac
|
||||
@@ -158,9 +163,34 @@ done
|
||||
# Validate
|
||||
################################################################################
|
||||
|
||||
for cmd in xorriso curl git openssl sha256sum; do
|
||||
command -v "$cmd" &>/dev/null || die "'$cmd' not found - install it: sudo apt install xorriso curl git openssl"
|
||||
# Build tools: command -> the apt package that provides it.
|
||||
declare -A DEP_PKG=([xorriso]=xorriso [curl]=curl [git]=git [openssl]=openssl [sha256sum]=coreutils)
|
||||
if (( OFFLINE )); then
|
||||
DEP_PKG+=([apt-ftparchive]=apt-utils [python3]=python3 [gpg]=gnupg [dpkg-deb]=dpkg)
|
||||
fi
|
||||
missing_pkgs=()
|
||||
for cmd in "${!DEP_PKG[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_pkgs+=("${DEP_PKG[$cmd]}")
|
||||
done
|
||||
if (( OFFLINE )) && [[ ! -r /usr/share/keyrings/ubuntu-archive-keyring.gpg ]]; then
|
||||
missing_pkgs+=(ubuntu-keyring)
|
||||
fi
|
||||
if (( ${#missing_pkgs[@]} )); then
|
||||
mapfile -t missing_pkgs < <(printf '%s\n' "${missing_pkgs[@]}" | sort -u)
|
||||
echo "Missing build tools: ${missing_pkgs[*]}"
|
||||
command -v apt-get &>/dev/null \
|
||||
|| die "install them with your package manager and re-run (this script expects Ubuntu/Debian)"
|
||||
if (( ! INSTALL_DEPS )); then
|
||||
[[ -t 0 ]] || die "install them first: sudo apt install ${missing_pkgs[*]} (or re-run with --install-deps)"
|
||||
read -r -p "Install them now with 'sudo apt install'? [Y/n] " reply
|
||||
[[ "${reply,,}" =~ ^(|y|yes)$ ]] || die "install them first: sudo apt install ${missing_pkgs[*]}"
|
||||
fi
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y "${missing_pkgs[@]}" || die "couldn't install: ${missing_pkgs[*]}"
|
||||
fi
|
||||
if (( OFFLINE )) && [[ "$(dpkg --print-architecture 2>/dev/null)" != "amd64" ]]; then
|
||||
die "--offline must be built on an amd64 (64-bit Intel/AMD) machine - it runs the bundled Node.js to build the kiosk's modules"
|
||||
fi
|
||||
|
||||
if [[ -n "$ADMIN_USER" ]]; then
|
||||
[[ "$ADMIN_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "invalid username: $ADMIN_USER"
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
#
|
||||
# Installed by the custom ISO (iso/build-iso.sh); the bundle itself is
|
||||
# only there on ISOs built with --offline. kiosk-firstboot turns this on
|
||||
# by itself when first boot finds no internet, and back off afterwards.
|
||||
# By hand it's for installing things later with no internet - e.g. the
|
||||
# CUPS addon: `sudo kiosk-offline-apt on`, ./install.sh -> Addons -> CUPS,
|
||||
# then `sudo kiosk-offline-apt off`. While it's on, install.sh also
|
||||
# by itself when first boot finds no internet, and back off afterwards;
|
||||
# so does the CUPS addon install (lib/config.sh's
|
||||
# run_with_offline_fallback). By hand it's for installing anything else
|
||||
# from the bundle with no internet. While it's on, install.sh also
|
||||
# unpacks the bundle's prebuilt node_modules instead of running npm
|
||||
# (lib/config.sh's offline_mode_active checks for the file this writes).
|
||||
#
|
||||
|
||||
@@ -104,6 +104,36 @@ offline_install_node_modules() {
|
||||
sudo -u "$KIOSK_USER" tar -xzf "$tarball" -C "$dest"
|
||||
}
|
||||
|
||||
# Quick reachability check for the package mirrors install.sh needs.
|
||||
have_internet() {
|
||||
curl -fsS --max-time 8 -o /dev/null http://archive.ubuntu.com/ubuntu/ 2>/dev/null \
|
||||
|| curl -fsS --max-time 8 -o /dev/null https://deb.nodesource.com/ 2>/dev/null
|
||||
}
|
||||
|
||||
# run_with_offline_fallback CMD [ARGS...]
|
||||
# Runs CMD normally - but on a kiosk installed from an --offline ISO with
|
||||
# no internet right now, first points apt at the bundle on this disk
|
||||
# (kiosk-offline-apt on) and switches it back afterwards, whatever CMD
|
||||
# returns. Lets apt-based installs (e.g. the CUPS addon) work from the
|
||||
# normal menus and the Web UI with no extra steps. Everywhere else
|
||||
# (manual installs, or online) it just runs CMD.
|
||||
run_with_offline_fallback() {
|
||||
local restore=0 rc=0
|
||||
if ! offline_mode_active && [[ -f "$KIOSK_OFFLINE_DIR/apt/Packages" ]] && ! have_internet; then
|
||||
log_info "No internet - installing from the offline bundle on this disk ($(sed -n 's/^Built: *//p' "$KIOSK_OFFLINE_DIR/bundle-info.txt" 2>/dev/null))"
|
||||
if sudo kiosk-offline-apt on >/dev/null; then
|
||||
restore=1
|
||||
else
|
||||
log_warning "Couldn't switch apt to the offline bundle - trying normally"
|
||||
fi
|
||||
fi
|
||||
"$@" || rc=$?
|
||||
if (( restore )); then
|
||||
sudo kiosk-offline-apt off >/dev/null || true
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
kiosk_user_exists() {
|
||||
id "$KIOSK_USER" &>/dev/null
|
||||
}
|
||||
|
||||
+14
-6
@@ -22,7 +22,11 @@ CUPS_APT_PACKAGES=(
|
||||
)
|
||||
|
||||
cups_is_installed() {
|
||||
dpkg -l 2>/dev/null | grep -q "^ii\s\+cups\s"
|
||||
# Not `dpkg -l | grep -q`: grep -q exits at the first match, dpkg
|
||||
# takes SIGPIPE writing the rest of its long list, and under
|
||||
# install.sh's `set -o pipefail` that made this report "not
|
||||
# installed" for an installed CUPS.
|
||||
[[ "$(dpkg-query -W -f='${db:Status-Abbrev}' cups 2>/dev/null)" == "ii "* ]]
|
||||
}
|
||||
|
||||
cups_is_active() {
|
||||
@@ -60,11 +64,7 @@ addon_cups_menu() {
|
||||
# Actions
|
||||
################################################################################
|
||||
|
||||
action_install_cups() {
|
||||
echo
|
||||
ask_yes_no "Install CUPS printing?" "n" || { echo "Cancelled"; return; }
|
||||
|
||||
echo "Installing CUPS from scratch..."
|
||||
cups_apt_install() {
|
||||
if ! sudo apt update; then
|
||||
log_error "apt update failed - check network/package sources and try again"
|
||||
return 1
|
||||
@@ -73,6 +73,14 @@ action_install_cups() {
|
||||
log_error "CUPS package installation failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
action_install_cups() {
|
||||
echo
|
||||
ask_yes_no "Install CUPS printing?" "n" || { echo "Cancelled"; return; }
|
||||
|
||||
echo "Installing CUPS from scratch..."
|
||||
run_with_offline_fallback cups_apt_install || return 1
|
||||
|
||||
sudo systemctl enable cups 2>/dev/null || true
|
||||
sudo systemctl start cups 2>/dev/null || true
|
||||
|
||||
+14
-2
@@ -18,11 +18,23 @@ SITE_EDIT_IDX=""
|
||||
################################################################################
|
||||
|
||||
# Normalize whatever the user typed into a URL, same rules the old
|
||||
# installer used: bare host -> https://, bare IP -> http://.
|
||||
# installer used: bare host -> https://, bare IP -> http://. Plus local
|
||||
# files on the kiosk itself (pages, PDFs, images): an absolute path ->
|
||||
# file://, percent-encoded the same way webui/server.js's parseUrl does.
|
||||
sites_parse_url() {
|
||||
local raw="$1"
|
||||
if [[ "$raw" =~ ^https?:// ]]; then
|
||||
if [[ "$raw" =~ ^(https?|file):// ]]; then
|
||||
echo "$raw"
|
||||
elif [[ "$raw" == /* ]]; then
|
||||
local LC_ALL=C out="" c i
|
||||
for (( i = 0; i < ${#raw}; i++ )); do
|
||||
c="${raw:i:1}"
|
||||
case "$c" in
|
||||
[A-Za-z0-9._~/!\'\(\)*-]) out+="$c" ;;
|
||||
*) out+="$(printf '%%%02X' "'$c")" ;;
|
||||
esac
|
||||
done
|
||||
echo "file://$out"
|
||||
elif [[ "$raw" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "http://${raw}"
|
||||
else
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
<div class="site-row-grid">
|
||||
<label>
|
||||
URL
|
||||
<input type="text" class="site-url" placeholder="example.com or https://example.com">
|
||||
<input type="text" class="site-url" placeholder="example.com, https://example.com or /path/to/file.pdf">
|
||||
</label>
|
||||
<label>
|
||||
Name (optional)
|
||||
|
||||
+4
-2
@@ -35,9 +35,11 @@ const NAV_MODES = ['restricted', 'same-origin', 'open'];
|
||||
const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||
|
||||
// Same normalization rule as menus/sites.sh's sites_parse_url(): bare
|
||||
// host -> https://, bare IPv4 -> http://, else passed through as-is.
|
||||
// host -> https://, bare IPv4 -> http://, absolute path -> file:// (local
|
||||
// pages/PDFs/images on the kiosk itself), else passed through as-is.
|
||||
function parseUrl(raw) {
|
||||
if (/^https?:\/\//.test(raw)) return raw;
|
||||
if (/^(https?|file):\/\//.test(raw)) return raw;
|
||||
if (raw.startsWith('/')) return `file://${raw.split('/').map(encodeURIComponent).join('/')}`;
|
||||
if (/^\d+\.\d+\.\d+\.\d+/.test(raw)) return `http://${raw}`;
|
||||
return `https://${raw}`;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ async function main() {
|
||||
{ url: 'example.com', duration: 30, name: 'bare host' },
|
||||
{ url: '192.168.1.50', duration: 30, name: 'bare ip' },
|
||||
{ url: 'https://already.example.com', duration: 30, name: 'already a url' },
|
||||
{ url: '/home/kiosk/docs/menu #2.pdf', duration: 30, name: 'local path' },
|
||||
{ url: 'file:///srv/kiosk/index.html', duration: 30, name: 'already a file url' },
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -88,6 +90,8 @@ async function main() {
|
||||
assert.strictEqual(body.tabs[0].url, 'https://example.com');
|
||||
assert.strictEqual(body.tabs[1].url, 'http://192.168.1.50');
|
||||
assert.strictEqual(body.tabs[2].url, 'https://already.example.com');
|
||||
assert.strictEqual(body.tabs[3].url, 'file:///home/kiosk/docs/menu%20%232.pdf');
|
||||
assert.strictEqual(body.tabs[4].url, 'file:///srv/kiosk/index.html');
|
||||
});
|
||||
|
||||
await check('PUT rejects homeTabIndex out of range', async () => {
|
||||
|
||||
Reference in New Issue
Block a user