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

260 lines
7.3 KiB
Go
Executable File

package agenthttp
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"zlh-agent/internal/provision"
"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")
// Installer (downloads files, patches, configs, etc.)
if err := provision.ProvisionAll(*cfg); err != nil {
state.SetError(err)
state.SetState(state.StateError)
return err
}
// Extra Minecraft verification
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, no goto
----------------------------------------------------------------------------*/
func ensureProvisioned(cfg *state.Config) error {
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 // already provisioned
}
return runProvisionPipeline(cfg)
}
// ---------- VANILLA / PAPER / PURPUR / FABRIC ----------
jar := filepath.Join(dir, "server.jar")
if fileExists(jar) {
return nil // already provisioned
}
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
}
// Run provision + server start asynchronously
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
}
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 (does NOT re-provision)
----------------------------------------------------------------------------*/
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 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
}
_ = 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 + WebSocket
----------------------------------------------------------------------------*/
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
}