package agenthttp import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "time" "zlh-agent/internal/provision" "zlh-agent/internal/provision/devcontainer" "zlh-agent/internal/provision/devcontainer/go" "zlh-agent/internal/provision/devcontainer/java" "zlh-agent/internal/provision/devcontainer/node" "zlh-agent/internal/provision/devcontainer/python" "zlh-agent/internal/provision/minecraft" "zlh-agent/internal/state" "zlh-agent/internal/system" ) /* -------------------------------------------------------------------------- Helpers ----------------------------------------------------------------------------*/ func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func dirExists(path string) bool { s, err := os.Stat(path) return err == nil && s.IsDir() } /* -------------------------------------------------------------------------- Shared provision pipeline (installer + Minecraft verify) ----------------------------------------------------------------------------*/ func runProvisionPipeline(cfg *state.Config) error { state.SetState(state.StateInstalling) state.SetInstallStep("provision_all") if err := provision.ProvisionAll(*cfg); err != nil { state.SetError(err) state.SetState(state.StateError) return err } // Extra Minecraft verification ONLY for Minecraft if strings.ToLower(cfg.Game) == "minecraft" { if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil { state.SetError(err) state.SetState(state.StateError) return fmt.Errorf("minecraft verification failed: %w", err) } } state.SetInstallStep("") state.SetState(state.StateIdle) return nil } /* -------------------------------------------------------------------------- ensureProvisioned() — idempotent, unified ----------------------------------------------------------------------------*/ func ensureProvisioned(cfg *state.Config) error { /* --------------------------------------------------------- DEV CONTAINERS (FIRST-CLASS) --------------------------------------------------------- */ if cfg.ContainerType == "dev" { // If not provisioned yet, install if !devcontainer.IsProvisioned() { return runProvisionPipeline(cfg) } // Verify runtime switch strings.ToLower(cfg.Runtime) { case "node": return node.Verify(*cfg) case "python": return python.Verify(*cfg) case "go": return goenv.Verify(*cfg) case "java": return java.Verify(*cfg) default: return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime) } } /* --------------------------------------------------------- GAME SERVERS (EXISTING LOGIC) --------------------------------------------------------- */ dir := provision.ServerDir(*cfg) game := strings.ToLower(cfg.Game) variant := strings.ToLower(cfg.Variant) _ = os.MkdirAll(dir, 0o755) isSteam := provision.IsSteamGame(game) isForgeLike := variant == "forge" || variant == "neoforge" // ---------- STEAM GAMES ALWAYS REQUIRE PROVISION ---------- if isSteam { return runProvisionPipeline(cfg) } // ---------- FORGE / NEOFORGE ---------- if isForgeLike { runSh := filepath.Join(dir, "run.sh") libraries := filepath.Join(dir, "libraries") if fileExists(runSh) && dirExists(libraries) { return nil } return runProvisionPipeline(cfg) } // ---------- VANILLA / PAPER / PURPUR / FABRIC ---------- jar := filepath.Join(dir, "server.jar") if fileExists(jar) { return nil } return runProvisionPipeline(cfg) } /* -------------------------------------------------------------------------- /config — the REAL provisioning trigger (async) ----------------------------------------------------------------------------*/ func handleConfig(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST only", http.StatusMethodNotAllowed) return } body, _ := io.ReadAll(r.Body) var cfg state.Config if err := json.Unmarshal(body, &cfg); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } if err := state.SaveConfig(&cfg); err != nil { http.Error(w, "save config failed: "+err.Error(), http.StatusInternalServerError) return } go func(c state.Config) { log.Println("[agent] async provision+start begin") if err := ensureProvisioned(&c); err != nil { log.Println("[agent] provision error:", err) return } // Dev containers may not start a server process if c.ContainerType != "dev" { if err := system.StartServer(&c); err != nil { log.Println("[agent] start error:", err) state.SetError(err) state.SetState(state.StateError) return } } log.Println("[agent] async provision+start complete") }(cfg) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{"ok": true, "state": "installing"}`)) } /* -------------------------------------------------------------------------- /start — manual UI start ----------------------------------------------------------------------------*/ func handleStart(w http.ResponseWriter, r *http.Request) { cfg, err := state.LoadConfig() if err != nil { http.Error(w, "no config: "+err.Error(), http.StatusBadRequest) return } if cfg.ContainerType == "dev" { http.Error(w, "dev containers do not support manual start", http.StatusBadRequest) return } if err := system.StartServer(cfg); err != nil { http.Error(w, "start error: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"ok": true, "state": "starting"}`)) } /* -------------------------------------------------------------------------- /stop ----------------------------------------------------------------------------*/ func handleStop(w http.ResponseWriter, r *http.Request) { if err := system.StopServer(); err != nil { http.Error(w, "stop error: "+err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } /* -------------------------------------------------------------------------- /restart ----------------------------------------------------------------------------*/ func handleRestart(w http.ResponseWriter, r *http.Request) { cfg, err := state.LoadConfig() if err != nil { http.Error(w, "no config", http.StatusBadRequest) return } if cfg.ContainerType == "dev" { http.Error(w, "dev containers do not support restart", http.StatusBadRequest) return } _ = system.StopServer() if err := system.StartServer(cfg); err != nil { http.Error(w, "restart error: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"ok": true, "state": "starting"}`)) } /* -------------------------------------------------------------------------- /status — API polls this ----------------------------------------------------------------------------*/ func handleStatus(w http.ResponseWriter, r *http.Request) { cfg, _ := state.LoadConfig() resp := map[string]any{ "state": state.GetState(), "installStep": state.GetInstallStep(), "crashCount": state.GetCrashCount(), "error": nil, "config": cfg, "timestamp": time.Now().Unix(), } if err := state.GetError(); err != nil { resp["error"] = err.Error() } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) } /* -------------------------------------------------------------------------- /console/command ----------------------------------------------------------------------------*/ func handleSendCommand(w http.ResponseWriter, r *http.Request) { cmd := r.URL.Query().Get("cmd") if cmd == "" { http.Error(w, "cmd required", http.StatusBadRequest) return } if err := system.SendConsoleCommand(cmd); err != nil { http.Error(w, "command error: "+err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } /* -------------------------------------------------------------------------- Router ----------------------------------------------------------------------------*/ func NewMux() *http.ServeMux { m := http.NewServeMux() m.HandleFunc("/config", handleConfig) m.HandleFunc("/start", handleStart) m.HandleFunc("/stop", handleStop) m.HandleFunc("/restart", handleRestart) m.HandleFunc("/status", handleStatus) m.HandleFunc("/console/command", handleSendCommand) registerWebSocket(m) m.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) log.Println("[agent] routes registered") return m }