package server import ( "context" "database/sql" "net" "strings" "time" "github.com/gin-gonic/gin" "vdi-cloud/platform/internal/model" ) type desktopReadinessCheck struct { Name string `json:"name"` Status string `json:"status"` Message string `json:"message"` } type desktopReadiness struct { DesktopID string `json:"desktopId"` ObservedAt time.Time `json:"observedAt"` Ready bool `json:"ready"` PowerState string `json:"powerState"` ServiceState string `json:"serviceState"` SetupRequired bool `json:"setupRequired"` Checks []desktopReadinessCheck `json:"checks"` } // checkDesktopReadiness verifies the parts of the provisioning contract that // the platform can observe without logging into the guest OS. It never starts // or stops a VM and never changes guest configuration. func (s *Server) checkDesktopReadiness(c *gin.Context) { ctx := c.Request.Context() var desktop model.Desktop if err := s.DB.GetContext(ctx, &desktop, "SELECT * FROM desktops WHERE id=$1", c.Param("id")); err != nil { if err == sql.ErrNoRows { s.fail(c, 404, "desktop_not_found", "桌面不存在") } else { s.dbError(c, err) } return } result := desktopReadiness{ DesktopID: desktop.ID, ObservedAt: time.Now().UTC(), Ready: false, PowerState: desktop.PowerState, ServiceState: desktop.ServiceState, SetupRequired: desktop.SetupRequired, Checks: []desktopReadinessCheck{}, } add := func(name, status, message string) { result.Checks = append(result.Checks, desktopReadinessCheck{Name: name, Status: status, Message: message}) } if desktop.Provider != "proxmox" { add("provider", "fail", "桌面不是 Proxmox 资源,无法执行真实连接探测") c.JSON(200, result) return } if desktop.Endpoint == "" { add("endpoint", "fail", "尚未配置桌面 IP 或主机名") } else { add("endpoint", "pass", "已配置桌面连接地址") } if desktop.SetupRequired { add("initialization", "fail", "尚未确认系统身份、硬件编码、Sunshine 和终端配对") } else { add("initialization", "pass", "已完成平台初始化确认") } pve := s.currentPVE() if pve == nil { add("power", "unknown", "尚未配置 Proxmox,无法读取真实电源状态") } else { power, err := pve.Power(ctx, desktop.Node, desktop.VMID) if err != nil { result.PowerState = "unknown" add("power", "unknown", "无法读取 Proxmox 实际电源状态") } else { result.PowerState = power if power == "running" { add("power", "pass", "虚拟机正在运行") } else { add("power", "fail", "虚拟机未运行,请先开机") } } } serviceReady := false if desktop.Endpoint != "" && result.PowerState == "running" { serviceReady = sunshineReady(ctx, desktop.Endpoint) if serviceReady { result.ServiceState = "ready" add("sunshine", "pass", "Sunshine serverinfo 已响应") } else { result.ServiceState = "unknown" add("sunshine", "fail", "Sunshine 未响应,请检查服务、网络和硬件编码配置") } } else { add("sunshine", "unknown", "虚拟机未运行或尚未配置连接地址,暂不探测 Sunshine") } result.Ready = !desktop.SetupRequired && result.PowerState == "running" && serviceReady if _, err := s.DB.ExecContext(ctx, "UPDATE desktops SET power_state=$1,service_state=$2,updated_at=now() WHERE id=$3", result.PowerState, result.ServiceState, desktop.ID); err != nil { s.dbError(c, err) return } c.JSON(200, result) } // sunshineReadyAt is split out so the real probe remains fixed to Sunshine's // serverinfo port while tests can use an isolated local listener. func sunshineReadyAt(ctx context.Context, endpoint, port string) bool { host := endpoint if parsed, _, err := net.SplitHostPort(endpoint); err == nil { host = parsed } if strings.ContainsAny(host, "/\\@?#") || host == "" { return false } return sunshineHTTPProbe(ctx, host, port) }