Cleanup repo + harden offline install and watchdog
Repo hygiene: - Remove dead scripts (.wait-for-display, .display-keepalive, .keep-screen-alive) - Remove stale backup/scratch (install.sh.bak, New.txt, stale .pyc) - Stop tracking machine-specific/generated state (config/app_config.json, playlists/server_playlist.json, src/player_auth.json, generated launchers) - Prune duplicate wheel versions (one per package) - Reorganize working_files -> dev/, dev/tests/, documentation/dev-notes/, scripts/ - Extend .gitignore; fix no-op rules in repo/.gitignore Offline install (install.sh): - Non-fatal system-dep step; per-package apt retry; .deb bundle install - Skip pip self-upgrade offline; guard online fallbacks behind connectivity probe - Dependency-free fetch_url (curl/wget/urllib); --no-reboot flag - Fix corrupted literal backslash-bang escapes (shebangs, test -f, banner) - Idempotent cron install (no more stacked @reboot lines) Autostart / watchdog: - start.sh: single-instance flock lock (.player_watchdog.lock) acquired only after a display is confirmed, preventing parallel watchdogs - Set XDG_RUNTIME_DIR when missing; exit early if headless - run_player.sh: activate .venv; stop_player.sh: clear lock file
This commit is contained in:
+163
-49
@@ -1,4 +1,4 @@
|
||||
#\!/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
# Kivy Signage Player Installation Script
|
||||
# Supports both online and offline installation using a Python virtual environment
|
||||
@@ -77,6 +77,48 @@ detect_raspberry_pi() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Helper: Internet reachability probe (dependency-free)
|
||||
# ============================================================
|
||||
# Returns 0 if the public internet (PyPI) is reachable, 1 otherwise.
|
||||
# Uses only the Python stdlib so it works on a bare, offline host.
|
||||
has_internet() {
|
||||
python3 - << 'PYEOF' >/dev/null 2>&1
|
||||
import socket
|
||||
try:
|
||||
socket.setdefaulttimeout(3)
|
||||
socket.create_connection(("pypi.org", 443), timeout=3).close()
|
||||
raise SystemExit(0)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
PYEOF
|
||||
return $?
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Helper: Fetch a URL using curl, wget, or the Python stdlib
|
||||
# ============================================================
|
||||
# Works on minimal/offline images where curl/wget may be absent.
|
||||
fetch_url() {
|
||||
local url="$1"
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -sSf --max-time 20 "$url"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -qO- --timeout=20 "$url"
|
||||
else
|
||||
python3 - "$url" << 'PYEOF'
|
||||
import sys, urllib.request
|
||||
url = sys.argv[1]
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=20) as r:
|
||||
sys.stdout.write(r.read().decode("utf-8", "replace"))
|
||||
except Exception as e:
|
||||
sys.stderr.write("fetch failed: %s\n" % e)
|
||||
raise SystemExit(1)
|
||||
PYEOF
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# CLI Argument Parsing
|
||||
# ============================================================
|
||||
@@ -92,6 +134,7 @@ MAX_RESOLUTION=""
|
||||
EDIT_ENABLED=""
|
||||
PROVISION_URL=""
|
||||
CONFIG_FILE_PATH=""
|
||||
NO_REBOOT=false
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
@@ -119,6 +162,7 @@ usage() {
|
||||
echo ""
|
||||
echo "Installation options:"
|
||||
echo " --offline Force offline installation from local packages"
|
||||
echo " --no-reboot Do not reboot automatically at the end"
|
||||
echo " -q, --quiet Silent mode (minimal output)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
@@ -186,6 +230,10 @@ while [ $# -gt 0 ]; do
|
||||
OFFLINE_MODE=true
|
||||
shift
|
||||
;;
|
||||
--no-reboot)
|
||||
NO_REBOOT=true
|
||||
shift
|
||||
;;
|
||||
-q|--quiet)
|
||||
exec > >(tee -a /tmp/kivy-install.log) 2>&1
|
||||
shift
|
||||
@@ -205,6 +253,12 @@ if [ "$OFFLINE_MODE" = false ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A "$WHEELS_DI
|
||||
OFFLINE_MODE=true
|
||||
fi
|
||||
|
||||
# Probe internet reachability once (guards every network step below)
|
||||
HAS_INTERNET=false
|
||||
if has_internet; then
|
||||
HAS_INTERNET=true
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Mode Banner
|
||||
# ============================================================
|
||||
@@ -216,6 +270,11 @@ if [ "$OFFLINE_MODE" = true ]; then
|
||||
else
|
||||
echo "Mode: Online Installation"
|
||||
fi
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
echo "Internet: reachable"
|
||||
else
|
||||
echo "Internet: NOT available (local-network / isolated install)"
|
||||
fi
|
||||
if [ -n "$SUDO_USERNAME" ]; then
|
||||
echo "Sudo user: $SUDO_USERNAME"
|
||||
fi
|
||||
@@ -228,27 +287,54 @@ echo "Step 1/6: Installing system dependencies..."
|
||||
echo "--------------------"
|
||||
|
||||
echo "Updating package lists..."
|
||||
run_sudo apt update
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
run_sudo apt update 2>/dev/null || echo " WARNING: 'apt update' failed; continuing with existing lists"
|
||||
else
|
||||
echo " Offline / no internet — skipping 'apt update'"
|
||||
run_sudo apt update 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Installing system packages..."
|
||||
|
||||
if [ "$OFFLINE_MODE" = true ] && [ -d "$DEB_DIR" ] && [ "$(ls -A $DEB_DIR/*.deb 2>/dev/null)" ]; then
|
||||
echo "Installing from offline .deb packages..."
|
||||
# System dependencies required for Kivy + video playback
|
||||
APT_PACKAGES="python3-pip python3-setuptools python3-dev python3-venv \
|
||||
libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
|
||||
libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev \
|
||||
zlib1g-dev ffmpeg libavcodec-extra \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
curl wget"
|
||||
|
||||
# Prefer the offline .deb bundle when present
|
||||
USED_DEB_BUNDLE=false
|
||||
if [ -d "$DEB_DIR" ] && ls "$DEB_DIR"/*.deb >/dev/null 2>&1; then
|
||||
echo "Installing from offline .deb packages in $DEB_DIR..."
|
||||
cd "$DEB_DIR"
|
||||
run_sudo dpkg -i *.deb 2>/dev/null || true
|
||||
run_sudo apt-get install -f -y || true
|
||||
# Resolve dependencies from the local bundle only (no-op if already satisfied)
|
||||
run_sudo apt-get install -f -y 2>/dev/null || true
|
||||
cd "$SCRIPT_DIR"
|
||||
USED_DEB_BUNDLE=true
|
||||
elif [ "$OFFLINE_MODE" = true ]; then
|
||||
echo "WARNING: Offline mode but no .deb bundle found in $DEB_DIR"
|
||||
echo " System packages will still be attempted via apt (may fail without internet)"
|
||||
fi
|
||||
|
||||
run_sudo apt install -y \
|
||||
python3-pip python3-setuptools python3-dev \
|
||||
python3-venv \
|
||||
libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
|
||||
libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev \
|
||||
zlib1g-dev ffmpeg libavcodec-extra \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good
|
||||
# Install/complete system packages. Never abort the installer:
|
||||
# offline, some packages may already be present (fine) or simply unavailable.
|
||||
if run_sudo apt install -y $APT_PACKAGES 2>/dev/null; then
|
||||
echo "System dependencies installed"
|
||||
else
|
||||
echo "WARNING: bulk system package install did not fully succeed."
|
||||
echo " Installing available packages individually..."
|
||||
for pkg in $APT_PACKAGES; do
|
||||
run_sudo apt install -y "$pkg" 2>/dev/null || echo " - unavailable (skipped): $pkg"
|
||||
done
|
||||
if [ "$HAS_INTERNET" != true ] && [ "$USED_DEB_BUNDLE" != true ]; then
|
||||
echo " NOTE: Host is offline and no .deb bundle was found."
|
||||
echo " Add .deb files to $DEB_DIR to satisfy missing system packages."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "System dependencies installed"
|
||||
echo ""
|
||||
|
||||
# ============================================================
|
||||
@@ -268,8 +354,12 @@ echo "Virtual environment created at $VENV_DIR"
|
||||
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
echo "Upgrading pip..."
|
||||
pip install --upgrade pip --quiet
|
||||
if [ "$OFFLINE_MODE" = true ] && [ "$HAS_INTERNET" != true ]; then
|
||||
echo "Skipping pip self-upgrade (offline, no internet)"
|
||||
else
|
||||
echo "Upgrading pip..."
|
||||
pip install --upgrade pip --quiet 2>/dev/null || echo " WARNING: pip upgrade failed; continuing"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -293,9 +383,13 @@ if [ -f "$REQUIREMENTS_FILE" ]; then
|
||||
else
|
||||
echo ""
|
||||
echo "Offline installation failed (some wheels may be missing or incompatible)"
|
||||
echo " Falling back to online installation (requires internet)..."
|
||||
echo ""
|
||||
pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
echo " Falling back to online installation (requires internet)..."
|
||||
pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true
|
||||
else
|
||||
echo " No internet available — cannot fall back to online installation."
|
||||
echo " Add the missing/compatible wheels to: $WHEELS_DIR"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
pip install -r "$REQUIREMENTS_FILE" --find-links="$WHEELS_DIR" 2>&1 || true
|
||||
@@ -306,16 +400,20 @@ if [ -f "$REQUIREMENTS_FILE" ]; then
|
||||
echo "Verifying installed packages..."
|
||||
pip list --format=columns 2>/dev/null | grep -iE "kivy|ffpyplayer" || true
|
||||
|
||||
# If kivy is still missing after pip install, try system-wide fallback
|
||||
python3 -c "import kivy" 2>/dev/null && echo " kivy OK" || {
|
||||
echo ""
|
||||
echo " kivy not found in venv — installing system-wide via pip (break-system-packages)..."
|
||||
pip install kivy --break-system-packages 2>/dev/null || pip install --user kivy 2>/dev/null || true
|
||||
}
|
||||
python3 -c "import ffpyplayer" 2>/dev/null && echo " ffpyplayer OK" || {
|
||||
echo " ffpyplayer not found — installing system-wide..."
|
||||
pip install ffpyplayer --break-system-packages 2>/dev/null || pip install --user ffpyplayer 2>/dev/null || true
|
||||
}
|
||||
# If a critical package is missing, fall back to online install only when reachable
|
||||
for pkg in kivy ffpyplayer; do
|
||||
if python3 -c "import $pkg" 2>/dev/null; then
|
||||
echo " $pkg OK"
|
||||
else
|
||||
echo " WARNING: $pkg could not be imported in the virtual environment"
|
||||
if [ "$HAS_INTERNET" = true ]; then
|
||||
echo " Attempting online fallback install for $pkg..."
|
||||
pip install "$pkg" 2>/dev/null || echo " - $pkg online install failed (continuing)"
|
||||
else
|
||||
echo " Offline: ensure a compatible '$pkg' wheel exists in $WHEELS_DIR"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "requirements.txt not found - skipping pip install"
|
||||
fi
|
||||
@@ -358,24 +456,32 @@ run_sudo chown "$ACTUAL_USER:$ACTUAL_USER" "$AUTOSTART_DIR/kivy-signage-player.d
|
||||
chmod 644 "$AUTOSTART_DIR/kivy-signage-player.desktop" 2>/dev/null || true
|
||||
echo "XDG autostart entry created"
|
||||
|
||||
# Cron fallback
|
||||
# Cron fallback (secondary to XDG autostart; start.sh enforces single-instance)
|
||||
if command -v crontab &>/dev/null; then
|
||||
CRON_WRAPPER="$SCRIPT_DIR/.start-player-cron.sh"
|
||||
CRON_MARKER_TAG=".start-player-cron.sh"
|
||||
cat > "$CRON_WRAPPER" << EOF
|
||||
#\!/bin/bash
|
||||
#!/bin/bash
|
||||
# Kiwy signage player cron fallback (managed by install.sh)
|
||||
sleep 15
|
||||
cd $SCRIPT_DIR && bash start.sh
|
||||
EOF
|
||||
chmod +x "$CRON_WRAPPER"
|
||||
|
||||
CRON_ENTRY="@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1"
|
||||
# Use sudo -u which works when running with sudo (no password prompt)
|
||||
if sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null | grep -q "kivy-signage-player"; then
|
||||
true
|
||||
else
|
||||
(sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null || true; echo "$CRON_ENTRY") | sudo -u "$ACTUAL_USER" crontab - 2>/dev/null || true
|
||||
fi
|
||||
echo "Cron fallback configured"
|
||||
|
||||
# Idempotent: strip ALL previous Kiwy cron lines, then add exactly one.
|
||||
# (The old guard grepped for a string that never appeared in the entry,
|
||||
# so every re-install appended another @reboot line.)
|
||||
CURRENT_CRON="$(sudo -u "$ACTUAL_USER" crontab -l 2>/dev/null || true)"
|
||||
CLEANED_CRON="$(printf '%s\n' "$CURRENT_CRON" \
|
||||
| grep -v -F "$CRON_MARKER_TAG" \
|
||||
| grep -v -F "$CRON_WRAPPER" \
|
||||
| grep -v -F "# Kiwy signage player" || true)"
|
||||
{
|
||||
printf '%s\n' "$CLEANED_CRON" | grep -v '^$'
|
||||
echo "# Kiwy signage player autostart fallback (managed by install.sh)"
|
||||
echo "@reboot sleep 20 && $CRON_WRAPPER > /tmp/kivy-player-cron.log 2>&1"
|
||||
} | sudo -u "$ACTUAL_USER" crontab - 2>/dev/null || true
|
||||
echo "Cron fallback configured (single entry, deduplicated)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -442,7 +548,7 @@ write_player_config() {
|
||||
if [ -n "$PROVISION_URL" ]; then
|
||||
echo "Fetching provisioning data from: $PROVISION_URL"
|
||||
local response
|
||||
response=$(curl -sSf "$PROVISION_URL" 2>&1 || true)
|
||||
response=$(fetch_url "$PROVISION_URL" 2>/dev/null || true)
|
||||
if [ -n "$response" ]; then
|
||||
echo "$response" > "$config_file"
|
||||
echo "Provisioning data saved"
|
||||
@@ -509,7 +615,7 @@ with open(config_file, 'w') as f:
|
||||
|
||||
print("Configuration written")
|
||||
PYEOF
|
||||
elif [ \! -f "$config_file" ]; then
|
||||
elif [ ! -f "$config_file" ]; then
|
||||
cat > "$config_file" << 'EOF'
|
||||
{
|
||||
"server_ip": "",
|
||||
@@ -568,18 +674,18 @@ fi
|
||||
|
||||
# Create background runner
|
||||
NOHUP_WRAPPER="$SCRIPT_DIR/.run-background.sh"
|
||||
cat > "$NOHUP_WRAPPER" << EOF
|
||||
#\!/bin/bash
|
||||
cat > "$NOHUP_WRAPPER" << 'EOF'
|
||||
#!/bin/bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOG_FILE="$SCRIPT_DIR/logs/player-background.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting player in background..." >> "$LOG_FILE"
|
||||
cd "$SCRIPT_DIR" || exit 1
|
||||
nohup bash start.sh >> "$LOG_FILE" 2>&1 &
|
||||
PLAYER_PID=\$\!
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Player started with PID: \$PLAYER_PID" >> "$LOG_FILE"
|
||||
echo "Player started in background (PID: \$PLAYER_PID)"
|
||||
echo "Logs: \$LOG_FILE"
|
||||
PLAYER_PID=$!
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Player started with PID: $PLAYER_PID" >> "$LOG_FILE"
|
||||
echo "Player started in background (PID: $PLAYER_PID)"
|
||||
echo "Logs: $LOG_FILE"
|
||||
EOF
|
||||
chmod +x "$NOHUP_WRAPPER"
|
||||
echo "Background runner created"
|
||||
@@ -589,7 +695,7 @@ echo "Background runner created"
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Installation Complete\!"
|
||||
echo "Installation Complete!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Installation: $SCRIPT_DIR"
|
||||
@@ -636,7 +742,15 @@ if [ -f "$PI_BOOT_PATCH_SCRIPT" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -z "$SUDO_PASSWORD" ]; then
|
||||
if [ "$NO_REBOOT" = true ]; then
|
||||
echo "=========================================="
|
||||
echo "Reboot Skipped (--no-reboot)"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Some changes (HDMI settings, GPU memory) may require a reboot."
|
||||
echo "Reboot manually when convenient: sudo reboot"
|
||||
echo ""
|
||||
elif [ -z "$SUDO_PASSWORD" ]; then
|
||||
echo "=========================================="
|
||||
echo "System Reboot Recommended"
|
||||
echo "=========================================="
|
||||
|
||||
Reference in New Issue
Block a user