260 lines
7.3 KiB
Go
Executable File
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
|
|
}
|