fix: stop ignoring scripts/linux/lib; track common.sh (neko + ws-scrcpy git url)
Made-with: Cursor
This commit is contained in:
735
scripts/linux/lib/common.sh
Normal file
735
scripts/linux/lib/common.sh
Normal file
@@ -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 <<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
|
||||
}
|
||||
Reference in New Issue
Block a user