#!/bin/sh
# VDI 云桌面串流启动器（Moonlight 客户端侧）
#
# 职责：
#   1. 读取本地配置（主机、应用、分辨率、帧率、码率、编解码）
#   2. 按配置拉起 Moonlight，全屏串流云桌面
#   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；验证后的独立 RKMPP 客户端
# 通过 VDI_MOONLIGHT_PROGRAM 与 decoder=hardware 显式启用。

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=1
FRAME_PACING=1
PERFORMANCE_OVERLAY=0
PROFILE=""
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" ;;
        profile) PROFILE="$value" ;;
        retry) RETRY="$value" ;;
        retry-delay) RETRY_DELAY="$value" ;;
        hang-timeout) HANG_TIMEOUT="$value" ;;
        wait-host) WAIT_HOST="$value" ;;
        esac
    done < "$CONF"
}

apply_profile() {
    case "$PROFILE" in
    smooth)
        VSYNC=1
        FRAME_PACING=1
        ;;
    low-latency)
        VSYNC=0
        FRAME_PACING=0
        ;;
    diagnostic)
        VSYNC=1
        FRAME_PACING=1
        PERFORMANCE_OVERLAY=1
        ;;
    "")
        # 兼容旧配置：没有profile时保留moonlight.conf中的显式开关。
        ;;
    *)
        log "未知串流 profile=$PROFILE，回退 smooth"
        PROFILE=smooth
        VSYNC=1
        FRAME_PACING=1
        ;;
    esac
}

# 默认路由出接口的累计收字节数，用于判断串流是否还有数据流入
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="${VDI_MOONLIGHT_PROGRAM:-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"
    # 按profile选择同步策略；默认保持当前稳定档。
    [ "$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 "启动串流 profile=${PROFILE:-config} decoder=$DECODER codec=$CODEC vsync=$VSYNC frame-pacing=$FRAME_PACING renderer=${SDL_RENDER_DRIVER:-} video-driver=${SDL_VIDEODRIVER:-}: $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
PROFILE="${VDI_STREAM_PROFILE:-$PROFILE}"
apply_profile
DECODER="${VDI_MOONLIGHT_DECODER:-$DECODER}"
CODEC="${VDI_MOONLIGHT_CODEC:-$CODEC}"
case "$DECODER" in
software|hardware|auto) ;;
*) log "未知解码器 $DECODER，回退 software"; DECODER=software ;;
esac
case "$CODEC" in
H.264|HEVC|AV1|auto) ;;
*) log "未知视频编码 $CODEC，回退 H.264"; CODEC=H.264 ;;
esac
PERFORMANCE_OVERLAY="${VDI_STREAM_STATS:-$PERFORMANCE_OVERLAY}"
if [ -z "$HOST" ]; then
    log "配置缺失或缺少 host: $CONF"
    exit 2
fi

export DISPLAY="${VDI_DISPLAY:-:0}"
export XAUTHORITY="${VDI_XAUTHORITY:-$HOME/.Xauthority}"
export SDL_RENDER_DRIVER="${VDI_SDL_RENDER_DRIVER:-opengles2}"
export SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR="${VDI_SDL_BYPASS_COMPOSITOR:-1}"
[ -n "${VDI_SDL_FORCE_EGL:-}" ] && export SDL_VIDEO_X11_FORCE_EGL="$VDI_SDL_FORCE_EGL"
[ -n "${VDI_SDL_VIDEODRIVER:-}" ] && export SDL_VIDEODRIVER="$VDI_SDL_VIDEODRIVER"

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
