zlh-agent/internal/provision/minecraft/verify.go
2025-12-13 20:54:18 +00:00

264 lines
6.8 KiB
Go

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/<game>/<variant>/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
}