1572 lines
51 KiB
Bash
1572 lines
51 KiB
Bash
#!/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"
|
||
MITMPROXY_COMPOSE="$ROOT_DIR/services/mitmproxy/docker-compose.yml"
|
||
VSCODE_SERVER_COMPOSE="$ROOT_DIR/services/vscode-server/docker-compose.yml"
|
||
UPTIME_KUMA_COMPOSE="$ROOT_DIR/services/uptime-kuma/docker-compose.yml"
|
||
OPENCLAW_COMPOSE="$ROOT_DIR/services/openclaw/docker-compose.yml"
|
||
BROWSER_LAUNCHER="$ROOT_DIR/scripts/linux/launch_browser.sh"
|
||
KMSCUBE_LAUNCHER="$ROOT_DIR/scripts/linux/run_kmscube.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}"
|
||
NEKO_HTTP_PORT_1="${NEKO_HTTP_PORT_1:-$NEKO_HTTP_PORT}"
|
||
NEKO_HTTP_PORT_2="${NEKO_HTTP_PORT_2:-9201}"
|
||
NEKO_HTTP_PORT_3="${NEKO_HTTP_PORT_3:-9202}"
|
||
NEKO_INSTANCE_COUNT="${NEKO_INSTANCE_COUNT:-2}"
|
||
NEKO_WEBRTC_TCPMUX="${NEKO_WEBRTC_TCPMUX:-52300}"
|
||
NEKO_WEBRTC_UDPMUX="${NEKO_WEBRTC_UDPMUX:-52300}"
|
||
NEKO_WEBRTC_TCPMUX_1="${NEKO_WEBRTC_TCPMUX_1:-$NEKO_WEBRTC_TCPMUX}"
|
||
NEKO_WEBRTC_TCPMUX_2="${NEKO_WEBRTC_TCPMUX_2:-52301}"
|
||
NEKO_WEBRTC_TCPMUX_3="${NEKO_WEBRTC_TCPMUX_3:-52302}"
|
||
NEKO_WEBRTC_UDPMUX_1="${NEKO_WEBRTC_UDPMUX_1:-$NEKO_WEBRTC_UDPMUX}"
|
||
NEKO_WEBRTC_UDPMUX_2="${NEKO_WEBRTC_UDPMUX_2:-52301}"
|
||
NEKO_WEBRTC_UDPMUX_3="${NEKO_WEBRTC_UDPMUX_3:-52302}"
|
||
NEKO_WEBRTC_UDP_RANGE="${NEKO_WEBRTC_UDP_RANGE:-52000-52099}"
|
||
NEKO_WEBRTC_UDP_RANGE_1="${NEKO_WEBRTC_UDP_RANGE_1:-$NEKO_WEBRTC_UDP_RANGE}"
|
||
NEKO_WEBRTC_UDP_RANGE_2="${NEKO_WEBRTC_UDP_RANGE_2:-52101-52199}"
|
||
NEKO_WEBRTC_UDP_RANGE_3="${NEKO_WEBRTC_UDP_RANGE_3:-52201-52299}"
|
||
NEKO_WEBRTC_EPR="${NEKO_WEBRTC_EPR:-$NEKO_WEBRTC_UDP_RANGE_1}"
|
||
NEKO_WEBRTC_EPR_1="${NEKO_WEBRTC_EPR_1:-$NEKO_WEBRTC_EPR}"
|
||
NEKO_WEBRTC_EPR_2="${NEKO_WEBRTC_EPR_2:-$NEKO_WEBRTC_UDP_RANGE_2}"
|
||
NEKO_WEBRTC_EPR_3="${NEKO_WEBRTC_EPR_3:-$NEKO_WEBRTC_UDP_RANGE_3}"
|
||
NEKO_START_URL="${NEKO_START_URL:-https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com/}"
|
||
NEKO_IMAGE_X86="${NEKO_IMAGE_X86:-ghcr.io/m1k1o/neko/google-chrome:3.0.0}"
|
||
NEKO_IMAGE_ARM64="${NEKO_IMAGE_ARM64:-ghcr.io/m1k1o/neko/chromium:3.0.0}"
|
||
case "$NEKO_IMAGE_X86" in
|
||
ghcr.io/m1k1o/neko/google-chrome:latest) NEKO_IMAGE_X86="ghcr.io/m1k1o/neko/google-chrome:3.0.0" ;;
|
||
esac
|
||
case "$NEKO_IMAGE_ARM64" in
|
||
ghcr.io/m1k1o/neko/chromium:latest) NEKO_IMAGE_ARM64="ghcr.io/m1k1o/neko/chromium:3.0.0" ;;
|
||
esac
|
||
NEKO_SHM_SIZE="${NEKO_SHM_SIZE:-512mb}"
|
||
NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"
|
||
NEKO_MAX_FPS="${NEKO_MAX_FPS:-15}"
|
||
NEKO_VIDEO_BITRATE="${NEKO_VIDEO_BITRATE:-1200}"
|
||
NEKO_BROWSER_RENDER_MODE="${NEKO_BROWSER_RENDER_MODE:-auto}"
|
||
ENABLE_NEKO="${ENABLE_NEKO:-1}"
|
||
ENABLE_MITMPROXY="${ENABLE_MITMPROXY:-1}"
|
||
MITMPROXY_WEB_PORT="${MITMPROXY_WEB_PORT:-8086}"
|
||
MITMPROXY_PROXY_PORT="${MITMPROXY_PROXY_PORT:-8087}"
|
||
ENABLE_VSCODE_SERVER="${ENABLE_VSCODE_SERVER:-1}"
|
||
VSCODE_SERVER_PORT="${VSCODE_SERVER_PORT:-8440}"
|
||
VSCODE_SERVER_PASSWORD="${VSCODE_SERVER_PASSWORD:-12345678}"
|
||
ENABLE_UPTIME_KUMA="${ENABLE_UPTIME_KUMA:-1}"
|
||
UPTIME_KUMA_PORT="${UPTIME_KUMA_PORT:-3001}"
|
||
ENABLE_PULSEAUDIO="${ENABLE_PULSEAUDIO:-1}"
|
||
PULSEAUDIO_TCP_PORT="${PULSEAUDIO_TCP_PORT:-4713}"
|
||
ENABLE_KMSCUBE="${ENABLE_KMSCUBE:-1}"
|
||
KMSCUBE_DRM_DEVICE="${KMSCUBE_DRM_DEVICE:-/dev/dri/card0}"
|
||
ENABLE_OPENCLAW="${ENABLE_OPENCLAW:-1}"
|
||
OPENCLAW_IMAGE="${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:latest}"
|
||
OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
|
||
OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
|
||
OPENCLAW_GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-12345678}"
|
||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-1}"
|
||
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-/var/lib/live-openclaw/config}"
|
||
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-/var/lib/live-openclaw/workspace}"
|
||
ENABLE_REDROID="${ENABLE_REDROID:-1}"
|
||
REDROID_DEFAULT_PORT="${REDROID_DEFAULT_PORT:-5555}"
|
||
REDROID_INSTANCE_PREFIX="${REDROID_INSTANCE_PREFIX:-redroid13}"
|
||
REDROID_DATA_PREFIX="${REDROID_DATA_PREFIX:-/var/lib/live-redroid}"
|
||
REDROID_GPU_MODE="${REDROID_GPU_MODE:-guest}"
|
||
REDROID_DEVICE_PROFILE="${REDROID_DEVICE_PROFILE:-pixel-7-pro}"
|
||
REDROID_BOOT_ARGS="${REDROID_BOOT_ARGS:-androidboot.redroid_magisk=1}"
|
||
REDROID_EXPECT_GMS="${REDROID_EXPECT_GMS:-1}"
|
||
REDROID_EXPECT_MAGISK="${REDROID_EXPECT_MAGISK:-1}"
|
||
REDROID_IMAGE_X86="${REDROID_IMAGE_X86:-fahaddz/redroid:13}"
|
||
REDROID_IMAGE_X86_ALT="${REDROID_IMAGE_X86_ALT:-fahaddz/redroid:13}"
|
||
REDROID_IMAGE_ARM64="${REDROID_IMAGE_ARM64:-fahaddz/redroid:13-arm-pi5}"
|
||
REDROID_IMAGE_ARM64_ALT="${REDROID_IMAGE_ARM64_ALT:-fahaddz/redroid:13-arm-pi5}"
|
||
if [ "$REDROID_IMAGE_ARM64" = "cnflysky/redroid-rk3588:lineage-20" ]; then
|
||
REDROID_IMAGE_ARM64="fahaddz/redroid:13-arm-pi5"
|
||
fi
|
||
if [ "$REDROID_IMAGE_ARM64_ALT" = "cnflysky/redroid-rk3588:lineage-20" ]; then
|
||
REDROID_IMAGE_ARM64_ALT="fahaddz/redroid:13-arm-pi5"
|
||
fi
|
||
if [ "${REDROID_IMAGE:-}" = "cnflysky/redroid-rk3588:lineage-20" ]; then
|
||
REDROID_IMAGE="fahaddz/redroid:13-arm-pi5"
|
||
fi
|
||
case " ${REDROID_BOOT_ARGS} " in
|
||
*" androidboot.redroid_gpu_mode="*)
|
||
;;
|
||
*)
|
||
REDROID_BOOT_ARGS="${REDROID_BOOT_ARGS} androidboot.redroid_gpu_mode=${REDROID_GPU_MODE:-guest}"
|
||
;;
|
||
esac
|
||
CONSOLE_BASE_URL="http://${HOSTNAME_ALIAS}.local:${PORT}"
|
||
|
||
kv() {
|
||
printf '%s=%s\n' "$1" "$2"
|
||
}
|
||
|
||
have() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
allow_tcp_port_if_firewall_active() {
|
||
local port="$1"
|
||
if have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||
ufw allow "${port}/tcp" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
allow_udp_port_if_firewall_active() {
|
||
local port="$1"
|
||
if have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||
ufw allow "${port}/udp" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
allow_udp_range_if_firewall_active() {
|
||
local port_range="$1"
|
||
local ufw_range="${port_range//-/:}"
|
||
if have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||
ufw allow "${ufw_range}/udp" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
prune_neko_profile() {
|
||
local prof="$1"
|
||
[ -n "$prof" ] || return 0
|
||
mkdir -p "$prof"
|
||
rm -rf \
|
||
"$prof/SingletonLock" \
|
||
"$prof/SingletonSocket" \
|
||
"$prof/SingletonCookie" \
|
||
"$prof/SingletonStartupLock" \
|
||
"$prof/lockfile" \
|
||
"$prof/DevToolsActivePort" \
|
||
"$prof/BrowserMetrics" \
|
||
"$prof/Crash Reports" \
|
||
"$prof/Crashpad" \
|
||
"$prof/ShaderCache" \
|
||
"$prof/GrShaderCache" \
|
||
"$prof/GraphiteDawnCache" \
|
||
"$prof/Default/Cache" \
|
||
"$prof/Default/Code Cache" \
|
||
"$prof/Default/GPUCache" \
|
||
"$prof/Default/DawnGraphiteCache" \
|
||
"$prof/Default/DawnWebGPUCache" \
|
||
2>/dev/null || true
|
||
chmod -R 0777 "$prof" 2>/dev/null || true
|
||
find "$prof" -type d -exec chmod 0777 {} \; 2>/dev/null || true
|
||
find "$prof" -type f -exec chmod 0666 {} \; 2>/dev/null || true
|
||
}
|
||
|
||
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-compose v1 将 ./卷 解析为当前工作目录,必须在 compose 文件所在目录执行(与 Neko 一致)
|
||
compose_in_service_dir() {
|
||
local compose_file="$1"
|
||
shift
|
||
local d base
|
||
d="$(cd "$(dirname "$compose_file")" && pwd)"
|
||
base="$(basename "$compose_file")"
|
||
(
|
||
cd "$d" || exit 1
|
||
cmd="$(compose_bin)" || exit 1
|
||
if [ "$cmd" = "docker compose" ]; then
|
||
docker compose -f "$base" "$@"
|
||
else
|
||
docker-compose -f "$base" "$@"
|
||
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
|
||
}
|
||
|
||
resolve_neko_image() {
|
||
case "$(uname -m)" in
|
||
x86_64|amd64)
|
||
printf '%s' "$NEKO_IMAGE_X86"
|
||
;;
|
||
aarch64|arm64)
|
||
printf '%s' "$NEKO_IMAGE_ARM64"
|
||
;;
|
||
*)
|
||
printf '%s' "$NEKO_IMAGE_ARM64"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
resolve_neko_supervisor_conf() {
|
||
case "$(uname -m)" in
|
||
x86_64|amd64)
|
||
printf '%s' "google-chrome.conf"
|
||
;;
|
||
aarch64|arm64)
|
||
printf '%s' "chromium.conf"
|
||
;;
|
||
*)
|
||
printf '%s' "chromium.conf"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
resolve_neko_profile_dir() {
|
||
local family
|
||
family="$(resolve_neko_supervisor_conf)"
|
||
family="${family%.conf}"
|
||
printf '%s' "$ROOT_DIR/services/neko/data/${family}-profile-v2"
|
||
}
|
||
|
||
resolve_neko_profile_dir_for_instance() {
|
||
local idx="${1:-1}"
|
||
local base
|
||
base="${NEKO_PROFILE_HOST_DIR:-$(resolve_neko_profile_dir)}"
|
||
case "$idx" in
|
||
1) printf '%s' "$base" ;;
|
||
*) printf '%s-%s' "$base" "$idx" ;;
|
||
esac
|
||
}
|
||
|
||
resolve_neko_instance_count() {
|
||
local requested="${NEKO_INSTANCE_COUNT:-2}"
|
||
local cpu count
|
||
cpu="$(nproc 2>/dev/null || echo 4)"
|
||
case "$requested" in
|
||
1|2|3) count="$requested" ;;
|
||
auto|"")
|
||
if [ "$cpu" -le 4 ]; then
|
||
count=2
|
||
else
|
||
count=3
|
||
fi
|
||
;;
|
||
*)
|
||
count=2
|
||
;;
|
||
esac
|
||
if [ "$count" -lt 1 ]; then
|
||
count=1
|
||
fi
|
||
if [ "$count" -gt 3 ]; then
|
||
count=3
|
||
fi
|
||
printf '%s' "$count"
|
||
}
|
||
|
||
remove_legacy_neko_container() {
|
||
if have docker && docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx 'live-neko-chromium'; then
|
||
docker stop live-neko-chromium >/dev/null 2>&1 || true
|
||
docker rm live-neko-chromium >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
detect_preferred_lan_ip() {
|
||
local src iface candidate path name
|
||
|
||
src="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="src") {print $(i+1); exit}}')"
|
||
if [ -n "$src" ]; then
|
||
printf '%s\n' "$src"
|
||
return 0
|
||
fi
|
||
|
||
iface="$(ip route show default 2>/dev/null | awk 'NR==1 {print $5}')"
|
||
if [ -n "$iface" ]; then
|
||
src="$(ip -4 addr show dev "$iface" 2>/dev/null | awk '$1=="inet" {sub(/\/.*/, "", $2); print $2; exit}')"
|
||
if [ -n "$src" ]; then
|
||
printf '%s\n' "$src"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
for path in /sys/class/net/*; do
|
||
[ -e "$path" ] || continue
|
||
name="${path##*/}"
|
||
case "$name" in
|
||
lo|docker*|br-*|veth*|virbr*|tun*|tap*|zt*|tailscale*|wg*|sit*|dummy*|p2p-dev-*|mon.*)
|
||
continue
|
||
;;
|
||
esac
|
||
candidate="$(ip -4 addr show dev "$name" 2>/dev/null | awk '$1=="inet" {sub(/\/.*/, "", $2); print $2; exit}')"
|
||
if [ -n "$candidate" ]; then
|
||
printf '%s\n' "$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.' | grep -Ev '^(127\.|169\.254\.)' | head -n 1
|
||
}
|
||
|
||
sync_mdns_hosts_entry() {
|
||
local preferred_lan_ip
|
||
preferred_lan_ip="$(detect_preferred_lan_ip || true)"
|
||
python3 - <<PY
|
||
from pathlib import Path
|
||
|
||
hosts = Path("/etc/hosts")
|
||
host_alias = "${HOSTNAME_ALIAS}"
|
||
preferred_lan_ip = "${preferred_lan_ip}".strip()
|
||
host_lines = hosts.read_text(encoding="utf-8").splitlines()
|
||
next_host_lines = []
|
||
loopback_written = False
|
||
lan_written = False
|
||
|
||
for raw in host_lines:
|
||
stripped = raw.strip()
|
||
if not stripped or stripped.startswith("#"):
|
||
next_host_lines.append(raw)
|
||
continue
|
||
parts = raw.split()
|
||
names = parts[1:]
|
||
if parts[0] == "127.0.1.1":
|
||
next_host_lines.append(f"127.0.1.1\t{host_alias}")
|
||
loopback_written = True
|
||
continue
|
||
if f"{host_alias}.local" in names:
|
||
if preferred_lan_ip and not lan_written:
|
||
next_host_lines.append(f"{preferred_lan_ip}\t{host_alias}.local")
|
||
lan_written = True
|
||
continue
|
||
next_host_lines.append(raw)
|
||
|
||
if not loopback_written:
|
||
next_host_lines.append(f"127.0.1.1\t{host_alias}")
|
||
if preferred_lan_ip and not lan_written:
|
||
next_host_lines.append(f"{preferred_lan_ip}\t{host_alias}.local")
|
||
|
||
hosts.write_text("\\n".join(next_host_lines) + "\\n", encoding="utf-8")
|
||
PY
|
||
}
|
||
|
||
neko_nat1to1_is_usable() {
|
||
local candidate="$1"
|
||
local iface
|
||
[ -n "$candidate" ] || return 1
|
||
iface="$(ip -4 addr show 2>/dev/null | awk -v want="$candidate" '
|
||
/^[0-9]+: / {
|
||
iface=$2
|
||
sub(/:$/, "", iface)
|
||
}
|
||
$1=="inet" {
|
||
ip=$2
|
||
sub(/\/.*/, "", ip)
|
||
if (ip==want) {
|
||
print iface
|
||
exit
|
||
}
|
||
}
|
||
')"
|
||
case "$iface" in
|
||
""|lo|docker*|br-*|veth*|virbr*|tun*|tap*|zt*|tailscale*|wg*|sit*|dummy*)
|
||
return 1
|
||
;;
|
||
*)
|
||
return 0
|
||
;;
|
||
esac
|
||
}
|
||
|
||
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"
|
||
sync_mdns_hosts_entry
|
||
systemctl restart avahi-daemon
|
||
kv message "mDNS hostname refreshed"
|
||
}
|
||
|
||
service_mdns_stop() {
|
||
systemctl stop avahi-daemon
|
||
kv message "avahi-daemon stopped"
|
||
}
|
||
|
||
wifi_detect_iface() {
|
||
local iface path name
|
||
if have iw; then
|
||
while read -r iface; do
|
||
[ -n "$iface" ] || continue
|
||
case "$iface" in
|
||
p2p-dev-*|mon.*) continue ;;
|
||
esac
|
||
printf '%s\n' "$iface"
|
||
return 0
|
||
done < <(iw dev 2>/dev/null | awk '$1=="Interface"{print $2}')
|
||
fi
|
||
for path in /sys/class/net/*; do
|
||
[ -e "$path" ] || continue
|
||
name="${path##*/}"
|
||
case "$name" in
|
||
lo|docker*|br-*|veth*|virbr*|tun*|tap*|p2p-dev-*|mon.*) continue ;;
|
||
esac
|
||
if [ -d "$path/wireless" ]; then
|
||
printf '%s\n' "$name"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
service_wifi_status() {
|
||
if ! have nmcli; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "NetworkManager (nmcli) is not available"
|
||
return 0
|
||
fi
|
||
local active detail iface profile_exists running status
|
||
iface="$(wifi_detect_iface || true)"
|
||
active="$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | awk -F: -v want="$WIFI_CONNECTION_NAME" '$1==want{print $2; exit}')"
|
||
profile_exists="$(nmcli -t -f NAME connection show 2>/dev/null | grep -Fx "$WIFI_CONNECTION_NAME" || true)"
|
||
detail="ssid=${WIFI_SSID};iface=${iface:-none};profile=$([ -n "$profile_exists" ] && echo present || echo missing)"
|
||
if [ -z "$iface" ]; then
|
||
detail="${detail};warning=no_wireless_interface_detected"
|
||
fi
|
||
if [ -n "$active" ]; then
|
||
running=1
|
||
status=online
|
||
detail="${detail};active_device=${active}"
|
||
elif [ -n "$profile_exists" ]; then
|
||
running=0
|
||
status=configured
|
||
else
|
||
running=0
|
||
status=provisioning
|
||
fi
|
||
kv available 1
|
||
kv running "$running"
|
||
kv status "$status"
|
||
kv host "${HOSTNAME_ALIAS}.local"
|
||
kv port ""
|
||
kv url ""
|
||
kv detail "$detail"
|
||
}
|
||
|
||
service_wifi_start() {
|
||
if ! have nmcli; then
|
||
kv message "NetworkManager (nmcli) is not available"
|
||
exit 1
|
||
fi
|
||
local iface ifname
|
||
iface="$(wifi_detect_iface || true)"
|
||
ifname="${iface:-*}"
|
||
if ! nmcli connection show "$WIFI_CONNECTION_NAME" >/dev/null 2>&1; then
|
||
nmcli connection add type wifi ifname "$ifname" 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
|
||
else
|
||
nmcli connection modify "$WIFI_CONNECTION_NAME" \
|
||
connection.interface-name "$ifname" \
|
||
802-11-wireless.ssid "$WIFI_SSID" \
|
||
wifi-sec.key-mgmt wpa-psk \
|
||
wifi-sec.psk "${WIFI_PSK:-12345678}" \
|
||
connection.autoconnect yes \
|
||
ipv4.method auto >/dev/null 2>&1 || true
|
||
fi
|
||
nmcli connection up "$WIFI_CONNECTION_NAME" >/dev/null 2>&1 || true
|
||
kv message "Wi-Fi profile ensured (iface=${iface:-*})"
|
||
}
|
||
|
||
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_in_service_dir "$SRS_COMPOSE" up -d
|
||
kv message "SRS started"
|
||
}
|
||
|
||
service_srs_stop() {
|
||
compose_in_service_dir "$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_in_service_dir "$FILEBROWSER_COMPOSE" up -d
|
||
kv message "File Browser started"
|
||
}
|
||
|
||
service_filebrowser_stop() {
|
||
compose_in_service_dir "$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_in_service_dir "$HOMEPAGE_COMPOSE" up -d
|
||
kv message "Homepage started"
|
||
}
|
||
|
||
service_homepage_stop() {
|
||
compose_in_service_dir "$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=${CONSOLE_BASE_URL}/android;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() {
|
||
if [ -z "${DISPLAY:-}" ] && command -v xvfb-run >/dev/null 2>&1; then
|
||
nohup xvfb-run -a "$BROWSER_LAUNCHER" >/tmp/live-browser.log 2>&1 &
|
||
else
|
||
[ -z "${DISPLAY:-}" ] && export DISPLAY=:0
|
||
nohup "$BROWSER_LAUNCHER" >/tmp/live-browser.log 2>&1 &
|
||
fi
|
||
disown || true
|
||
kv message "Chromium launched"
|
||
}
|
||
|
||
service_chromium_stop() {
|
||
pkill -f 'chromium|chrome' || true
|
||
kv message "Chromium stopped"
|
||
}
|
||
|
||
service_neko_status() {
|
||
if [ "${ENABLE_NEKO:-1}" != "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
|
||
local running_count=0 total_count=0 states="" name instance_count
|
||
instance_count="$(resolve_neko_instance_count)"
|
||
total_count="$instance_count"
|
||
for name in live-neko-1 live-neko-2 live-neko-3; do
|
||
case "$name" in
|
||
live-neko-1) [ "$instance_count" -ge 1 ] || continue ;;
|
||
live-neko-2) [ "$instance_count" -ge 2 ] || continue ;;
|
||
live-neko-3) [ "$instance_count" -ge 3 ] || continue ;;
|
||
esac
|
||
state="$(docker_container_status "$name" || true)"
|
||
if echo "$state" | grep -qi running; then
|
||
running_count=$((running_count + 1))
|
||
fi
|
||
states="${states}${name}:${state:-missing};"
|
||
done
|
||
if [ "$running_count" -eq 0 ]; then
|
||
state="$(docker_container_status live-neko-chromium || true)"
|
||
if echo "$state" | grep -qi running; then
|
||
running_count=1
|
||
total_count=1
|
||
states="live-neko-chromium:${state};"
|
||
fi
|
||
fi
|
||
kv running "$([ "$running_count" -ge 1 ] && echo 1 || echo 0)"
|
||
kv status "$([ "$running_count" -ge "$total_count" ] && echo online || ([ "$running_count" -gt 0 ] && echo degraded || echo stopped))"
|
||
kv host "${HOSTNAME_ALIAS}.local"
|
||
kv port "$NEKO_HTTP_PORT_1"
|
||
kv url "http://${HOSTNAME_ALIAS}.local:${NEKO_HTTP_PORT_1}"
|
||
if ! neko_nat1to1_is_usable "${NEKO_WEBRTC_NAT1TO1:-}"; then
|
||
NEKO_WEBRTC_NAT1TO1="$(detect_preferred_lan_ip || true)"
|
||
fi
|
||
kv detail "image=$(resolve_neko_image); supervisor_conf=$(resolve_neko_supervisor_conf); start_url=${NEKO_START_URL}; nat1to1=${NEKO_WEBRTC_NAT1TO1:-unset}; instances=${running_count}/${total_count}; ports=${NEKO_HTTP_PORT_1},${NEKO_HTTP_PORT_2},${NEKO_HTTP_PORT_3}; profiles=$(resolve_neko_profile_dir_for_instance 1),$(resolve_neko_profile_dir_for_instance 2),$(resolve_neko_profile_dir_for_instance 3); screen=${NEKO_DESKTOP_SCREEN}; shm=${NEKO_SHM_SIZE}; fps=${NEKO_MAX_FPS}; bitrate=${NEKO_VIDEO_BITRATE}; render=${NEKO_BROWSER_RENDER_MODE}; udp_ranges=${NEKO_WEBRTC_UDP_RANGE_1},${NEKO_WEBRTC_UDP_RANGE_2},${NEKO_WEBRTC_UDP_RANGE_3}; tcpmux=${NEKO_WEBRTC_TCPMUX_1},${NEKO_WEBRTC_TCPMUX_2},${NEKO_WEBRTC_TCPMUX_3}; udpmux=${NEKO_WEBRTC_UDPMUX_1},${NEKO_WEBRTC_UDPMUX_2},${NEKO_WEBRTC_UDPMUX_3}; containers=${states}"
|
||
}
|
||
|
||
service_neko_start() {
|
||
if [ "${ENABLE_NEKO:-1}" != "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 prof
|
||
local instance_count
|
||
local service_names="" extra_service_names="" idx_name
|
||
instance_count="$(resolve_neko_instance_count)"
|
||
for idx_name in 1 2 3; do
|
||
case "$idx_name" in
|
||
1)
|
||
if [ "$instance_count" -ge 1 ]; then
|
||
service_names="${service_names} neko1"
|
||
else
|
||
extra_service_names="${extra_service_names} neko1"
|
||
fi
|
||
;;
|
||
2)
|
||
if [ "$instance_count" -ge 2 ]; then
|
||
service_names="${service_names} neko2"
|
||
else
|
||
extra_service_names="${extra_service_names} neko2"
|
||
fi
|
||
;;
|
||
3)
|
||
if [ "$instance_count" -ge 3 ]; then
|
||
service_names="${service_names} neko3"
|
||
else
|
||
extra_service_names="${extra_service_names} neko3"
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
for idx in 1 2 3; do
|
||
[ "$idx" -le "$instance_count" ] || break
|
||
prof="$(resolve_neko_profile_dir_for_instance "$idx")"
|
||
prune_neko_profile "$prof"
|
||
done
|
||
export_neko_env
|
||
for port in "$NEKO_HTTP_PORT_1" "$NEKO_HTTP_PORT_2" "$NEKO_HTTP_PORT_3" "$NEKO_WEBRTC_TCPMUX_1" "$NEKO_WEBRTC_TCPMUX_2" "$NEKO_WEBRTC_TCPMUX_3"; do
|
||
case "$port" in
|
||
"$NEKO_HTTP_PORT_1"|"$NEKO_WEBRTC_TCPMUX_1") [ "$instance_count" -ge 1 ] || continue ;;
|
||
"$NEKO_HTTP_PORT_2"|"$NEKO_WEBRTC_TCPMUX_2") [ "$instance_count" -ge 2 ] || continue ;;
|
||
"$NEKO_HTTP_PORT_3"|"$NEKO_WEBRTC_TCPMUX_3") [ "$instance_count" -ge 3 ] || continue ;;
|
||
esac
|
||
allow_tcp_port_if_firewall_active "$port"
|
||
done
|
||
for port in "$NEKO_WEBRTC_UDPMUX_1" "$NEKO_WEBRTC_UDPMUX_2" "$NEKO_WEBRTC_UDPMUX_3"; do
|
||
case "$port" in
|
||
"$NEKO_WEBRTC_UDPMUX_1") [ "$instance_count" -ge 1 ] || continue ;;
|
||
"$NEKO_WEBRTC_UDPMUX_2") [ "$instance_count" -ge 2 ] || continue ;;
|
||
"$NEKO_WEBRTC_UDPMUX_3") [ "$instance_count" -ge 3 ] || continue ;;
|
||
esac
|
||
allow_udp_port_if_firewall_active "$port"
|
||
done
|
||
for port_range in "$NEKO_WEBRTC_UDP_RANGE_1" "$NEKO_WEBRTC_UDP_RANGE_2" "$NEKO_WEBRTC_UDP_RANGE_3"; do
|
||
case "$port_range" in
|
||
"$NEKO_WEBRTC_UDP_RANGE_1") [ "$instance_count" -ge 1 ] || continue ;;
|
||
"$NEKO_WEBRTC_UDP_RANGE_2") [ "$instance_count" -ge 2 ] || continue ;;
|
||
"$NEKO_WEBRTC_UDP_RANGE_3") [ "$instance_count" -ge 3 ] || continue ;;
|
||
esac
|
||
allow_udp_range_if_firewall_active "$port_range"
|
||
done
|
||
remove_legacy_neko_container
|
||
# 只启动实际需要的实例,避免多余的 Chrome/Xorg 进程继续吃 CPU
|
||
# shellcheck disable=SC2086
|
||
compose_in_service_dir "$NEKO_COMPOSE" up -d $service_names
|
||
if [ -n "${extra_service_names# }" ]; then
|
||
# shellcheck disable=SC2086
|
||
compose_in_service_dir "$NEKO_COMPOSE" stop $extra_service_names >/dev/null 2>&1 || true
|
||
fi
|
||
kv message "Neko stack started (${instance_count} browser instances)"
|
||
}
|
||
|
||
export_neko_env() {
|
||
export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}"
|
||
export NEKO_HTTP_PORT_1="${NEKO_HTTP_PORT_1:-$NEKO_HTTP_PORT}"
|
||
export NEKO_HTTP_PORT_2="${NEKO_HTTP_PORT_2:-9201}"
|
||
export NEKO_HTTP_PORT_3="${NEKO_HTTP_PORT_3:-9202}"
|
||
export NEKO_INSTANCE_COUNT="${NEKO_INSTANCE_COUNT:-2}"
|
||
export NEKO_USER_PASS="${NEKO_USER_PASS:-12345678}"
|
||
export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-12345678}"
|
||
export NEKO_PROFILE_HOST_DIR="${NEKO_PROFILE_HOST_DIR:-$(resolve_neko_profile_dir)}"
|
||
export NEKO_PROFILE_HOST_DIR_1="${NEKO_PROFILE_HOST_DIR_1:-$(resolve_neko_profile_dir_for_instance 1)}"
|
||
export NEKO_PROFILE_HOST_DIR_2="${NEKO_PROFILE_HOST_DIR_2:-$(resolve_neko_profile_dir_for_instance 2)}"
|
||
export NEKO_PROFILE_HOST_DIR_3="${NEKO_PROFILE_HOST_DIR_3:-$(resolve_neko_profile_dir_for_instance 3)}"
|
||
export NEKO_START_URL="${NEKO_START_URL:-https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com/}"
|
||
export NEKO_IMAGE="$(resolve_neko_image)"
|
||
export NEKO_BROWSER_SUPERVISOR_CONF="$(resolve_neko_supervisor_conf)"
|
||
export NEKO_SHM_SIZE="${NEKO_SHM_SIZE:-512mb}"
|
||
export NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"
|
||
export NEKO_MAX_FPS="${NEKO_MAX_FPS:-15}"
|
||
export NEKO_VIDEO_BITRATE="${NEKO_VIDEO_BITRATE:-1200}"
|
||
export NEKO_BROWSER_RENDER_MODE="${NEKO_BROWSER_RENDER_MODE:-auto}"
|
||
export NEKO_WEBRTC_UDP_RANGE="${NEKO_WEBRTC_UDP_RANGE:-52000-52099}"
|
||
export NEKO_WEBRTC_UDP_RANGE_1="${NEKO_WEBRTC_UDP_RANGE_1:-$NEKO_WEBRTC_UDP_RANGE}"
|
||
export NEKO_WEBRTC_UDP_RANGE_2="${NEKO_WEBRTC_UDP_RANGE_2:-52101-52199}"
|
||
export NEKO_WEBRTC_UDP_RANGE_3="${NEKO_WEBRTC_UDP_RANGE_3:-52201-52299}"
|
||
export NEKO_WEBRTC_EPR="${NEKO_WEBRTC_EPR:-$NEKO_WEBRTC_UDP_RANGE_1}"
|
||
export NEKO_WEBRTC_EPR_1="${NEKO_WEBRTC_EPR_1:-$NEKO_WEBRTC_EPR}"
|
||
export NEKO_WEBRTC_EPR_2="${NEKO_WEBRTC_EPR_2:-$NEKO_WEBRTC_UDP_RANGE_2}"
|
||
export NEKO_WEBRTC_EPR_3="${NEKO_WEBRTC_EPR_3:-$NEKO_WEBRTC_UDP_RANGE_3}"
|
||
# Upstream n.eko images include STUN servers by default, and ICE Lite with ICE servers is unstable.
|
||
# Keep full ICE on by default even if an older system-stack.env still contains NEKO_WEBRTC_ICELITE=1.
|
||
if [ "${NEKO_FORCE_ICELITE:-0}" = "1" ]; then
|
||
export NEKO_WEBRTC_ICELITE="${NEKO_WEBRTC_ICELITE:-1}"
|
||
else
|
||
export NEKO_WEBRTC_ICELITE=0
|
||
fi
|
||
export NEKO_WEBRTC_TCPMUX="${NEKO_WEBRTC_TCPMUX:-52300}"
|
||
export NEKO_WEBRTC_UDPMUX="${NEKO_WEBRTC_UDPMUX:-52300}"
|
||
export NEKO_WEBRTC_TCPMUX_1="${NEKO_WEBRTC_TCPMUX_1:-$NEKO_WEBRTC_TCPMUX}"
|
||
export NEKO_WEBRTC_TCPMUX_2="${NEKO_WEBRTC_TCPMUX_2:-52301}"
|
||
export NEKO_WEBRTC_TCPMUX_3="${NEKO_WEBRTC_TCPMUX_3:-52302}"
|
||
export NEKO_WEBRTC_UDPMUX_1="${NEKO_WEBRTC_UDPMUX_1:-$NEKO_WEBRTC_UDPMUX}"
|
||
export NEKO_WEBRTC_UDPMUX_2="${NEKO_WEBRTC_UDPMUX_2:-52301}"
|
||
export NEKO_WEBRTC_UDPMUX_3="${NEKO_WEBRTC_UDPMUX_3:-52302}"
|
||
if ! neko_nat1to1_is_usable "${NEKO_WEBRTC_NAT1TO1:-}"; then
|
||
export NEKO_WEBRTC_NAT1TO1="$(detect_preferred_lan_ip || true)"
|
||
else
|
||
export NEKO_WEBRTC_NAT1TO1
|
||
fi
|
||
export NEKO_PLUGINS_ENABLED="${NEKO_PLUGINS_ENABLED:-true}"
|
||
}
|
||
|
||
service_neko_stop() {
|
||
if [ ! -f "$NEKO_COMPOSE" ]; then
|
||
kv message "Neko compose missing"
|
||
return 0
|
||
fi
|
||
export_neko_env
|
||
compose_in_service_dir "$NEKO_COMPOSE" down
|
||
kv message "Neko stack stopped"
|
||
}
|
||
|
||
service_mitmproxy_status() {
|
||
if [ "${ENABLE_MITMPROXY:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_MITMPROXY=1 in config/system-stack.env to enable mitmproxy"
|
||
return 0
|
||
fi
|
||
if ! have docker || [ ! -f "$MITMPROXY_COMPOSE" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Docker or mitmproxy compose file is missing"
|
||
return 0
|
||
fi
|
||
local state
|
||
state="$(docker_container_status live-mitmproxy || 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 "$MITMPROXY_WEB_PORT"
|
||
kv url "http://${HOSTNAME_ALIAS}.local:${MITMPROXY_WEB_PORT}"
|
||
kv detail "${state:-container=missing};proxy=${MITMPROXY_PROXY_PORT}"
|
||
}
|
||
|
||
service_mitmproxy_start() {
|
||
compose_in_service_dir "$MITMPROXY_COMPOSE" up -d
|
||
kv message "mitmproxy started"
|
||
}
|
||
|
||
service_mitmproxy_stop() {
|
||
compose_in_service_dir "$MITMPROXY_COMPOSE" down
|
||
kv message "mitmproxy stopped"
|
||
}
|
||
|
||
service_vscode_server_status() {
|
||
if [ "${ENABLE_VSCODE_SERVER:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_VSCODE_SERVER=1 in config/system-stack.env to enable code-server"
|
||
return 0
|
||
fi
|
||
if ! have docker || [ ! -f "$VSCODE_SERVER_COMPOSE" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Docker or code-server compose file is missing"
|
||
return 0
|
||
fi
|
||
local state
|
||
state="$(docker_container_status live-code-server || 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 "$VSCODE_SERVER_PORT"
|
||
kv url "http://${HOSTNAME_ALIAS}.local:${VSCODE_SERVER_PORT}"
|
||
kv detail "${state:-container=missing};password=${VSCODE_SERVER_PASSWORD}"
|
||
}
|
||
|
||
service_vscode_server_start() {
|
||
compose_in_service_dir "$VSCODE_SERVER_COMPOSE" up -d
|
||
kv message "code-server started"
|
||
}
|
||
|
||
service_vscode_server_stop() {
|
||
compose_in_service_dir "$VSCODE_SERVER_COMPOSE" down
|
||
kv message "code-server stopped"
|
||
}
|
||
|
||
service_uptime_kuma_status() {
|
||
if [ "${ENABLE_UPTIME_KUMA:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_UPTIME_KUMA=1 in config/system-stack.env to enable Uptime Kuma"
|
||
return 0
|
||
fi
|
||
if ! have docker || [ ! -f "$UPTIME_KUMA_COMPOSE" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Docker or Uptime Kuma compose file is missing"
|
||
return 0
|
||
fi
|
||
local state
|
||
state="$(docker_container_status live-uptime-kuma || 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 "$UPTIME_KUMA_PORT"
|
||
kv url "http://${HOSTNAME_ALIAS}.local:${UPTIME_KUMA_PORT}"
|
||
kv detail "${state:-container=missing}"
|
||
}
|
||
|
||
service_uptime_kuma_start() {
|
||
compose_in_service_dir "$UPTIME_KUMA_COMPOSE" up -d
|
||
kv message "Uptime Kuma started"
|
||
}
|
||
|
||
service_uptime_kuma_stop() {
|
||
compose_in_service_dir "$UPTIME_KUMA_COMPOSE" down
|
||
kv message "Uptime Kuma stopped"
|
||
}
|
||
|
||
service_pulseaudio_status() {
|
||
if [ "${ENABLE_PULSEAUDIO:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_PULSEAUDIO=1 in config/system-stack.env to enable PulseAudio"
|
||
return 0
|
||
fi
|
||
if ! unit_exists live-pulseaudio.service && ! have pulseaudio; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "PulseAudio is not installed"
|
||
return 0
|
||
fi
|
||
kv available 1
|
||
kv running "$(unit_active live-pulseaudio.service && echo 1 || echo 0)"
|
||
kv status "$(unit_active live-pulseaudio.service && echo online || echo stopped)"
|
||
kv host "${HOSTNAME_ALIAS}.local"
|
||
kv port "$PULSEAUDIO_TCP_PORT"
|
||
kv url "${CONSOLE_BASE_URL}/?view=services"
|
||
kv detail "tcp=${PULSEAUDIO_TCP_PORT}; control=${CONSOLE_BASE_URL}"
|
||
}
|
||
|
||
service_pulseaudio_start() {
|
||
systemctl enable --now live-pulseaudio.service
|
||
kv message "PulseAudio started"
|
||
}
|
||
|
||
service_pulseaudio_stop() {
|
||
systemctl stop live-pulseaudio.service
|
||
kv message "PulseAudio stopped"
|
||
}
|
||
|
||
service_kmscube_status() {
|
||
if [ "${ENABLE_KMSCUBE:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_KMSCUBE=1 in config/system-stack.env to enable kmscube diagnostics"
|
||
return 0
|
||
fi
|
||
if ! unit_exists live-kmscube.service && ! have kmscube; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "kmscube is not installed"
|
||
return 0
|
||
fi
|
||
kv available 1
|
||
kv running "$(unit_active live-kmscube.service && echo 1 || echo 0)"
|
||
kv status "$(unit_active live-kmscube.service && echo online || echo stopped)"
|
||
kv host "${HOSTNAME_ALIAS}.local"
|
||
kv port ""
|
||
kv url "${CONSOLE_BASE_URL}/?view=services"
|
||
kv detail "launcher=${KMSCUBE_LAUNCHER}; drm=${KMSCUBE_DRM_DEVICE}"
|
||
}
|
||
|
||
service_kmscube_start() {
|
||
systemctl start live-kmscube.service
|
||
kv message "kmscube started"
|
||
}
|
||
|
||
service_kmscube_stop() {
|
||
systemctl stop live-kmscube.service
|
||
kv message "kmscube stopped"
|
||
}
|
||
|
||
service_openclaw_status() {
|
||
if [ "${ENABLE_OPENCLAW:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_OPENCLAW=1 in config/system-stack.env to enable OpenClaw"
|
||
return 0
|
||
fi
|
||
if ! have docker || [ ! -f "$OPENCLAW_COMPOSE" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Docker or OpenClaw compose file is missing"
|
||
return 0
|
||
fi
|
||
local state
|
||
state="$(docker_container_status live-openclaw || 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 "$OPENCLAW_GATEWAY_PORT"
|
||
kv url "http://${HOSTNAME_ALIAS}.local:${OPENCLAW_GATEWAY_PORT}"
|
||
kv detail "${state:-container=missing};bridge=${OPENCLAW_BRIDGE_PORT}"
|
||
}
|
||
|
||
service_openclaw_start() {
|
||
mkdir -p "$OPENCLAW_CONFIG_DIR" "$OPENCLAW_WORKSPACE_DIR"
|
||
compose_in_service_dir "$OPENCLAW_COMPOSE" up -d
|
||
kv message "OpenClaw started"
|
||
}
|
||
|
||
service_openclaw_stop() {
|
||
compose_in_service_dir "$OPENCLAW_COMPOSE" down
|
||
kv message "OpenClaw stopped"
|
||
}
|
||
|
||
redroid_default_name() {
|
||
printf '%s' "${REDROID_INSTANCE_PREFIX}-1"
|
||
}
|
||
|
||
redroid_resolved_image() {
|
||
if [ -n "${REDROID_IMAGE:-}" ]; then
|
||
printf '%s' "$REDROID_IMAGE"
|
||
return 0
|
||
fi
|
||
case "$(uname -m)" in
|
||
x86_64|amd64)
|
||
printf '%s' "${REDROID_IMAGE_X86_ALT:-$REDROID_IMAGE_X86}"
|
||
;;
|
||
aarch64|arm64)
|
||
printf '%s' "${REDROID_IMAGE_ARM64_ALT:-$REDROID_IMAGE_ARM64}"
|
||
;;
|
||
*)
|
||
printf '%s' "$REDROID_IMAGE_X86"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
redroid_binderfs_ready() {
|
||
[ -e /dev/binderfs/binder-control ]
|
||
}
|
||
|
||
redroid_legacy_binder_devices_ready() {
|
||
[ -e /dev/binder ] && [ -e /dev/hwbinder ] && [ -e /dev/vndbinder ]
|
||
}
|
||
|
||
redroid_binder_mode() {
|
||
if redroid_binderfs_ready; then
|
||
printf '%s\n' binderfs
|
||
return 0
|
||
fi
|
||
if redroid_legacy_binder_devices_ready; then
|
||
printf '%s\n' legacy
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
redroid_binder_is_provisionable() {
|
||
redroid_binder_mode >/dev/null 2>&1 && return 0
|
||
grep -qw binder /proc/filesystems 2>/dev/null
|
||
}
|
||
|
||
redroid_support_detail() {
|
||
if redroid_binderfs_ready; then
|
||
printf '%s' "binderfs mounted"
|
||
return 0
|
||
fi
|
||
if redroid_legacy_binder_devices_ready; then
|
||
printf '%s' "legacy binder devices available"
|
||
return 0
|
||
fi
|
||
if grep -qw binder /proc/filesystems 2>/dev/null; then
|
||
printf '%s' "binder filesystem supported but not mounted"
|
||
return 0
|
||
fi
|
||
printf '%s' "binder kernel support unavailable"
|
||
}
|
||
|
||
ensure_redroid_host_support() {
|
||
if redroid_binder_mode >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
if have modprobe; then
|
||
modprobe binder_linux devices=binder,hwbinder,vndbinder >/dev/null 2>&1 || \
|
||
modprobe binder_linux >/dev/null 2>&1 || true
|
||
fi
|
||
if redroid_binder_mode >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
if grep -qw binder /proc/filesystems 2>/dev/null; then
|
||
mkdir -p /dev/binderfs
|
||
if have mountpoint; then
|
||
if ! mountpoint -q /dev/binderfs 2>/dev/null; then
|
||
mount -t binder binder /dev/binderfs >/dev/null 2>&1 || \
|
||
mount -t binderfs binderfs /dev/binderfs >/dev/null 2>&1 || true
|
||
fi
|
||
elif ! grep -qE '[[:space:]]/dev/binderfs[[:space:]]' /proc/mounts 2>/dev/null; then
|
||
mount -t binder binder /dev/binderfs >/dev/null 2>&1 || \
|
||
mount -t binderfs binderfs /dev/binderfs >/dev/null 2>&1 || true
|
||
fi
|
||
fi
|
||
redroid_binder_mode >/dev/null 2>&1
|
||
}
|
||
|
||
redroid_container_matches_host_support() {
|
||
local name="$1"
|
||
case "$(redroid_binder_mode 2>/dev/null || true)" in
|
||
binderfs)
|
||
docker inspect -f '{{range .Mounts}}{{println .Destination}}{{end}}' "$name" 2>/dev/null | grep -qx '/dev/binderfs'
|
||
;;
|
||
legacy)
|
||
docker inspect -f '{{range .HostConfig.Devices}}{{println .PathOnHost}}{{end}}' "$name" 2>/dev/null | grep -qx '/dev/binder' && \
|
||
docker inspect -f '{{range .HostConfig.Devices}}{{println .PathOnHost}}{{end}}' "$name" 2>/dev/null | grep -qx '/dev/hwbinder' && \
|
||
docker inspect -f '{{range .HostConfig.Devices}}{{println .PathOnHost}}{{end}}' "$name" 2>/dev/null | grep -qx '/dev/vndbinder'
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
service_redroid_status() {
|
||
if [ "${ENABLE_REDROID:-1}" != "1" ]; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "Set ENABLE_REDROID=1 in config/system-stack.env to enable Redroid"
|
||
return 0
|
||
fi
|
||
if ! have docker; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "docker not installed"
|
||
return 0
|
||
fi
|
||
local default_name running_line all_lines binder_detail
|
||
default_name="$(redroid_default_name)"
|
||
binder_detail="$(redroid_support_detail)"
|
||
running_line="$(docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' 2>/dev/null | grep -E "^${REDROID_INSTANCE_PREFIX}-|^${default_name} " | head -n1 || true)"
|
||
all_lines="$(docker ps -a --format '{{.Names}} {{.Image}} {{.Status}} {{.Ports}}' 2>/dev/null | grep -i redroid || true)"
|
||
if ! redroid_binder_mode >/dev/null 2>&1 && ! redroid_binder_is_provisionable; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv host "${HOSTNAME_ALIAS}.local"
|
||
kv port "$REDROID_DEFAULT_PORT"
|
||
kv url ""
|
||
kv detail "${binder_detail}; image=$(redroid_resolved_image); default=${default_name}; adb=${HOSTNAME_ALIAS}.local:${REDROID_DEFAULT_PORT}"
|
||
return 0
|
||
fi
|
||
kv available 1
|
||
kv running "$([ -n "$running_line" ] && echo 1 || echo 0)"
|
||
kv status "$([ -n "$running_line" ] && echo online || echo stopped)"
|
||
kv host "${HOSTNAME_ALIAS}.local"
|
||
kv port "$REDROID_DEFAULT_PORT"
|
||
kv url ""
|
||
kv detail "${all_lines:-binder=${binder_detail}; image=$(redroid_resolved_image); default=${default_name}; adb=${HOSTNAME_ALIAS}.local:${REDROID_DEFAULT_PORT}; boot=${REDROID_BOOT_ARGS}; expect_gms=${REDROID_EXPECT_GMS}; expect_magisk=${REDROID_EXPECT_MAGISK}}"
|
||
}
|
||
|
||
service_redroid_start() {
|
||
if [ "${ENABLE_REDROID:-1}" != "1" ]; then
|
||
kv message "Set ENABLE_REDROID=1 first"
|
||
exit 1
|
||
fi
|
||
if ! have docker; then
|
||
kv message "docker not installed"
|
||
exit 1
|
||
fi
|
||
local name image data_dir
|
||
name="$(redroid_default_name)"
|
||
image="$(redroid_resolved_image)"
|
||
data_dir="${REDROID_DATA_PREFIX}-${name}"
|
||
if ! ensure_redroid_host_support; then
|
||
kv message "Redroid host prerequisites unavailable: $(redroid_support_detail)"
|
||
exit 1
|
||
fi
|
||
mkdir -p "$data_dir"
|
||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||
if redroid_container_matches_host_support "$name"; then
|
||
kv message "Redroid already running"
|
||
exit 0
|
||
fi
|
||
docker rm -f "$name" >/dev/null
|
||
fi
|
||
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||
if ! redroid_container_matches_host_support "$name"; then
|
||
docker rm -f "$name" >/dev/null
|
||
else
|
||
docker start "$name" >/dev/null
|
||
kv message "Redroid container started"
|
||
exit 0
|
||
fi
|
||
fi
|
||
docker run -d \
|
||
--name "$name" \
|
||
--privileged \
|
||
--pull always \
|
||
--restart unless-stopped \
|
||
-p "${REDROID_DEFAULT_PORT}:5555" \
|
||
-v "${data_dir}:/data" \
|
||
-e "REDROID_GPU_MODE=${REDROID_GPU_MODE:-guest}" \
|
||
-e "REDROID_DEVICE_PROFILE=${REDROID_DEVICE_PROFILE:-pixel-7-pro}" \
|
||
$( [ "${REDROID_ADB_INSECURE:-1}" = "1" ] && printf '%s ' '-e' 'REDROID_ADB_INSECURE=1' ) \
|
||
$( [ -e /dev/kvm ] && printf '%s ' '--device' '/dev/kvm' ) \
|
||
$( [ -d /dev/dri ] && printf '%s ' '-v /dev/dri:/dev/dri' ) \
|
||
$( [ -d /dev/dma_heap ] && printf '%s ' '-v /dev/dma_heap:/dev/dma_heap' ) \
|
||
$( redroid_binderfs_ready && printf '%s ' '-v' '/dev/binderfs:/dev/binderfs' ) \
|
||
$( redroid_legacy_binder_devices_ready && printf '%s ' '--device' '/dev/binder' '--device' '/dev/hwbinder' '--device' '/dev/vndbinder' ) \
|
||
$( [ -e /dev/rga ] && printf '%s ' '--device /dev/rga' ) \
|
||
$( [ -e /dev/mpp_service ] && printf '%s ' '--device /dev/mpp_service' ) \
|
||
"$image" ${REDROID_BOOT_ARGS:-} >/dev/null
|
||
kv message "Redroid container created"
|
||
}
|
||
|
||
service_redroid_stop() {
|
||
local name
|
||
name="$(redroid_default_name)"
|
||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||
docker stop "$name" >/dev/null
|
||
kv message "Redroid container stopped"
|
||
return 0
|
||
fi
|
||
kv message "Redroid container not running"
|
||
}
|
||
|
||
service_android_gateway_status() {
|
||
if ! have nmcli; then
|
||
kv available 0
|
||
kv running 0
|
||
kv status unavailable
|
||
kv detail "需要 NetworkManager(nmcli) 才能创建共享连接"
|
||
return 0
|
||
fi
|
||
if [ -z "${ANDROID_DOWNLINK_IF:-}" ]; then
|
||
kv available 1
|
||
kv running 0
|
||
kv status skipped
|
||
kv detail "单网口或未配置下行口:无需透明网关。安卓可经 USB 网络共享 / Wi‑Fi / 同 LAN 访问板子。仅当板载第二以太网直连安卓板时,在 system-stack.env 设置 ANDROID_DOWNLINK_IF=eth1(或实际接口名)"
|
||
kv url ""
|
||
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:单网口场景无需启动安卓网关"
|
||
exit 0
|
||
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
|
||
if [ "$ACTION" = "restart" ] && declare -F "service_${SERVICE_ID}_stop" >/dev/null 2>&1 && declare -F "service_${SERVICE_ID}_start" >/dev/null 2>&1; then
|
||
"service_${SERVICE_ID}_stop" >/dev/null 2>&1 || true
|
||
"service_${SERVICE_ID}_start"
|
||
return 0
|
||
fi
|
||
if [ "$ACTION" = "install" ] && declare -F "service_${SERVICE_ID}_start" >/dev/null 2>&1; then
|
||
"service_${SERVICE_ID}_start"
|
||
return 0
|
||
fi
|
||
if [ "$ACTION" = "uninstall" ] && declare -F "service_${SERVICE_ID}_stop" >/dev/null 2>&1; then
|
||
"service_${SERVICE_ID}_stop" >/dev/null 2>&1 || true
|
||
kv message "Service uninstalled via stop fallback"
|
||
return 0
|
||
fi
|
||
if [ "$ACTION" = "upgrade" ] && declare -F "service_${SERVICE_ID}_stop" >/dev/null 2>&1 && declare -F "service_${SERVICE_ID}_start" >/dev/null 2>&1; then
|
||
"service_${SERVICE_ID}_stop" >/dev/null 2>&1 || true
|
||
"service_${SERVICE_ID}_start" >/dev/null 2>&1 || true
|
||
kv message "Service upgraded via restart fallback"
|
||
return 0
|
||
fi
|
||
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
|