736 lines
21 KiB
Bash
736 lines
21 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:-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 <<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"
|
|
log "Rendering Homepage service links"
|
|
cat >"$target" <<YAML
|
|
- Infrastructure:
|
|
- Hub Console:
|
|
href: http://${HOSTNAME_ALIAS}.local:${PORT}
|
|
description: Chinese-first control plane and modular service center
|
|
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"
|
|
}
|
|
|
|
print_summary() {
|
|
local profile="$1"
|
|
cat <<TXT
|
|
|
|
Profile: $profile
|
|
Control Plane: http://${HOSTNAME_ALIAS}.local:${PORT}
|
|
Homepage: http://${HOSTNAME_ALIAS}.local${HOMEPAGE_PORT:+:${HOMEPAGE_PORT}}
|
|
WebTTY: http://${HOSTNAME_ALIAS}.local:${WEBTTY_PORT}
|
|
TXT
|
|
}
|