Files
gitlab-instance-0a899031_sh/d2ypp2/scripts/linux/service_ctl.sh
2026-05-28 15:59:03 -05:00

1889 lines
60 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
SERVICE_ID="${1:-}"
ACTION="${2:-status}"
ACTION_ARG1="${3:-}"
ACTION_ARG2="${4:-}"
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() {
if [ -n "${NEKO_IMAGE:-}" ]; then
printf '%s' "$NEKO_IMAGE"
return 0
fi
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 override
base="${NEKO_PROFILE_HOST_DIR:-$(resolve_neko_profile_dir)}"
eval "override=\"\${NEKO_PROFILE_HOST_DIR_${idx}:-}\""
if [ -n "$override" ]; then
printf '%s' "$override"
return 0
fi
case "$idx" in
1) printf '%s' "$base" ;;
*) printf '%s-%s' "$base" "$idx" ;;
esac
}
neko_managed_policies_enabled() {
case "${NEKO_MANAGED_POLICIES:-1}" in
1|true|TRUE|yes|YES|on|ON|enable|enabled)
return 0
;;
*)
return 1
;;
esac
}
copy_neko_tampermonkey_seed_dir() {
local src="${1:-}" dst="${2:-}" marker="${3:-}"
[ -e "$src" ] || return 0
if [ -e "$dst" ]; then
if [ -z "$marker" ] || grep -R -a -q "$marker" "$dst" 2>/dev/null; then
return 0
fi
rm -rf "$dst" 2>/dev/null || true
fi
mkdir -p "$(dirname "$dst")"
cp -a "$src" "$dst" 2>/dev/null || true
rm -f "$dst/LOCK" 2>/dev/null || true
}
seed_neko_tampermonkey_profile() {
local target="${1:-}" donor="${2:-}" rel src dst marker
[ -n "$target" ] || return 0
if [ -z "$donor" ] || [ ! -d "$donor/Default/Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo" ]; then
donor="$(find_neko_tampermonkey_seed_profile "$target")"
fi
[ "$target" = "$donor" ] && return 0
[ -d "$donor/Default" ] || return 0
mkdir -p "$target/Default"
for rel in \
"Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo" \
"Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo"; do
src="$donor/Default/$rel"
dst="$target/Default/$rel"
marker="Tampermonkey"
case "$rel" in
"Local Extension Settings/"*) marker="YouTube Studio Auto Dismiss" ;;
esac
copy_neko_tampermonkey_seed_dir "$src" "$dst" "$marker"
done
rel="Managed Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo"
src="$donor/Default/$rel"
dst="$target/Default/$rel"
if neko_managed_policies_enabled; then
copy_neko_tampermonkey_seed_dir "$src" "$dst" "YouTube Studio Auto Dismiss"
else
rm -rf "$dst" 2>/dev/null || true
fi
sync_neko_tampermonkey_preferences "$target" "$donor"
}
find_neko_tampermonkey_seed_profile() {
local target="${1:-}" base data_dir bundled cand
if [ -n "${NEKO_TAMPERMONKEY_SEED_PROFILE:-}" ] && [ -d "$NEKO_TAMPERMONKEY_SEED_PROFILE/Default/Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo" ]; then
printf '%s' "$NEKO_TAMPERMONKEY_SEED_PROFILE"
return 0
fi
base="$(resolve_neko_profile_dir_for_instance 1)"
data_dir="$ROOT_DIR/services/neko/data"
bundled="$ROOT_DIR/services/neko/tampermonkey-seed"
for cand in "$base" "$base"-* "$data_dir"/*profile-v2 "$data_dir"/*profile-v2-*; do
[ -n "$cand" ] || continue
[ "$cand" != "$target" ] || continue
[ -d "$cand/Default/Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo" ] || continue
[ -d "$cand/Default/Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo" ] || continue
printf '%s' "$cand"
return 0
done
if [ -d "$bundled/Default/Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo" ]; then
printf '%s' "$bundled"
return 0
fi
}
sync_neko_tampermonkey_preferences() {
local target="${1:-}" donor="${2:-}"
[ -n "$target" ] && [ -n "$donor" ] || return 0
command -v python3 >/dev/null 2>&1 || return 0
DONOR_PROFILE="$donor" TARGET_PROFILE="$target" python3 - <<'PY'
import json
import os
from pathlib import Path
ext_id = "dhdgffkkebhmkfjojejmpbldmpobfkfo"
donor_pref = Path(os.environ["DONOR_PROFILE"]) / "Default" / "Preferences"
target_pref = Path(os.environ["TARGET_PROFILE"]) / "Default" / "Preferences"
def load(path: Path) -> dict:
if not path.exists():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
donor = load(donor_pref)
source = donor.get("extensions", {}).get("settings", {}).get(ext_id)
if not isinstance(source, dict):
raise SystemExit(0)
target = load(target_pref)
settings = target.setdefault("extensions", {}).setdefault("settings", {})
def should_replace_current(value: object) -> bool:
if not isinstance(value, dict):
return True
if value.get("state") != 1:
return True
if value.get("from_webstore") is not True:
return True
path = str(value.get("path") or "")
if path.startswith("/"):
return True
if value.get("location") == 8:
return True
return False
if should_replace_current(settings.get(ext_id)):
settings[ext_id] = source
target_pref.parent.mkdir(parents=True, exist_ok=True)
target_pref.write_text(json.dumps(target, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
PY
}
resolve_neko_instance_count() {
local requested="${NEKO_INSTANCE_COUNT:-2}"
local cpu count max_count
max_count="${NEKO_MAX_INSTANCE_COUNT:-64}"
case "$max_count" in
''|*[!0-9]*) max_count=64 ;;
esac
[ "$max_count" -lt 2 ] && max_count=2
[ "$max_count" -gt 64 ] && max_count=64
cpu="$(nproc 2>/dev/null || echo 4)"
case "$requested" in
auto|"")
if [ "$cpu" -le 4 ]; then
count=2
else
count=3
fi
;;
*[!0-9]*) count=2 ;;
*)
count="$requested"
;;
esac
if [ "$count" -lt 2 ]; then
count=2
fi
if [ "$count" -gt "$max_count" ]; then
count="$max_count"
fi
printf '%s' "$count"
}
resolve_neko_http_port_for_instance() {
local idx="${1:-1}" value base
eval "value=\"\${NEKO_HTTP_PORT_${idx}:-}\""
if [ -n "$value" ]; then
printf '%s' "$value"
return 0
fi
base="${NEKO_HTTP_PORT:-9200}"
printf '%s' $((base + idx - 1))
}
resolve_neko_tcpmux_for_instance() {
local idx="${1:-1}" value base
eval "value=\"\${NEKO_WEBRTC_TCPMUX_${idx}:-}\""
if [ -n "$value" ]; then
printf '%s' "$value"
return 0
fi
base="${NEKO_WEBRTC_TCPMUX:-52300}"
printf '%s' $((base + idx - 1))
}
resolve_neko_udpmux_for_instance() {
local idx="${1:-1}" value base
eval "value=\"\${NEKO_WEBRTC_UDPMUX_${idx}:-}\""
if [ -n "$value" ]; then
printf '%s' "$value"
return 0
fi
base="${NEKO_WEBRTC_UDPMUX:-52300}"
printf '%s' $((base + idx - 1))
}
resolve_neko_udp_range_for_instance() {
local idx="${1:-1}" value start
eval "value=\"\${NEKO_WEBRTC_UDP_RANGE_${idx}:-}\""
if [ -n "$value" ]; then
printf '%s' "$value"
return 0
fi
if [ "$idx" -eq 1 ] && [ -n "${NEKO_WEBRTC_UDP_RANGE:-}" ]; then
printf '%s' "$NEKO_WEBRTC_UDP_RANGE"
return 0
fi
case "$idx" in
2) start=52101 ;;
3) start=52201 ;;
*) start=$((52400 + ((idx - 4) * 100))) ;;
esac
[ "$idx" -eq 1 ] && start=52000
printf '%s-%s' "$start" "$((start + 99))"
}
render_neko_compose_file() {
local count out
count="$(resolve_neko_instance_count)"
out="$ROOT_DIR/services/neko/docker-compose.generated.yml"
python3 "$ROOT_DIR/tools/render_neko_compose.py" --count "$count" --output "$out"
printf '%s' "$out"
}
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([ ]|$)' | 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 [ -x "$SHELLCRASH_DIR/start.sh" ]; then
"$SHELLCRASH_DIR/start.sh" start
elif unit_exists shellcrash.service; then
systemctl enable --now shellcrash.service
else
kv message "ShellCrash is not installed"
exit 1
fi
kv message "ShellCrash started"
}
service_shellcrash_restart() {
if [ -x "$SHELLCRASH_DIR/start.sh" ]; then
"$SHELLCRASH_DIR/start.sh" restart
elif unit_exists shellcrash.service; then
systemctl restart shellcrash.service
else
kv message "ShellCrash is not installed"
exit 1
fi
kv message "ShellCrash restarted"
}
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_shellcrash_apply_profile() {
local src="${ACTION_ARG1:-$SHELLCRASH_DIR/yamls/LsRzcR3pHoA9.yaml}"
local target_node="${ACTION_ARG2:-5d723acc-singbox_tuic3}"
local src_real stage_root shell_yaml_root profile_name cfg unit_path selected
if [ -z "$target_node" ] || printf '%s' "$target_node" | grep -q '[[:cntrl:]]'; then
kv message "Invalid ShellCrash target node"
exit 1
fi
if ! have realpath; then
kv message "realpath is required to apply ShellCrash profile"
exit 1
fi
src_real="$(realpath -m "$src")"
stage_root="$(realpath -m "$ROOT_DIR/.process_runner")"
shell_yaml_root="$(realpath -m "$SHELLCRASH_DIR/yamls")"
case "$src_real" in
"$stage_root"/*|"$shell_yaml_root"/*) ;;
*)
kv message "ShellCrash profile path must be staged under $stage_root or $shell_yaml_root"
exit 1
;;
esac
if [ ! -s "$src_real" ]; then
kv message "ShellCrash profile is missing or empty: $src_real"
exit 1
fi
mkdir -p "$SHELLCRASH_DIR/yamls" "$SHELLCRASH_DIR/configs" /tmp/ShellCrash
profile_name="$(basename "$src_real")"
case "$profile_name" in
*.yaml|*.yml) ;;
*) profile_name="LsRzcR3pHoA9.yaml" ;;
esac
install -m 0644 "$src_real" "$SHELLCRASH_DIR/yamls/$profile_name"
install -m 0644 "$src_real" "$SHELLCRASH_DIR/yamls/config.yaml"
rm -f "$SHELLCRASH_DIR/configs/web_save" "$SHELLCRASH_DIR/.start_error"
cfg="$SHELLCRASH_DIR/configs/ShellCrash.cfg"
[ -f "$cfg" ] || printf '%s\n' "#ShellCrash配置文件不明勿动" >"$cfg"
set_shellcrash_cfg() {
local key="$1" value="$2"
sed -i "/^${key}=.*/d" "$cfg"
printf '%s=%s\n' "$key" "$value" >>"$cfg"
}
set_shellcrash_cfg crashcore meta
set_shellcrash_cfg cpucore arm64
set_shellcrash_cfg redir_mod "Redir模式"
set_shellcrash_cfg firewall_area 3
set_shellcrash_cfg dns_mod redir_host
set_shellcrash_cfg mix_port 7890
set_shellcrash_cfg redir_port 7892
set_shellcrash_cfg tproxy_port 7893
set_shellcrash_cfg db_port 9999
set_shellcrash_cfg dns_port 1053
set_shellcrash_cfg proxies_bypass ON
set_shellcrash_cfg sniffer ON
set_shellcrash_cfg skip_cert ON
set_shellcrash_cfg common_ports ON
set_shellcrash_cfg multiport "22,53,80,123,443,8080,8443"
set_shellcrash_cfg network_check OFF
cat >"$SHELLCRASH_DIR/configs/command.env" <<EOF
TMPDIR=/tmp/ShellCrash
BINDIR=$SHELLCRASH_DIR
COMMAND="\$TMPDIR/CrashCore -d \$BINDIR -f \$TMPDIR/config.yaml"
EOF
if unit_exists shellcrash.service; then
unit_path="$(systemctl show -p FragmentPath shellcrash.service | sed 's/^FragmentPath=//')"
if [ -n "$unit_path" ] && [ -f "$unit_path" ]; then
sed -i "s#^ExecStart=.*#ExecStart=/tmp/ShellCrash/CrashCore -d $SHELLCRASH_DIR -f /tmp/ShellCrash/config.yaml#" "$unit_path"
systemctl daemon-reload
fi
systemctl reset-failed shellcrash.service >/dev/null 2>&1 || true
systemctl restart shellcrash.service
elif [ -x "$SHELLCRASH_DIR/start.sh" ]; then
"$SHELLCRASH_DIR/start.sh" restart
else
kv message "ShellCrash is not installed"
exit 1
fi
selected=""
if have python3; then
selected="$(python3 - "$target_node" "$SHELLCRASH_DIR/configs/ShellCrash.cfg" <<'PY' || true
import json
import sys
import time
import urllib.parse
import urllib.request
target = sys.argv[1]
cfg_path = sys.argv[2]
base = "http://127.0.0.1:9999"
secret = ""
try:
with open(cfg_path, encoding="utf-8", errors="replace") as fh:
for line in fh:
if line.startswith("secret="):
secret = line.split("=", 1)[1].strip()
break
except OSError:
pass
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
def request(path, *, data=None, method="GET"):
req = urllib.request.Request(base + path, data=data, headers=headers, method=method)
return urllib.request.urlopen(req, timeout=3).read()
last = None
for _ in range(30):
try:
payload = json.loads(request("/proxies").decode("utf-8"))
break
except Exception as exc: # noqa: BLE001
last = exc
time.sleep(1)
else:
raise SystemExit(f"api unavailable: {last}")
changed = []
for name, item in payload.get("proxies", {}).items():
all_nodes = item.get("all") or []
if target in all_nodes and item.get("type") in ("Selector", "URLTest", "Fallback", "LoadBalance"):
body = json.dumps({"name": target}).encode("utf-8")
request("/proxies/" + urllib.parse.quote(name, safe=""), data=body, method="PUT")
changed.append(name)
print("|".join(changed))
PY
)"
fi
kv message "ShellCrash profile applied"
kv profile "$SHELLCRASH_DIR/yamls/config.yaml"
kv target_node "$target_node"
kv selected "$selected"
}
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 idx ports="" profiles=""
instance_count="$(resolve_neko_instance_count)"
total_count="$instance_count"
for idx in $(seq 1 "$instance_count"); do
name="live-neko-$idx"
state="$(docker_container_status "$name" || true)"
if echo "$state" | grep -qi running; then
running_count=$((running_count + 1))
fi
states="${states}${name}:${state:-missing};"
ports="${ports}$(resolve_neko_http_port_for_instance "$idx"),"
profiles="${profiles}$(resolve_neko_profile_dir_for_instance "$idx"),"
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=${ports%,}; profiles=${profiles%,}; screen=${NEKO_DESKTOP_SCREEN}; shm=${NEKO_SHM_SIZE}; fps=${NEKO_MAX_FPS}; bitrate=${NEKO_VIDEO_BITRATE}; render=${NEKO_BROWSER_RENDER_MODE}; 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 "$ROOT_DIR/tools/render_neko_compose.py" ]; then
kv message "Neko compose renderer missing"
return 0
fi
local prof idx port mux udp range compose_file service_names=""
local instance_count
instance_count="$(resolve_neko_instance_count)"
export_neko_env
for idx in $(seq 1 "$instance_count"); do
prof="$(resolve_neko_profile_dir_for_instance "$idx")"
seed_neko_tampermonkey_profile "$prof" "$(resolve_neko_profile_dir_for_instance 1)"
prune_neko_profile "$prof"
port="$(resolve_neko_http_port_for_instance "$idx")"
mux="$(resolve_neko_tcpmux_for_instance "$idx")"
udp="$(resolve_neko_udpmux_for_instance "$idx")"
range="$(resolve_neko_udp_range_for_instance "$idx")"
allow_tcp_port_if_firewall_active "$port"
allow_tcp_port_if_firewall_active "$mux"
allow_udp_port_if_firewall_active "$udp"
allow_udp_range_if_firewall_active "$range"
service_names="${service_names} neko${idx}"
done
remove_legacy_neko_container
compose_file="$(render_neko_compose_file)"
# shellcheck disable=SC2086
compose_in_service_dir "$compose_file" up -d $service_names
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_MANAGED_POLICIES="${NEKO_MANAGED_POLICIES:-1}"
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 "$ROOT_DIR/tools/render_neko_compose.py" ]; then
kv message "Neko compose missing"
return 0
fi
local compose_file
export_neko_env
compose_file="$(render_neko_compose_file)"
compose_in_service_dir "$compose_file" down --remove-orphans
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 网络共享 / WiFi / 同 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