264 lines
6.8 KiB
Go
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
|
|
}
|