Files
gitlab-instance-0a899031_sh/d2ypp2/scripts/linux/service_ctl.sh
root efa5533032 s
2026-05-18 08:36:28 +00:00

1572 lines
51 KiB
Bash
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}"
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 网络共享 / 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