diff --git a/.gitignore b/.gitignore index 554211e..1a4f59e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,9 @@ develop-eggs/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +# 仅忽略仓库根目录的 lib(勿用 lib/ 否则会误伤 scripts/linux/lib) +/lib/ +/lib64/ parts/ sdist/ var/ diff --git a/scripts/linux/lib/common.sh b/scripts/linux/lib/common.sh new file mode 100644 index 0000000..9239246 --- /dev/null +++ b/scripts/linux/lib/common.sh @@ -0,0 +1,735 @@ +#!/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 <