package minecraft import ( "fmt" "os" "path/filepath" "strings" "zlh-agent/internal/provcommon" "zlh-agent/internal/state" ) /* Smart verification + self-repair for Minecraft installs. Layout: /opt/zlh///world - Ensures Java symlink exists and is executable - For Vanilla-like variants: verifies ONLY server.jar (NO start.sh) - For Forge/NeoForge: verifies run.sh, user_jvm_args.txt, libraries/, patched scripts - Automatic correction on failure (2 attempts) */ func VerifyMinecraftInstallWithRepair(cfg state.Config) error { const maxAttempts = 2 var lastErr error for attempt := 1; attempt <= maxAttempts; attempt++ { if attempt > 1 { fmt.Println("[verify] Attempt", attempt, "after correction") } if err := verifyMinecraftInstallOnce(cfg); err != nil { lastErr = err fmt.Println("[verify] Install verification failed:", err) // Try to correct issues before retrying if attempt < maxAttempts { if corrErr := correctMinecraftInstall(cfg); corrErr != nil { fmt.Println("[verify] Correction step failed:", corrErr) break } continue } break } fmt.Println("[verify] Minecraft installation verified OK") return nil } if lastErr == nil { lastErr = fmt.Errorf("minecraft installation invalid (unknown reason)") } return lastErr } // ----------------------------------------------------------------------------- // Single verification attempt (no repair) // ----------------------------------------------------------------------------- func verifyMinecraftInstallOnce(cfg state.Config) error { dir := provcommon.ServerDir(cfg) variant := strings.ToLower(cfg.Variant) // Java symlink must exist if err := verifyJavaSymlink(); err != nil { return err } // --------------------------------------------------------------------- // VANILLA / PAPER / PURPUR / FABRIC / QUILT → *ONLY REQUIRE server.jar* // --------------------------------------------------------------------- if variant == "vanilla" || variant == "paper" || variant == "purpur" || variant == "fabric" || variant == "quilt" { if err := verifyServerJar(dir); err != nil { return err } // ❌ Removed verifyStartScript(dir) // Vanilla-like variants DO NOT use start.sh return nil } // --------------------------------------------------------------------- // FORGE / NEOFORGE → require run.sh + libs + JVM args // --------------------------------------------------------------------- if variant == "forge" || variant == "neoforge" { if err := verifyForgeLayout(dir); err != nil { return err } return nil } return fmt.Errorf("unknown minecraft variant: %s", variant) } /* -------------------------------------------------------------------------- Java symlink check ----------------------------------------------------------------------------*/ func verifyJavaSymlink() error { linkPath := filepath.Join(provcommon.JavaRoot, "java") info, err := os.Lstat(linkPath) if err != nil { return fmt.Errorf("java symlink missing at %s: %w", linkPath, err) } if info.Mode()&os.ModeSymlink == 0 { return fmt.Errorf("expected java symlink at %s, found non-symlink", linkPath) } target, err := os.Readlink(linkPath) if err != nil { return fmt.Errorf("readlink java symlink: %w", err) } if !filepath.IsAbs(target) { target = filepath.Join(filepath.Dir(linkPath), target) } if tInfo, err := os.Stat(target); err != nil || tInfo.IsDir() { return fmt.Errorf("java symlink invalid: %s", target) } return nil } // ----------------------------------------------------------------------------- // Vanilla-like variants: require ONLY server.jar // ----------------------------------------------------------------------------- func verifyServerJar(dir string) error { jar := filepath.Join(dir, "server.jar") if _, err := os.Stat(jar); err != nil { return fmt.Errorf("server.jar missing in %s: %w", dir, err) } return nil } // ----------------------------------------------------------------------------- // Forge / NeoForge layout validation // ----------------------------------------------------------------------------- func verifyForgeLayout(dir string) error { runSh := filepath.Join(dir, "run.sh") info, err := os.Stat(runSh) if err != nil { return fmt.Errorf("forge requires run.sh but it is missing: %w", err) } if info.Mode()&0o111 == 0 { return fmt.Errorf("run.sh is not executable (%s)", runSh) } userArgs := filepath.Join(dir, "user_jvm_args.txt") if _, err := os.Stat(userArgs); err != nil { return fmt.Errorf("forge user_jvm_args.txt missing: %w", err) } libs := filepath.Join(dir, "libraries") if s, err := os.Stat(libs); err != nil || !s.IsDir() { return fmt.Errorf("forge libraries folder missing or not a directory: %w", err) } if err := verifyJavaPatchedInScripts(dir); err != nil { return err } return nil } func verifyJavaPatchedInScripts(dir string) error { runSh := filepath.Join(dir, "run.sh") data, err := os.ReadFile(runSh) if err != nil { return fmt.Errorf("read run.sh failed: %w", err) } if !strings.Contains(string(data), "/opt/zlh/runtime/java") { return fmt.Errorf("run.sh does not reference /opt/zlh/runtime/java") } return nil } /* -------------------------------------------------------------------------- Automatic correction logic ----------------------------------------------------------------------------*/ func correctMinecraftInstall(cfg state.Config) error { dir := provcommon.ServerDir(cfg) fmt.Println("[verify] Attempting automatic correction of Minecraft install") if err := ensureJavaSymlink(cfg); err != nil { fmt.Println("[verify] ensureJavaSymlink failed:", err) } if err := ensureScriptsPatched(dir); err != nil { fmt.Println("[verify] ensureScriptsPatched failed:", err) } // If start.sh exists (forge), ensure executable script := filepath.Join(dir, "start.sh") if info, err := os.Stat(script); err == nil { if info.Mode()&0o111 == 0 { _ = os.Chmod(script, info.Mode()|0o755) fmt.Println("[verify] made start.sh executable") } } return nil } func ensureJavaSymlink(cfg state.Config) error { if err := verifyJavaSymlink(); err == nil { return nil } return fmt.Errorf("java runtime invalid or missing") } func ensureScriptsPatched(dir string) error { entries, err := os.ReadDir(dir) if err != nil { return err } for _, e := range entries { if e.IsDir() { continue } name := e.Name() if !strings.HasSuffix(name, ".sh") && !strings.HasSuffix(name, ".txt") && !strings.HasSuffix(name, ".args") { continue } path := filepath.Join(dir, name) data, err := os.ReadFile(path) if err != nil { continue } content := string(data) patched := strings.ReplaceAll(content, "java ", "/opt/zlh/runtime/java ") if patched != content { fmt.Println("[verify] patching java call in", name) _ = os.WriteFile(path, []byte(patched), 0o755) } } return nil }