#!/usr/bin/env bash set -euo pipefail LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="${ROOT_DIR:-$(cd "$LIB_DIR/../../.." && pwd)}" CONFIG_DIR="$ROOT_DIR/config" STACK_ENV="$CONFIG_DIR/system-stack.env" STACK_ENV_EXAMPLE="$CONFIG_DIR/system-stack.env.example" SERVICE_CTL="$ROOT_DIR/scripts/linux/service_ctl.sh" WEBTTY_SHELL="$ROOT_DIR/scripts/linux/webtty_shell.sh" APT_STAMP="/tmp/live-stack-apt-updated.stamp" INSTALL_TAG="${INSTALL_TAG:-stack}" log() { printf '[%s] %s\n' "$INSTALL_TAG" "$*" } ensure_root() { if [ "${EUID:-$(id -u)}" -ne 0 ]; then exec sudo -E bash "$0" "$@" fi } apt_update_once() { local now age now="$(date +%s)" if [ -f "$APT_STAMP" ]; then age=$((now - $(stat -c %Y "$APT_STAMP"))) if [ "$age" -lt 21600 ]; then return 0 fi fi log "Updating apt metadata" apt-get update touch "$APT_STAMP" } apt_install() { apt_update_once apt-get install -y --no-install-recommends "$@" } have() { command -v "$1" >/dev/null 2>&1 } need_docker() { [ "${ENABLE_SRS:-1}" = "1" ] || [ "${ENABLE_HOMEPAGE:-1}" = "1" ] || [ "${ENABLE_FILEBROWSER:-1}" = "1" ] || [ "${ENABLE_NEKO:-0}" = "1" ] } compose_run() { local compose_file="$1" shift if docker compose version >/dev/null 2>&1; then docker compose -f "$compose_file" "$@" else docker-compose -f "$compose_file" "$@" fi } as_live() { runuser -u live -- bash -lc "cd '$ROOT_DIR' && $*" } load_env() { mkdir -p "$CONFIG_DIR" if [ ! -f "$STACK_ENV" ] && [ -f "$STACK_ENV_EXAMPLE" ]; then cp "$STACK_ENV_EXAMPLE" "$STACK_ENV" fi if [ -f "$STACK_ENV" ]; then set -a . "$STACK_ENV" set +a fi HOSTNAME_ALIAS="${HOSTNAME_ALIAS:-live}" PORT="${PORT:-8001}" WEBTTY_PORT="${WEBTTY_PORT:-7681}" HOMEPAGE_PORT="${HOMEPAGE_PORT:-80}" FILEBROWSER_PORT="${FILEBROWSER_PORT:-8082}" NETDATA_PORT="${NETDATA_PORT:-19999}" SRS_HTTP_PORT="${SRS_HTTP_PORT:-8080}" SRS_API_PORT="${SRS_API_PORT:-1985}" SRS_RTMP_PORT="${SRS_RTMP_PORT:-1935}" SRS_SRT_PORT="${SRS_SRT_PORT:-10080}" SRS_WEBRTC_PORT="${SRS_WEBRTC_PORT:-8088}" SHELLCRASH_DIR="${SHELLCRASH_DIR:-/etc/ShellCrash}" WEBTTY_USER="${WEBTTY_USER:-live}" WEBTTY_PASS="${WEBTTY_PASS:-12345678}" FILEBROWSER_USER="${FILEBROWSER_USER:-live}" FILEBROWSER_PASS="${FILEBROWSER_PASS:-12345678}" WIFI_CONNECTION_NAME="${WIFI_CONNECTION_NAME:-live-wifi}" WIFI_SSID="${WIFI_SSID:-live}" WIFI_PSK="${WIFI_PSK:-12345678}" BROWSER_PROFILE_DIR="${BROWSER_PROFILE_DIR:-/var/lib/live-browser/profile}" DISABLE_IPV6="${DISABLE_IPV6:-1}" ANDROID_PANEL_PORT="${ANDROID_PANEL_PORT:-5000}" ANDROID_PANEL_DIR="${ANDROID_PANEL_DIR:-/opt/live-modules/web-scrcpy}" ANDROID_PANEL_VIDEO_BIT_RATE="${ANDROID_PANEL_VIDEO_BIT_RATE:-1024000}" } ensure_live_user() { if ! id -u live >/dev/null 2>&1; then log "Creating live user" useradd -m -s /bin/bash live fi echo "live:12345678" | chpasswd usermod -aG sudo,audio,video,plugdev,render,gpio,spidev,pwm,i2c,docker live 2>/dev/null || true } install_base_packages() { log "Installing base packages" export DEBIAN_FRONTEND=noninteractive apt_install \ git curl wget jq unzip xz-utils ca-certificates sudo \ python3 python3-venv python3-pip build-essential \ avahi-daemon avahi-utils libnss-mdns \ network-manager dnsmasq net-tools \ bluez systemctl enable --now NetworkManager 2>/dev/null || true } install_media_packages() { log "Installing media and Android tools" export DEBIAN_FRONTEND=noninteractive apt_install ffmpeg mpv adb xinit xserver-xorg openbox dbus-x11 apt_install scrcpy || true } install_nodejs() { if have node; then local major major="$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)" if [ "$major" -ge 20 ]; then return 0 fi fi log "Installing Node.js 20" curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt_install nodejs } install_docker_stack() { if ! need_docker; then return 0 fi if have docker && (docker compose version >/dev/null 2>&1 || have docker-compose); then return 0 fi log "Installing Docker" apt_install docker.io docker-compose systemctl enable --now docker usermod -aG docker live || true } prepare_python_runtime() { log "Preparing Python virtual environment" chown -R live:live "$ROOT_DIR" as_live "python3 -m venv .venv" as_live ". .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt" } prepare_web_console() { log "Building web console" as_live "cd web-console && if [ -f package-lock.json ]; then npm ci; else npm install; fi && npm run build" } install_live_console_service() { log "Installing live console service" chmod +x "$ROOT_DIR/start.sh" "$ROOT_DIR/web.sh" "$ROOT_DIR/dev.sh" "$SERVICE_CTL" "$WEBTTY_SHELL" cat >/etc/systemd/system/live-console.service </etc/sudoers.d/live-control-plane </etc/sysctl.d/99-live-disable-ipv6.conf </dev/null 2>&1 || true fi systemctl enable --now avahi-daemon systemctl restart avahi-daemon } configure_wifi_profile() { if [ "${ENABLE_WIFI_PROFILE:-1}" != "1" ]; then return 0 fi if ! have nmcli || [ ! -d /sys/class/net/wlan0 ]; then log "Skipping Wi-Fi profile setup: wlan0 or nmcli is missing" return 0 fi log "Ensuring Wi-Fi profile ${WIFI_CONNECTION_NAME}" if ! nmcli connection show "$WIFI_CONNECTION_NAME" >/dev/null 2>&1; then nmcli connection add type wifi ifname wlan0 con-name "$WIFI_CONNECTION_NAME" ssid "$WIFI_SSID" \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$WIFI_PSK" ipv4.method auto connection.autoconnect yes fi } install_shellcrash() { if [ "${ENABLE_SHELLCRASH:-1}" != "1" ]; then return 0 fi if [ -x "$SHELLCRASH_DIR/start.sh" ]; then log "ShellCrash already installed" return 0 fi log "Installing ShellCrash" local url for url in \ 'https://testingcf.jsdelivr.net/gh/juewuy/ShellCrash@dev' \ 'https://raw.githubusercontent.com/juewuy/ShellCrash/dev' do if wget -q --no-check-certificate -O /tmp/install_shellcrash.sh "$url/install_en.sh" && [ -s /tmp/install_shellcrash.sh ]; then break fi done if [ ! -s /tmp/install_shellcrash.sh ]; then log "ShellCrash installer download failed" return 1 fi printf '1\n1\n1\n1\n' | bash /tmp/install_shellcrash.sh } install_browser_stack() { if [ "${ENABLE_BROWSER:-1}" != "1" ]; then return 0 fi log "Installing browser stack" export DEBIAN_FRONTEND=noninteractive apt_install chromium || apt_install chromium-browser || true mkdir -p /etc/chromium/policies/managed /etc/chromium-browser/policies/managed cat >/etc/chromium/policies/managed/live-browser.json </dev/null || true mkdir -p "$BROWSER_PROFILE_DIR" chown -R live:live "$(dirname "$BROWSER_PROFILE_DIR")" chmod +x "$ROOT_DIR/scripts/linux/launch_browser.sh" } install_neko_browser() { if [ "${ENABLE_NEKO:-0}" != "1" ]; then return 0 fi if ! have docker || ! (docker compose version >/dev/null 2>&1 || have docker-compose); then log "Neko skipped: docker compose unavailable" return 0 fi log "Installing Neko Chromium (Docker, m1k1o/neko)" set -a [ -f "$STACK_ENV" ] && . "$STACK_ENV" set +a export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}" compose_run "$ROOT_DIR/services/neko/docker-compose.yml" up -d } configure_adb_udev() { log "Configuring Android udev access" cat >/etc/udev/rules.d/51-live-android.rules </etc/systemd/system/live-android-panel.service </dev/null 2>&1; then log "ttyd package unavailable in apt repositories" fi if ! have ttyd; then log "WebTTY skipped because ttyd is unavailable" return 0 fi systemctl disable --now ttyd.service >/dev/null 2>&1 || true cat >/etc/systemd/system/live-webtty.service <"$target" <>"$target" <>"$target" <>"$target" <>"$target" <>"$target" <>"$target" <>"$target" </dev/null 2>&1 || true docker run --rm \ -v "$fb_db_dir:/database" \ -v "$fb_cfg_dir:/config" \ "$fb_image" \ config init --config /config/settings.json --database /database/filebrowser.db >/dev/null 2>&1 || true docker run --rm \ -v "$fb_db_dir:/database" \ -v "$fb_cfg_dir:/config" \ "$fb_image" \ config set \ --address 0.0.0.0 \ --port 80 \ --locale zh-cn \ --auth.method json \ --minimumPasswordLength 8 \ --hideLoginButton=false \ --branding.name "Live File Browser" \ --config /config/settings.json \ --database /database/filebrowser.db >/dev/null fb_hash="$(docker run --rm "$fb_image" hash "$FILEBROWSER_PASS" | tail -n 1)" fb_tmp_json="$fb_cfg_dir/filebrowser-users.import.json" FILEBROWSER_HASH="$fb_hash" FILEBROWSER_USER="$FILEBROWSER_USER" python3 - <<'PY' >"$fb_tmp_json" import json import os password_hash = os.environ["FILEBROWSER_HASH"].strip() username = os.environ["FILEBROWSER_USER"].strip() or "live" base_user = { "scope": ".", "locale": "zh-cn", "lockPassword": False, "viewMode": "mosaic", "singleClick": False, "redirectAfterCopyMove": True, "perm": { "admin": True, "execute": True, "create": True, "rename": True, "modify": True, "delete": True, "share": True, "download": True, }, "commands": [], "sorting": {"by": "name", "asc": False}, "rules": [], "hideDotfiles": False, "dateFormat": False, "aceEditorTheme": "", } print( json.dumps( [ {"id": 1, "username": "admin", "password": password_hash, **base_user}, {"id": 0, "username": username, "password": password_hash, **base_user}, ], indent=2, ) ) PY chmod 0666 "$fb_tmp_json" docker run --rm \ -w /config \ -v "$fb_db_dir:/database" \ -v "$fb_cfg_dir:/config" \ "$fb_image" \ users import $(basename "$fb_tmp_json") --overwrite --config /config/settings.json --database /database/filebrowser.db >/dev/null rm -f "$fb_cfg_dir/users.backup.json" rm -f "$fb_tmp_json" compose_run "$ROOT_DIR/services/filebrowser/docker-compose.yml" up -d } prepare_srs() { if [ "${ENABLE_SRS:-1}" != "1" ] || ! need_docker; then return 0 fi log "Starting SRS" mkdir -p "$ROOT_DIR/services/srs/data" compose_run "$ROOT_DIR/services/srs/docker-compose.yml" up -d } run_hardware_probe() { log "Probing hardware profile" as_live ". .venv/bin/activate && python scripts/hardware_probe.py --write" } print_summary() { local profile="$1" cat <