831 lines
24 KiB
Bash
831 lines
24 KiB
Bash
#!/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:-3080}"
|
||
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}"
|
||
ENABLE_LIVE_EDGE_PROXY="${ENABLE_LIVE_EDGE_PROXY:-1}"
|
||
}
|
||
|
||
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 <<UNIT
|
||
[Unit]
|
||
Description=Live Super Hub Control Plane
|
||
After=network-online.target docker.service
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
User=live
|
||
WorkingDirectory=$ROOT_DIR
|
||
Environment=HOST=0.0.0.0
|
||
Environment=PORT=$PORT
|
||
ExecStart=$ROOT_DIR/start.sh
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNIT
|
||
systemctl daemon-reload
|
||
systemctl enable --now live-console.service
|
||
}
|
||
|
||
configure_sudoers() {
|
||
log "Configuring sudoers for live control plane"
|
||
cat >/etc/sudoers.d/live-control-plane <<SUDOERS
|
||
live ALL=(root) NOPASSWD: /bin/bash $SERVICE_CTL *
|
||
SUDOERS
|
||
chmod 440 /etc/sudoers.d/live-control-plane
|
||
}
|
||
|
||
detect_mdns_interfaces() {
|
||
local primary iface path found=0
|
||
local -a ifaces=()
|
||
|
||
primary="$(ip route show default 2>/dev/null | awk 'NR==1 {print $5}')"
|
||
if [ -n "$primary" ] && [ -d "/sys/class/net/$primary" ]; then
|
||
ifaces+=("$primary")
|
||
found=1
|
||
fi
|
||
|
||
for path in /sys/class/net/*; do
|
||
[ -e "$path" ] || continue
|
||
iface="${path##*/}"
|
||
[ "$iface" = "lo" ] && continue
|
||
if [ -d "$path/wireless" ]; then
|
||
case " ${ifaces[*]} " in
|
||
*" $iface "*) ;;
|
||
*)
|
||
ifaces+=("$iface")
|
||
found=1
|
||
;;
|
||
esac
|
||
fi
|
||
done
|
||
|
||
if [ "$found" -eq 0 ]; then
|
||
for path in /sys/class/net/*; do
|
||
[ -e "$path" ] || continue
|
||
iface="${path##*/}"
|
||
case "$iface" in
|
||
lo|docker*|br-*|veth*|virbr*|tun*|tap*|zt*|tailscale*|wg*|sit*|dummy*)
|
||
continue
|
||
;;
|
||
esac
|
||
ifaces+=("$iface")
|
||
found=1
|
||
break
|
||
done
|
||
fi
|
||
|
||
if [ "${#ifaces[@]}" -gt 0 ]; then
|
||
local joined
|
||
joined="$(IFS=,; echo "${ifaces[*]}")"
|
||
printf '%s\n' "$joined"
|
||
fi
|
||
}
|
||
|
||
configure_mdns() {
|
||
if [ "${ENABLE_MDNS:-1}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
log "Configuring hostname and mDNS"
|
||
hostnamectl set-hostname "$HOSTNAME_ALIAS"
|
||
|
||
local mdns_interfaces
|
||
mdns_interfaces="$(detect_mdns_interfaces)"
|
||
|
||
python3 - <<PY
|
||
from pathlib import Path
|
||
import re
|
||
|
||
hosts = Path("/etc/hosts")
|
||
hosts_text = hosts.read_text(encoding="utf-8")
|
||
if f"127.0.1.1\t${HOSTNAME_ALIAS}" not in hosts_text and f"127.0.1.1 ${HOSTNAME_ALIAS}" not in hosts_text:
|
||
lines = []
|
||
replaced = False
|
||
for raw in hosts_text.splitlines():
|
||
if raw.startswith("127.0.1.1"):
|
||
lines.append(f"127.0.1.1\t${HOSTNAME_ALIAS}")
|
||
replaced = True
|
||
else:
|
||
lines.append(raw)
|
||
if not replaced:
|
||
lines.append(f"127.0.1.1\t${HOSTNAME_ALIAS}")
|
||
hosts.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||
|
||
path = Path("/etc/avahi/avahi-daemon.conf")
|
||
raw_lines = path.read_text(encoding="utf-8").splitlines()
|
||
server_keys = {
|
||
"use-ipv4": "yes",
|
||
"use-ipv6": "no",
|
||
}
|
||
mdns_interfaces = "${mdns_interfaces}".strip()
|
||
if mdns_interfaces:
|
||
server_keys["allow-interfaces"] = mdns_interfaces
|
||
|
||
key_pattern = re.compile(r"^\s*[#;]?\s*([A-Za-z0-9-]+)\s*=.*$")
|
||
section_pattern = re.compile(r"^\s*\[([^\]]+)\]\s*$")
|
||
|
||
lines = []
|
||
current_section = None
|
||
server_seen = set()
|
||
server_found = False
|
||
|
||
def flush_server_defaults():
|
||
missing = [f"{key}={value}" for key, value in server_keys.items() if key not in server_seen]
|
||
if missing:
|
||
lines.extend(missing)
|
||
|
||
for raw in raw_lines:
|
||
section_match = section_pattern.match(raw)
|
||
if section_match:
|
||
if current_section == "server":
|
||
flush_server_defaults()
|
||
current_section = section_match.group(1).strip().lower()
|
||
server_found = server_found or current_section == "server"
|
||
lines.append(raw)
|
||
continue
|
||
|
||
key_match = key_pattern.match(raw)
|
||
if key_match and key_match.group(1) in server_keys:
|
||
key = key_match.group(1)
|
||
if current_section == "server":
|
||
if key not in server_seen:
|
||
lines.append(f"{key}={server_keys[key]}")
|
||
server_seen.add(key)
|
||
continue
|
||
|
||
lines.append(raw)
|
||
|
||
if server_found:
|
||
if current_section == "server":
|
||
flush_server_defaults()
|
||
else:
|
||
if lines and lines[-1] != "":
|
||
lines.append("")
|
||
lines.append("[server]")
|
||
flush_server_defaults()
|
||
|
||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||
PY
|
||
if [ "$DISABLE_IPV6" = "1" ]; then
|
||
cat >/etc/sysctl.d/99-live-disable-ipv6.conf <<CONF
|
||
net.ipv6.conf.all.disable_ipv6=1
|
||
net.ipv6.conf.default.disable_ipv6=1
|
||
net.ipv6.conf.lo.disable_ipv6=1
|
||
CONF
|
||
sysctl --system >/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 <<JSON
|
||
{
|
||
"ExtensionInstallForcelist": [
|
||
"dhdgffkkebhmkfjojejmpbldmpobfkfo;https://clients2.google.com/service/update2/crx"
|
||
],
|
||
"DeveloperToolsAvailability": 1
|
||
}
|
||
JSON
|
||
cp /etc/chromium/policies/managed/live-browser.json /etc/chromium-browser/policies/managed/live-browser.json 2>/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 <<RULES
|
||
SUBSYSTEM=="usb", ATTR{idVendor}=="2207", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
||
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
||
RULES
|
||
udevadm control --reload-rules || true
|
||
udevadm trigger || true
|
||
}
|
||
|
||
install_android_panel() {
|
||
if [ "${ENABLE_ANDROID_PANEL:-1}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
log "Installing Android web panel (ws-scrcpy)"
|
||
mkdir -p "$(dirname "$ANDROID_PANEL_DIR")"
|
||
ANDROID_PANEL_GIT_URL="${ANDROID_PANEL_GIT_URL:-https://github.com/baixin1228/web-scrcpy.git}"
|
||
if [ ! -d "$ANDROID_PANEL_DIR/.git" ]; then
|
||
rm -rf "$ANDROID_PANEL_DIR"
|
||
git clone "$ANDROID_PANEL_GIT_URL" "$ANDROID_PANEL_DIR"
|
||
else
|
||
git -C "$ANDROID_PANEL_DIR" pull --ff-only
|
||
fi
|
||
python3 - <<PY
|
||
from pathlib import Path
|
||
path = Path(r"$ANDROID_PANEL_DIR/app.py")
|
||
text = path.read_text(encoding="utf-8")
|
||
if "import os" not in text:
|
||
text = "import os\n" + text
|
||
text = text.replace(
|
||
"socketio.run(app, host='0.0.0.0', port=5000)",
|
||
"socketio.run(app, host='0.0.0.0', port=int(os.environ.get('ANDROID_PANEL_PORT', '5000')), allow_unsafe_werkzeug=True)",
|
||
)
|
||
path.write_text(text, encoding="utf-8")
|
||
PY
|
||
chown -R live:live "$ANDROID_PANEL_DIR"
|
||
runuser -u live -- bash -lc "python3 -m venv '$ANDROID_PANEL_DIR/.venv' && . '$ANDROID_PANEL_DIR/.venv/bin/activate' && pip install --upgrade pip && pip install -r '$ANDROID_PANEL_DIR/requirements.txt'"
|
||
|
||
cat >/etc/systemd/system/live-android-panel.service <<UNIT
|
||
[Unit]
|
||
Description=Live Android Web Panel
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
User=live
|
||
WorkingDirectory=$ANDROID_PANEL_DIR
|
||
Environment=ANDROID_PANEL_PORT=$ANDROID_PANEL_PORT
|
||
ExecStart=$ANDROID_PANEL_DIR/.venv/bin/python $ANDROID_PANEL_DIR/app.py --video_bit_rate $ANDROID_PANEL_VIDEO_BIT_RATE
|
||
Restart=always
|
||
RestartSec=3
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNIT
|
||
systemctl daemon-reload
|
||
systemctl enable --now live-android-panel.service
|
||
}
|
||
|
||
install_webtty() {
|
||
if [ "${ENABLE_WEBTTY:-1}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
log "Installing WebTTY"
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
if ! apt-get install -y --no-install-recommends ttyd >/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 <<UNIT
|
||
[Unit]
|
||
Description=Live WebTTY
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
User=live
|
||
WorkingDirectory=$ROOT_DIR
|
||
ExecStart=$(command -v ttyd) -W -p $WEBTTY_PORT -c $WEBTTY_USER:$WEBTTY_PASS $WEBTTY_SHELL
|
||
Restart=always
|
||
RestartSec=3
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNIT
|
||
systemctl daemon-reload
|
||
systemctl enable --now live-webtty.service
|
||
}
|
||
|
||
install_cockpit() {
|
||
if [ "${ENABLE_COCKPIT:-1}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
log "Installing Cockpit"
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt_install cockpit cockpit-networkmanager
|
||
systemctl enable --now cockpit.socket
|
||
}
|
||
|
||
install_netdata() {
|
||
if [ "${ENABLE_NETDATA:-1}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
log "Installing Netdata"
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt_install netdata
|
||
python3 - <<PY
|
||
from pathlib import Path
|
||
path = Path("/etc/netdata/netdata.conf")
|
||
text = path.read_text(encoding="utf-8")
|
||
if "bind socket to IP = 127.0.0.1" in text:
|
||
text = text.replace("bind socket to IP = 127.0.0.1", "bind socket to IP = 0.0.0.0")
|
||
elif "bind socket to IP =" not in text:
|
||
text += "\n[global]\n\tbind socket to IP = 0.0.0.0\n"
|
||
path.write_text(text, encoding="utf-8")
|
||
PY
|
||
systemctl enable --now netdata
|
||
systemctl restart netdata
|
||
}
|
||
|
||
render_homepage_services() {
|
||
local target="$ROOT_DIR/services/homepage/config/services.yaml"
|
||
local primary_console
|
||
if [ "${ENABLE_LIVE_EDGE_PROXY:-1}" = "1" ]; then
|
||
primary_console="http://${HOSTNAME_ALIAS}.local"
|
||
else
|
||
primary_console="http://${HOSTNAME_ALIAS}.local:${PORT}"
|
||
fi
|
||
log "Rendering Homepage service links"
|
||
cat >"$target" <<YAML
|
||
- Infrastructure:
|
||
- Live Console:
|
||
href: ${primary_console}
|
||
description: Product control center (Caddy :80 when edge proxy enabled)
|
||
- Hub Console (direct port):
|
||
href: http://${HOSTNAME_ALIAS}.local:${PORT}
|
||
description: Direct access to FastAPI port
|
||
YAML
|
||
if [ "${ENABLE_WEBTTY:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- WebTTY:
|
||
href: http://${HOSTNAME_ALIAS}.local:${WEBTTY_PORT}
|
||
description: Browser terminal as the live user
|
||
YAML
|
||
fi
|
||
if [ "${ENABLE_COCKPIT:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- Cockpit:
|
||
href: https://${HOSTNAME_ALIAS}.local:9090
|
||
description: System administration
|
||
YAML
|
||
fi
|
||
if [ "${ENABLE_FILEBROWSER:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- File Browser:
|
||
href: http://${HOSTNAME_ALIAS}.local:${FILEBROWSER_PORT}
|
||
description: Beginner-friendly file manager
|
||
YAML
|
||
fi
|
||
if [ "${ENABLE_ANDROID_PANEL:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- Android Center:
|
||
href: http://${HOSTNAME_ALIAS}.local:${PORT}/android
|
||
description: Device center with preview, ADB controls and embedded web-scrcpy
|
||
YAML
|
||
fi
|
||
if [ "${ENABLE_SHELLCRASH:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- ShellCrash:
|
||
href: http://${HOSTNAME_ALIAS}.local:${PORT}/shellcrash
|
||
description: Dedicated YAML, JSON and rule editing workspace
|
||
YAML
|
||
fi
|
||
if [ "${ENABLE_NETDATA:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- Netdata:
|
||
href: http://${HOSTNAME_ALIAS}.local:${NETDATA_PORT}
|
||
description: Metrics and health monitoring
|
||
YAML
|
||
fi
|
||
if [ "${ENABLE_SRS:-1}" = "1" ]; then
|
||
cat >>"$target" <<YAML
|
||
- SRS:
|
||
href: http://${HOSTNAME_ALIAS}.local:${SRS_HTTP_PORT}
|
||
description: RTMP, SRT and preview endpoint
|
||
YAML
|
||
fi
|
||
}
|
||
|
||
prepare_homepage() {
|
||
if [ "${ENABLE_HOMEPAGE:-1}" != "1" ] || ! need_docker; then
|
||
return 0
|
||
fi
|
||
log "Preparing Homepage"
|
||
render_homepage_services
|
||
compose_run "$ROOT_DIR/services/homepage/docker-compose.yml" up -d
|
||
}
|
||
|
||
prepare_filebrowser() {
|
||
if [ "${ENABLE_FILEBROWSER:-1}" != "1" ] || ! need_docker; then
|
||
return 0
|
||
fi
|
||
log "Preparing File Browser"
|
||
local fb_image="filebrowser/filebrowser:latest"
|
||
local fb_db_dir="$ROOT_DIR/services/filebrowser/database"
|
||
local fb_cfg_dir="$ROOT_DIR/services/filebrowser/config"
|
||
local fb_db="$fb_db_dir/filebrowser.db"
|
||
local fb_settings="$fb_cfg_dir/settings.json"
|
||
local fb_tmp_json fb_hash
|
||
mkdir -p "$ROOT_DIR/services/filebrowser/database" "$ROOT_DIR/services/filebrowser/config"
|
||
rm -f "$fb_db"
|
||
touch "$fb_db"
|
||
chmod -R 0777 "$ROOT_DIR/services/filebrowser/database" "$ROOT_DIR/services/filebrowser/config"
|
||
compose_run "$ROOT_DIR/services/filebrowser/docker-compose.yml" down >/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"
|
||
}
|
||
|
||
install_live_edge_proxy() {
|
||
if [ "${ENABLE_LIVE_EDGE_PROXY:-1}" != "1" ]; then
|
||
return 0
|
||
fi
|
||
log "Installing edge proxy (Caddy :80 → control plane :${PORT})"
|
||
mkdir -p /opt/live/generated
|
||
local lp="$ROOT_DIR/live-platform"
|
||
if [ -f "$lp/tools/render_caddyfile.py" ]; then
|
||
python3 "$lp/tools/render_caddyfile.py" "$HOSTNAME_ALIAS" "$PORT" /opt/live/generated/Caddyfile
|
||
else
|
||
log "WARN: live-platform/tools/render_caddyfile.py missing, skip Caddyfile"
|
||
return 0
|
||
fi
|
||
|
||
# 若 Homepage 仍占 80,下线并改绑 3080 后重启
|
||
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx live-homepage; then
|
||
if docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null | grep -E 'live-homepage' | grep -qE '0\.0\.0\.0:80->|:::80->'; then
|
||
log "Rebinding Homepage from host :80 → :3080 (free port for Caddy)"
|
||
export HOMEPAGE_PORT=3080
|
||
if [ -f "$ROOT_DIR/config/system-stack.env" ] && grep -q '^HOMEPAGE_PORT=80' "$ROOT_DIR/config/system-stack.env"; then
|
||
sed -i 's/^HOMEPAGE_PORT=80$/HOMEPAGE_PORT=3080/' "$ROOT_DIR/config/system-stack.env" || true
|
||
fi
|
||
compose_run "$ROOT_DIR/services/homepage/docker-compose.yml" down >/dev/null 2>&1 || true
|
||
render_homepage_services
|
||
compose_run "$ROOT_DIR/services/homepage/docker-compose.yml" up -d || true
|
||
fi
|
||
fi
|
||
|
||
if ! command -v caddy >/dev/null 2>&1; then
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get install -y -qq caddy || true
|
||
fi
|
||
fi
|
||
if ! command -v caddy >/dev/null 2>&1; then
|
||
log "Installing Caddy from GitHub release (apt had no caddy)"
|
||
local arch raw
|
||
raw="$(uname -m)"
|
||
case "$raw" in
|
||
aarch64|arm64) arch="arm64" ;;
|
||
x86_64|amd64) arch="amd64" ;;
|
||
armv7l|armv6l) arch="armv7" ;;
|
||
*)
|
||
log "WARN: unknown arch $raw — install caddy manually"
|
||
return 0
|
||
;;
|
||
esac
|
||
local ver="2.8.4"
|
||
local tmp="/tmp/caddy-${ver}-linux-${arch}.tar.gz"
|
||
curl -fsSL "https://github.com/caddyserver/caddy/releases/download/v${ver}/caddy_${ver}_linux_${arch}.tar.gz" -o "$tmp"
|
||
tar -xzf "$tmp" -C /tmp caddy
|
||
install -m0755 /tmp/caddy /usr/local/bin/caddy
|
||
rm -f "$tmp" /tmp/caddy
|
||
fi
|
||
|
||
systemctl stop caddy >/dev/null 2>&1 || true
|
||
systemctl disable caddy >/dev/null 2>&1 || true
|
||
|
||
local caddy_bin
|
||
caddy_bin="$(command -v caddy || true)"
|
||
[ -n "$caddy_bin" ] || caddy_bin="/usr/local/bin/caddy"
|
||
|
||
cat >/etc/systemd/system/live-edge.service <<UNIT
|
||
[Unit]
|
||
Description=Live edge proxy (http://${HOSTNAME_ALIAS}.local -> :${PORT})
|
||
After=network-online.target live-console.service
|
||
Wants=network-online.target
|
||
Requires=live-console.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=$caddy_bin run --config /opt/live/generated/Caddyfile --adapter caddyfile
|
||
Restart=on-failure
|
||
RestartSec=4
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNIT
|
||
systemctl daemon-reload
|
||
systemctl enable --now live-edge.service
|
||
log "Edge proxy live-edge.service enabled (http://${HOSTNAME_ALIAS}.local)"
|
||
}
|
||
|
||
print_summary() {
|
||
local profile="$1"
|
||
cat <<TXT
|
||
|
||
Profile: $profile
|
||
Primary URL: http://${HOSTNAME_ALIAS}.local (Caddy → :${PORT} when live-edge is running)
|
||
Control Plane (direct): http://${HOSTNAME_ALIAS}.local:${PORT}
|
||
Homepage: http://${HOSTNAME_ALIAS}.local:${HOMEPAGE_PORT}
|
||
WebTTY: http://${HOSTNAME_ALIAS}.local:${WEBTTY_PORT}
|
||
TXT
|
||
}
|