package minecraft import ( "fmt" "log" "os" "path/filepath" "strconv" "strings" "time" "zlh-agent/internal/provision" "zlh-agent/internal/state" ) func WaitUntilReady(cfg state.Config, timeout, interval time.Duration) error { start := time.Now() ports := candidatePorts(cfg) protocols := []int{ProtocolForVersion(cfg.Version), 767, 765, 763, 762, 754} protocols = dedupeInts(protocols) attempt := 0 deadline := start.Add(timeout) var lastErr error for { attempt++ for _, port := range ports { for _, protocol := range protocols { if _, err := QueryStatus("127.0.0.1", port, protocol); err == nil { elapsed := time.Since(start).Milliseconds() log.Printf("[lifecycle] vmid=%d phase=probe result=ready attempt=%d elapsed_ms=%d port=%d protocol=%d", cfg.VMID, attempt, elapsed, port, protocol) return nil } else { lastErr = err } } } elapsed := time.Since(start).Milliseconds() log.Printf("[lifecycle] vmid=%d phase=probe result=not_ready attempt=%d elapsed_ms=%d err=%v", cfg.VMID, attempt, elapsed, lastErr) if time.Now().After(deadline) { if lastErr != nil { return fmt.Errorf("minecraft readiness probe timeout after %s: %w", timeout, lastErr) } return fmt.Errorf("minecraft readiness probe timeout after %s", timeout) } time.Sleep(interval) } } func candidatePorts(cfg state.Config) []int { ports := make([]int, 0, 3) propsPath := filepath.Join(provision.ServerDir(cfg), "server.properties") if b, err := os.ReadFile(propsPath); err == nil { lines := strings.Split(string(b), "\n") for _, l := range lines { if strings.HasPrefix(l, "server-port=") { if p, err := strconv.Atoi(strings.TrimPrefix(l, "server-port=")); err == nil && p > 0 { ports = append(ports, p) } break } } } if len(cfg.Ports) > 0 && cfg.Ports[0] > 0 { ports = append(ports, cfg.Ports[0]) } ports = append(ports, 25565) return dedupeInts(ports) } func dedupeInts(in []int) []int { seen := make(map[int]struct{}, len(in)) out := make([]int, 0, len(in)) for _, v := range in { if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } return out }