初始化:导入 VDI 云终端代码与会话基础能力
变更详情:init: VDI 云终端代码(kiosk-shell 登录/桌面列表/Moonlight/RDP/看门狗)
This commit is contained in:
commit
162dcc1e02
27 files changed
+3224
No files matched your search
@@ -0,0 +1,269 @@
|
||||
#!/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
|
||||
Reference in new issue
Block a user