#!/usr/bin/env bash set -euo pipefail SERVICE_ID="${1:-}" ACTION="${2:-status}" ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" CONFIG_DIR="$ROOT_DIR/config" STACK_ENV="$CONFIG_DIR/system-stack.env" HW_ENV="$CONFIG_DIR/hardware.env" SRS_COMPOSE="$ROOT_DIR/services/srs/docker-compose.yml" HOMEPAGE_COMPOSE="$ROOT_DIR/services/homepage/docker-compose.yml" FILEBROWSER_COMPOSE="$ROOT_DIR/services/filebrowser/docker-compose.yml" NEKO_COMPOSE="$ROOT_DIR/services/neko/docker-compose.yml" BROWSER_LAUNCHER="$ROOT_DIR/scripts/linux/launch_browser.sh" [ -f "$STACK_ENV" ] && set -a && . "$STACK_ENV" && set +a [ -f "$HW_ENV" ] && set -a && . "$HW_ENV" && set +a 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_RTMP_PORT="${SRS_RTMP_PORT:-1935}" SRS_API_PORT="${SRS_API_PORT:-1985}" 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}" ANDROID_GATEWAY_NAME="${ANDROID_GATEWAY_NAME:-live-android-gateway}" ANDROID_UPLINK_IF="${ANDROID_UPLINK_IF:-eth0}" ANDROID_DOWNLINK_IF="${ANDROID_DOWNLINK_IF:-}" ANDROID_GATEWAY_CIDR="${ANDROID_GATEWAY_CIDR:-192.168.51.1/24}" ANDROID_PANEL_PORT="${ANDROID_PANEL_PORT:-5000}" NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}" CONSOLE_BASE_URL="http://${HOSTNAME_ALIAS}.local:${PORT}" kv() { printf '%s=%s\n' "$1" "$2" } have() { command -v "$1" >/dev/null 2>&1 } filtered_pgrep() { local pattern="$1" pgrep -fa "$pattern" 2>/dev/null | grep -v -F "$0" | grep -v -F "pgrep -fa" || true } unit_exists() { systemctl list-unit-files "$1" --no-legend 2>/dev/null | grep -q "^$1" } unit_active() { systemctl is-active --quiet "$1" } compose_bin() { if have docker && docker compose version >/dev/null 2>&1; then echo "docker compose" return 0 fi if have docker-compose; then echo "docker-compose" return 0 fi return 1 } compose_run() { local compose_file="$1" shift local cmd cmd="$(compose_bin)" || return 127 if [ "$cmd" = "docker compose" ]; then docker compose -f "$compose_file" "$@" else docker-compose -f "$compose_file" "$@" fi } docker_container_status() { local name="$1" if ! have docker; then return 1 fi docker ps -a --filter "name=^/${name}$" --format '{{.State}}|{{.Status}}' | head -n 1 } service_mdns_status() { local detail hostname ipv6 hostname="$(hostname)" ipv6="unknown" if grep -Eq '^[[:space:]]*use-ipv6=no' /etc/avahi/avahi-daemon.conf 2>/dev/null; then ipv6="disabled" fi detail="hostname=${hostname}; avahi_ipv6=${ipv6}" kv available 1 kv running "$(unit_active avahi-daemon && echo 1 || echo 0)" kv status "$(unit_active avahi-daemon && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port 22 kv url "ssh ${WEBTTY_USER}@${HOSTNAME_ALIAS}.local" kv detail "$detail" } service_mdns_start() { hostnamectl set-hostname "$HOSTNAME_ALIAS" systemctl restart avahi-daemon kv message "mDNS hostname refreshed" } service_mdns_stop() { systemctl stop avahi-daemon kv message "avahi-daemon stopped" } service_wifi_status() { if ! have nmcli || [ ! -d /sys/class/net/wlan0 ]; then kv available 0 kv running 0 kv status unavailable kv detail "NetworkManager or wlan0 is not available" return 0 fi local active detail active="$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | grep "^${WIFI_CONNECTION_NAME}:" || true)" detail="$(nmcli -t -f NAME,UUID,TYPE,AUTOCONNECT connection show "$WIFI_CONNECTION_NAME" 2>/dev/null | paste -sd ';' - || true)" kv available 1 kv running "$([ -n "$active" ] && echo 1 || echo 0)" kv status "$([ -n "$active" ] && echo online || echo configured)" kv host "${HOSTNAME_ALIAS}.local" kv port "" kv url "" kv detail "${detail:-ssid=${WIFI_SSID}}" } service_wifi_start() { if ! have nmcli || [ ! -d /sys/class/net/wlan0 ]; then kv message "Wi-Fi interface is not available" exit 1 fi 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:-12345678}" ipv4.method auto connection.autoconnect yes fi nmcli connection up "$WIFI_CONNECTION_NAME" >/dev/null 2>&1 || true kv message "Wi-Fi profile ensured" } service_wifi_stop() { nmcli connection down "$WIFI_CONNECTION_NAME" >/dev/null 2>&1 || true kv message "Wi-Fi profile disconnected" } service_shellcrash_status() { local running detail if [ ! -x "$SHELLCRASH_DIR/start.sh" ] && ! unit_exists shellcrash.service; then kv available 0 kv running 0 kv status unavailable kv detail "ShellCrash is not installed" return 0 fi running=0 detail="$(filtered_pgrep 'CrashCore|shellcrash' | tr '\n' ';' || true)" if unit_active shellcrash.service || [ -n "$detail" ]; then running=1 fi [ -z "$detail" ] && detail="dir=$SHELLCRASH_DIR" kv available 1 kv running "$running" kv status "$([ "$running" = 1 ] && echo online || echo stopped)" kv url "${CONSOLE_BASE_URL}/shellcrash" kv host "${HOSTNAME_ALIAS}.local" kv port "" kv detail "$detail" } service_shellcrash_start() { if unit_exists shellcrash.service; then systemctl enable --now shellcrash.service elif [ -x "$SHELLCRASH_DIR/start.sh" ]; then "$SHELLCRASH_DIR/start.sh" start else kv message "ShellCrash is not installed" exit 1 fi kv message "ShellCrash started" } service_shellcrash_stop() { if unit_exists shellcrash.service; then systemctl stop shellcrash.service elif [ -x "$SHELLCRASH_DIR/start.sh" ]; then "$SHELLCRASH_DIR/start.sh" stop fi kv message "ShellCrash stopped" } service_srs_status() { if ! have docker || [ ! -f "$SRS_COMPOSE" ]; then kv available 0 kv running 0 kv status unavailable kv detail "Docker or SRS compose file is missing" return 0 fi local state state="$(docker_container_status live-srs || true)" kv available 1 kv running "$([[ "$state" == running* ]] && echo 1 || echo 0)" kv status "$([[ "$state" == running* ]] && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$SRS_HTTP_PORT" kv url "http://${HOSTNAME_ALIAS}.local:${SRS_HTTP_PORT}" kv detail "${state:-container=missing};rtmp=${SRS_RTMP_PORT};api=${SRS_API_PORT};srt=${SRS_SRT_PORT};rtc=${SRS_WEBRTC_PORT}" } service_srs_start() { compose_run "$SRS_COMPOSE" up -d kv message "SRS started" } service_srs_stop() { compose_run "$SRS_COMPOSE" down kv message "SRS stopped" } service_webtty_status() { if ! unit_exists live-webtty.service && ! have ttyd; then kv available 0 kv running 0 kv status unavailable kv detail "ttyd is not installed" return 0 fi kv available 1 kv running "$(unit_active live-webtty.service && echo 1 || echo 0)" kv status "$(unit_active live-webtty.service && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$WEBTTY_PORT" kv url "http://${HOSTNAME_ALIAS}.local:${WEBTTY_PORT}" kv detail "web user=${WEBTTY_USER}" } service_webtty_start() { systemctl enable --now live-webtty.service kv message "WebTTY started" } service_webtty_stop() { systemctl stop live-webtty.service kv message "WebTTY stopped" } service_cockpit_status() { if ! unit_exists cockpit.socket; then kv available 0 kv running 0 kv status unavailable kv detail "Cockpit is not installed" return 0 fi kv available 1 kv running "$(unit_active cockpit.socket && echo 1 || echo 0)" kv status "$(unit_active cockpit.socket && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port 9090 kv url "https://${HOSTNAME_ALIAS}.local:9090" kv detail "login with live or any enabled system account" } service_cockpit_start() { systemctl enable --now cockpit.socket kv message "Cockpit started" } service_cockpit_stop() { systemctl stop cockpit.socket kv message "Cockpit stopped" } service_filebrowser_status() { if ! have docker || [ ! -f "$FILEBROWSER_COMPOSE" ]; then kv available 0 kv running 0 kv status unavailable kv detail "Docker or File Browser compose file is missing" return 0 fi local state state="$(docker_container_status live-filebrowser || true)" kv available 1 kv running "$([[ "$state" == running* ]] && echo 1 || echo 0)" kv status "$([[ "$state" == running* ]] && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$FILEBROWSER_PORT" kv url "http://${HOSTNAME_ALIAS}.local:${FILEBROWSER_PORT}" kv detail "${state:-container=missing};user=${FILEBROWSER_USER:-live}" } service_filebrowser_start() { compose_run "$FILEBROWSER_COMPOSE" up -d kv message "File Browser started" } service_filebrowser_stop() { compose_run "$FILEBROWSER_COMPOSE" down kv message "File Browser stopped" } service_homepage_status() { if ! have docker || [ ! -f "$HOMEPAGE_COMPOSE" ]; then kv available 0 kv running 0 kv status unavailable kv detail "Docker or Homepage compose file is missing" return 0 fi local state state="$(docker_container_status live-homepage || true)" kv available 1 kv running "$([[ "$state" == running* ]] && echo 1 || echo 0)" kv status "$([[ "$state" == running* ]] && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$HOMEPAGE_PORT" kv url "$([ "$HOMEPAGE_PORT" = 80 ] && echo "http://${HOSTNAME_ALIAS}.local" || echo "http://${HOSTNAME_ALIAS}.local:${HOMEPAGE_PORT}")" kv detail "${state:-container=missing}" } service_homepage_start() { compose_run "$HOMEPAGE_COMPOSE" up -d kv message "Homepage started" } service_homepage_stop() { compose_run "$HOMEPAGE_COMPOSE" down kv message "Homepage stopped" } service_netdata_status() { if ! unit_exists netdata.service; then kv available 0 kv running 0 kv status unavailable kv detail "Netdata is not installed" return 0 fi kv available 1 kv running "$(unit_active netdata.service && echo 1 || echo 0)" kv status "$(unit_active netdata.service && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$NETDATA_PORT" kv url "http://${HOSTNAME_ALIAS}.local:${NETDATA_PORT}" kv detail "metrics and alarms API available at /api/v1" } service_netdata_start() { systemctl enable --now netdata kv message "Netdata started" } service_netdata_stop() { systemctl stop netdata kv message "Netdata stopped" } service_adb_status() { if ! have adb; then kv available 0 kv running 0 kv status unavailable kv detail "adb is not installed" return 0 fi local details details="$(adb devices -l 2>/dev/null | tr '\n' ';' || true)" kv available 1 kv running "$(filtered_pgrep 'adb -L tcp:5037|[ /]adb([ ]|$)' >/dev/null 2>&1 && echo 1 || echo 0)" kv status "$(filtered_pgrep 'adb -L tcp:5037|[ /]adb([ ]|$)' >/dev/null 2>&1 && echo online || echo idle)" kv host "${HOSTNAME_ALIAS}.local" kv port "" kv url "" kv detail "${details:-adb server ready}" } service_adb_start() { adb start-server >/dev/null kv message "ADB server started" } service_adb_stop() { adb kill-server >/dev/null 2>&1 || true kv message "ADB server stopped" } service_android_panel_status() { if ! unit_exists live-android-panel.service; then kv available 0 kv running 0 kv status unavailable kv detail "Android web panel is not installed" return 0 fi kv available 1 kv running "$(unit_active live-android-panel.service && echo 1 || echo 0)" kv status "$(unit_active live-android-panel.service && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$ANDROID_PANEL_PORT" kv url "${CONSOLE_BASE_URL}/android" kv detail "android-center=${CONSOLE_BASE_URL}/android;ws-scrcpy=http://${HOSTNAME_ALIAS}.local:${ANDROID_PANEL_PORT};git=ANDROID_PANEL_GIT_URL" } service_android_panel_start() { systemctl enable --now live-android-panel.service kv message "Android web panel started" } service_android_panel_stop() { systemctl stop live-android-panel.service kv message "Android web panel stopped" } service_chromium_status() { if ! have chromium && ! have chromium-browser && ! have google-chrome; then kv available 0 kv running 0 kv status unavailable kv detail "Chromium or Google Chrome is not installed" return 0 fi local details details="$(filtered_pgrep 'chromium|chrome' | tr '\n' ';' || true)" kv available 1 kv running "$([ -n "$details" ] && echo 1 || echo 0)" kv status "$([ -n "$details" ] && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "${BROWSER_REMOTE_DEBUG_PORT:-9222}" kv url "" kv detail "${details:-profile=${BROWSER_PROFILE_DIR:-$HOME/.local/share/live-browser/profile}}" } service_chromium_start() { nohup "$BROWSER_LAUNCHER" >/tmp/live-browser.log 2>&1 & disown || true kv message "Chromium launched" } service_chromium_stop() { pkill -f 'chromium|chrome' || true kv message "Chromium stopped" } service_neko_status() { if [ "${ENABLE_NEKO:-0}" != "1" ]; then kv available 0 kv running 0 kv status unavailable kv detail "在 config/system-stack.env 设置 ENABLE_NEKO=1 后执行安装脚本或在此 Start 拉取容器" return 0 fi if [ ! -f "$NEKO_COMPOSE" ]; then kv available 0 kv running 0 kv status unavailable kv detail "services/neko/docker-compose.yml missing" return 0 fi if ! have docker; then kv available 0 kv running 0 kv status unavailable kv detail "docker not installed" return 0 fi kv available 1 local state state="$(docker_container_status live-neko-chromium || true)" kv running "$(echo "$state" | grep -qi running && echo 1 || echo 0)" kv status "$(echo "$state" | grep -qi running && echo online || echo stopped)" kv host "${HOSTNAME_ALIAS}.local" kv port "$NEKO_HTTP_PORT" kv url "http://${HOSTNAME_ALIAS}.local:${NEKO_HTTP_PORT}" kv detail "m1k1o/neko Chromium in Docker; ENABLE_NEKO=1 + install_neko_browser or Modules start" } service_neko_start() { if [ "${ENABLE_NEKO:-0}" != "1" ]; then kv message "请先在 config/system-stack.env 设置 ENABLE_NEKO=1" exit 1 fi if [ ! -f "$NEKO_COMPOSE" ]; then kv message "Neko compose missing" return 0 fi local neko_dir neko_dir="$(cd "$(dirname "$NEKO_COMPOSE")" && pwd)" local prof="${NEKO_PROFILE_HOST_DIR:-$ROOT_DIR/services/neko/data/chromium-profile}" mkdir -p "$prof" chmod 777 "$prof" 2>/dev/null || true export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}" export NEKO_USER_PASS="${NEKO_USER_PASS:-live}" export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-admin}" export NEKO_PROFILE_HOST_DIR="$prof" export NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1920x1080@30}" export NEKO_WEBRTC_EPR="${NEKO_WEBRTC_EPR:-52000-52100}" export NEKO_WEBRTC_UDP_RANGE="${NEKO_WEBRTC_UDP_RANGE:-52000-52100}" export NEKO_WEBRTC_ICELITE="${NEKO_WEBRTC_ICELITE:-1}" if [ -z "${NEKO_WEBRTC_NAT1TO1:-}" ]; then export NEKO_WEBRTC_NAT1TO1="$(hostname -I 2>/dev/null | awk '{print $1}')" fi export NEKO_PLUGINS_ENABLED="${NEKO_PLUGINS_ENABLED:-true}" cmd="$(compose_bin)" || { kv message "docker compose unavailable" return 0 } ( cd "$neko_dir" || exit 1 if [ "$cmd" = "docker compose" ]; then docker compose -f docker-compose.yml up -d else docker-compose -f docker-compose.yml up -d fi ) kv message "Neko stack started" } service_neko_stop() { if [ ! -f "$NEKO_COMPOSE" ]; then kv message "Neko compose missing" return 0 fi local neko_dir neko_dir="$(cd "$(dirname "$NEKO_COMPOSE")" && pwd)" cmd="$(compose_bin)" || { kv message "docker compose unavailable" return 0 } ( cd "$neko_dir" || exit 1 if [ "$cmd" = "docker compose" ]; then docker compose -f docker-compose.yml down else docker-compose -f docker-compose.yml down fi ) kv message "Neko stack stopped" } service_android_gateway_status() { if ! have nmcli || [ -z "$ANDROID_DOWNLINK_IF" ]; then kv available 0 kv running 0 kv status unavailable kv detail "透明网关需第二块网卡:在 system-stack.env 设置 ANDROID_DOWNLINK_IF(如 eth1),并安装 NetworkManager(nmcli)" return 0 fi if [ ! -d "/sys/class/net/$ANDROID_DOWNLINK_IF" ]; then kv available 0 kv running 0 kv status unavailable kv detail "Downlink interface $ANDROID_DOWNLINK_IF is missing" return 0 fi local active active="$(nmcli -t -f NAME connection show --active 2>/dev/null | grep "^${ANDROID_GATEWAY_NAME}$" || true)" kv available 1 kv running "$([ -n "$active" ] && echo 1 || echo 0)" kv status "$([ -n "$active" ] && echo online || echo configured)" kv host "${HOSTNAME_ALIAS}.local" kv port "" kv url "" kv detail "uplink=${ANDROID_UPLINK_IF};downlink=${ANDROID_DOWNLINK_IF};gateway=${ANDROID_GATEWAY_CIDR}" } service_android_gateway_start() { if [ -z "$ANDROID_DOWNLINK_IF" ]; then kv message "ANDROID_DOWNLINK_IF is not configured" exit 1 fi if ! nmcli connection show "$ANDROID_GATEWAY_NAME" >/dev/null 2>&1; then nmcli connection add type ethernet ifname "$ANDROID_DOWNLINK_IF" con-name "$ANDROID_GATEWAY_NAME" \ ipv4.method shared ipv4.addresses "$ANDROID_GATEWAY_CIDR" connection.autoconnect yes fi nmcli connection up "$ANDROID_GATEWAY_NAME" kv message "Android gateway started" } service_android_gateway_stop() { nmcli connection down "$ANDROID_GATEWAY_NAME" >/dev/null 2>&1 || true kv message "Android gateway stopped" } dispatch() { local fn="service_${SERVICE_ID}_${ACTION}" if ! declare -F "$fn" >/dev/null 2>&1; then echo "MESSAGE=Unsupported service/action: ${SERVICE_ID}/${ACTION}" exit 1 fi "$fn" } if [ -z "$SERVICE_ID" ]; then echo "MESSAGE=Missing service id" exit 1 fi dispatch