变更详情:feat(kiosk-shell): 登录/桌面列表产品流程 + Moonlight 会话 + 看门狗进程组修复 - 登录页改为用户认证(本地演示认证,账号密码非空通过) - 新增桌面列表页:Windows/Moonlight 可连接,Ubuntu/麒麟待分配 - 新增 DesktopService 适配层(登录/桌面列表/连接票据,后续接 API) - 新增 MoonlightSession:QProcess 参数数组直启 moonlight-qt,退出回列表 - 状态机扩展 AUTHENTICATING / DESKTOP_LIST - 移除开机自动 RDP,收敛为 monitor -> kiosk-shell 单一自启链 - Monitor 按进程组结束整棵进程树,修复孤儿串流进程 - vdi-moonlight 启动器新增 vsync/frame-pacing/overlay 低延迟开关
270 lines
8.3 KiB
Bash
270 lines
8.3 KiB
Bash
#!/bin/sh
|
||
# VDI 云桌面串流启动器(Moonlight 客户端侧)
|
||
#
|
||
# 职责:
|
||
# 1. 读取本地配置(主机、应用、分辨率、帧率、码率、编解码)
|
||
# 2. 以软件解码拉起 moonlight-qt,全屏串流云桌面
|
||
# 3. 写心跳文件,供 vdi-kiosk-monitor 判断是否假死
|
||
# 4. 检测串流假死(CPU 与网络均无流量)并杀掉重连
|
||
#
|
||
# 被 vdi-kiosk-monitor 守护:
|
||
# vdi-kiosk-monitor -- /home/armbian/vdi-terminal/bin/vdi-moonlight
|
||
#
|
||
# 说明:RK3528 当前没有可用的 V4L2 request/m2m 硬解设备,moonlight-qt 的
|
||
# --video-decoder auto 会选到 h264_v4l2m2m 并报 "Could not find a valid device",
|
||
# 因此配置里显式指定 decoder=software。接入 MPP/VPU 硬解后再改回 auto。
|
||
|
||
set -u
|
||
|
||
CONF="${VDI_MOONLIGHT_CONF:-$HOME/.config/vdi-terminal/moonlight.conf}"
|
||
HEARTBEAT="${VDI_MOONLIGHT_HEARTBEAT:-/tmp/vdi-moonlight.heartbeat}"
|
||
PIDFILE="${VDI_MOONLIGHT_PIDFILE:-/tmp/vdi-moonlight.pid}"
|
||
LAUNCHER_PIDFILE="${VDI_MOONLIGHT_LAUNCHER_PIDFILE:-/tmp/vdi-moonlight-launcher.pid}"
|
||
LOG_DIR="${VDI_MOONLIGHT_LOG_DIR:-$HOME/vdi-terminal/logs}"
|
||
LOG_FILE="$LOG_DIR/moonlight-$(date +%Y%m%d).log"
|
||
|
||
ML_PID=""
|
||
|
||
# 默认值,均可被配置文件覆盖
|
||
HOST=""
|
||
PORT=""
|
||
APP="Desktop"
|
||
WIDTH=1920
|
||
HEIGHT=1080
|
||
FPS=60
|
||
BITRATE=20000
|
||
CODEC="H.264"
|
||
DECODER="software"
|
||
DISPLAY_MODE="fullscreen"
|
||
AUDIO=1
|
||
ABSOLUTE_MOUSE=1
|
||
VSYNC=0
|
||
FRAME_PACING=0
|
||
PERFORMANCE_OVERLAY=0
|
||
RETRY=3
|
||
RETRY_DELAY=5
|
||
HANG_TIMEOUT=90
|
||
WAIT_HOST=120
|
||
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
log() {
|
||
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
|
||
}
|
||
|
||
# 读取 key=value 形式的配置
|
||
load_config() {
|
||
[ -f "$CONF" ] || return 0
|
||
while IFS='=' read -r key value; do
|
||
case "$key" in
|
||
''|\#*) continue ;;
|
||
esac
|
||
value="${value%\"}"; value="${value#\"}"
|
||
case "$key" in
|
||
host) HOST="$value" ;;
|
||
port) PORT="$value" ;;
|
||
app) APP="$value" ;;
|
||
width) WIDTH="$value" ;;
|
||
height) HEIGHT="$value" ;;
|
||
fps) FPS="$value" ;;
|
||
bitrate) BITRATE="$value" ;;
|
||
codec) CODEC="$value" ;;
|
||
decoder) DECODER="$value" ;;
|
||
display-mode) DISPLAY_MODE="$value" ;;
|
||
audio) AUDIO="$value" ;;
|
||
absolute-mouse) ABSOLUTE_MOUSE="$value" ;;
|
||
vsync) VSYNC="$value" ;;
|
||
frame-pacing) FRAME_PACING="$value" ;;
|
||
performance-overlay) PERFORMANCE_OVERLAY="$value" ;;
|
||
retry) RETRY="$value" ;;
|
||
retry-delay) RETRY_DELAY="$value" ;;
|
||
hang-timeout) HANG_TIMEOUT="$value" ;;
|
||
wait-host) WAIT_HOST="$value" ;;
|
||
esac
|
||
done < "$CONF"
|
||
}
|
||
|
||
# 默认路由出接口的累计收字节数,用于判断串流是否还有数据流入
|
||
rx_bytes() {
|
||
iface=$(awk '$2=="00000000" {print $1; exit}' /proc/net/route)
|
||
[ -n "$iface" ] || iface=lo
|
||
awk -v i="$iface:" '$1==i {print $2; exit}' /proc/net/dev
|
||
}
|
||
|
||
# 进程累计 CPU 时间(utime+stime),用于判断串流是否还在解码
|
||
cpu_ticks() {
|
||
pid="$1"
|
||
[ -r "/proc/$pid/stat" ] || { echo 0; return; }
|
||
cut -d')' -f2 "/proc/$pid/stat" 2>/dev/null | awk '{print $12+$13}'
|
||
}
|
||
|
||
host_reachable() {
|
||
# 用 TCP 探测 Sunshine 的 HTTPS 控制端口。
|
||
# 注意:Windows 防火墙默认屏蔽 ICMP,ping 不通不代表服务不可用,
|
||
# 所以优先做 TCP 连接探测,ping 只作为最后兜底。
|
||
probe_port="${PORT:-47984}"
|
||
if command -v nc >/dev/null 2>&1; then
|
||
nc -z -w 2 "$HOST" "$probe_port" 2>/dev/null && return 0
|
||
return 1
|
||
fi
|
||
if command -v bash >/dev/null 2>&1; then
|
||
timeout 3 bash -c "exec 3<>/dev/tcp/$HOST/$probe_port" 2>/dev/null && return 0
|
||
return 1
|
||
fi
|
||
ping -c 1 -W 2 "$HOST" >/dev/null 2>&1
|
||
}
|
||
|
||
kill_stale() {
|
||
# 清理上次崩溃残留的串流进程,避免孤儿进程抢占显示。
|
||
# 顺序很重要:先结束残留的启动器实例(否则它会再拉起一条串流),
|
||
# 再清理残留的串流进程。
|
||
if [ -f "$LAUNCHER_PIDFILE" ]; then
|
||
old=$(cat "$LAUNCHER_PIDFILE" 2>/dev/null)
|
||
if [ -n "$old" ] && [ "$old" != "$$" ]; then
|
||
kill -9 "$old" 2>/dev/null
|
||
fi
|
||
rm -f "$LAUNCHER_PIDFILE"
|
||
fi
|
||
if [ -f "$PIDFILE" ]; then
|
||
old=$(cat "$PIDFILE" 2>/dev/null)
|
||
[ -n "$old" ] && kill -9 "$old" 2>/dev/null
|
||
rm -f "$PIDFILE"
|
||
fi
|
||
# 用 ^ 锚定命令行开头,避免误杀包含该字符串的 shell 包装进程
|
||
pkill -f '^moonlight-qt stream' 2>/dev/null
|
||
sleep 1
|
||
}
|
||
|
||
disable_screensaver() {
|
||
# Kiosk 场景不允许息屏把画面遮掉
|
||
command -v gsettings >/dev/null 2>&1 || return 0
|
||
gsettings set org.mate.screensaver idle-activation-enabled false 2>/dev/null
|
||
gsettings set org.mate.screensaver lock-enabled false 2>/dev/null
|
||
}
|
||
|
||
build_cmd() {
|
||
# moonlight-qt stream 只接受 <host> <app> 位置参数,没有 --port 选项,
|
||
# 服务端端口由 moonlight 自身按 GameStream 协议协商。
|
||
CMD="moonlight-qt stream $HOST $APP"
|
||
CMD="$CMD --resolution ${WIDTH}x${HEIGHT}"
|
||
CMD="$CMD --fps $FPS"
|
||
CMD="$CMD --bitrate $BITRATE"
|
||
CMD="$CMD --display-mode $DISPLAY_MODE"
|
||
CMD="$CMD --video-codec $CODEC"
|
||
CMD="$CMD --video-decoder $DECODER"
|
||
CMD="$CMD --keep-awake"
|
||
[ "$ABSOLUTE_MOUSE" = "1" ] && CMD="$CMD --absolute-mouse"
|
||
[ "$AUDIO" = "1" ] && CMD="$CMD --audio-on-host" || CMD="$CMD --no-audio-on-host"
|
||
# 低延迟开关:默认关闭 vsync 与 frame-pacing,削减帧队列/呈现延迟
|
||
[ "$VSYNC" = "1" ] && CMD="$CMD --vsync" || CMD="$CMD --no-vsync"
|
||
[ "$FRAME_PACING" = "1" ] && CMD="$CMD --frame-pacing" || CMD="$CMD --no-frame-pacing"
|
||
[ "$PERFORMANCE_OVERLAY" = "1" ] && CMD="$CMD --performance-overlay"
|
||
}
|
||
|
||
run_stream() {
|
||
build_cmd
|
||
log "启动串流: $CMD"
|
||
# shellcheck disable=SC2086
|
||
$CMD >> "$LOG_FILE" 2>&1 &
|
||
ML_PID=$!
|
||
echo "$ML_PID" > "$PIDFILE"
|
||
|
||
# 收到终止信号时把串流进程一起带走,避免留下占用显示的孤儿进程
|
||
trap 'if [ -n "$ML_PID" ] && kill -0 "$ML_PID" 2>/dev/null; then kill -TERM "$ML_PID" 2>/dev/null; sleep 1; kill -9 "$ML_PID" 2>/dev/null; fi; rm -f "$PIDFILE"; exit 143' TERM INT
|
||
|
||
last_cpu=$(cpu_ticks "$ML_PID")
|
||
last_rx=$(rx_bytes)
|
||
flat_since=$(date +%s)
|
||
|
||
while kill -0 "$ML_PID" 2>/dev/null; do
|
||
sleep 5
|
||
date +%s > "$HEARTBEAT" 2>/dev/null
|
||
|
||
cur_cpu=$(cpu_ticks "$ML_PID")
|
||
cur_rx=$(rx_bytes)
|
||
now=$(date +%s)
|
||
|
||
if [ "$cur_cpu" != "$last_cpu" ] || [ "$cur_rx" != "$last_rx" ]; then
|
||
flat_since=$now
|
||
elif [ "$HANG_TIMEOUT" -gt 0 ] && [ $((now - flat_since)) -ge "$HANG_TIMEOUT" ]; then
|
||
log "检测到串流假死(${HANG_TIMEOUT}s 内无 CPU 与网络活动),重启串流"
|
||
kill -9 "$ML_PID" 2>/dev/null
|
||
wait "$ML_PID" 2>/dev/null
|
||
return 3
|
||
fi
|
||
last_cpu=$cur_cpu
|
||
last_rx=$cur_rx
|
||
done
|
||
|
||
wait "$ML_PID"
|
||
code=$?
|
||
rm -f "$PIDFILE"
|
||
log "串流进程退出, code=$code"
|
||
return $code
|
||
}
|
||
|
||
case "${1:-start}" in
|
||
status)
|
||
if pgrep -f 'moonlight-qt stream' >/dev/null 2>&1; then
|
||
echo "moonlight: running"
|
||
exit 0
|
||
fi
|
||
echo "moonlight: stopped"
|
||
exit 1
|
||
;;
|
||
stop)
|
||
kill_stale
|
||
echo "moonlight: stopped"
|
||
exit 0
|
||
;;
|
||
esac
|
||
|
||
load_config
|
||
if [ -z "$HOST" ]; then
|
||
log "配置缺失或缺少 host: $CONF"
|
||
exit 2
|
||
fi
|
||
|
||
export DISPLAY="${VDI_DISPLAY:-:0}"
|
||
export XAUTHORITY="${VDI_XAUTHORITY:-$HOME/.Xauthority}"
|
||
|
||
disable_screensaver
|
||
kill_stale
|
||
echo "$$" > "$LAUNCHER_PIDFILE"
|
||
|
||
# 等待云桌面主机就绪(开机自启时虚拟机可能还没起来)
|
||
waited=0
|
||
log "等待云桌面主机 $HOST 就绪(最长 ${WAIT_HOST}s)"
|
||
while [ "$waited" -lt "$WAIT_HOST" ]; do
|
||
if host_reachable; then
|
||
log "主机 $HOST 已就绪(等待 ${waited}s)"
|
||
break
|
||
fi
|
||
sleep 5
|
||
waited=$((waited + 5))
|
||
done
|
||
if [ "$waited" -ge "$WAIT_HOST" ]; then
|
||
log "等待主机 $HOST 超时 ${WAIT_HOST}s,仍尝试启动串流"
|
||
fi
|
||
|
||
attempt=1
|
||
while [ "$attempt" -le "$RETRY" ]; do
|
||
run_stream
|
||
code=$?
|
||
if [ "$code" = "0" ]; then
|
||
log "串流正常结束"
|
||
exit 0
|
||
fi
|
||
if [ "$code" = "3" ]; then
|
||
# 假死重启,直接下一轮
|
||
attempt=$((attempt + 1))
|
||
continue
|
||
fi
|
||
log "第 $attempt/$RETRY 次串流失败 (code=$code),${RETRY_DELAY}s 后重试"
|
||
attempt=$((attempt + 1))
|
||
sleep "$RETRY_DELAY"
|
||
done
|
||
|
||
log "连续 $RETRY 次串流失败,退出,交由 Monitor 重启"
|
||
exit 1
|