218 lines
5.0 KiB
Go
Executable File
218 lines
5.0 KiB
Go
Executable File
package system
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings" // <-- ADD THIS
|
|
"sync"
|
|
"time"
|
|
|
|
"zlh-agent/internal/provision"
|
|
"zlh-agent/internal/state"
|
|
)
|
|
|
|
|
|
/* --------------------------------------------------------------------------
|
|
GLOBAL PROCESS STATE
|
|
----------------------------------------------------------------------------*/
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
serverCmd *exec.Cmd
|
|
serverStdin io.WriteCloser
|
|
)
|
|
|
|
/* --------------------------------------------------------------------------
|
|
StartServer (fixed)
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func StartServer(cfg *state.Config) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
// Already running?
|
|
if serverCmd != nil {
|
|
return fmt.Errorf("server already running")
|
|
}
|
|
|
|
dir := provision.ServerDir(*cfg)
|
|
startScript := filepath.Join(dir, "start.sh")
|
|
|
|
cmd := exec.Command("/bin/bash", startScript)
|
|
cmd.Dir = dir
|
|
|
|
stdout, _ := cmd.StdoutPipe()
|
|
stderr, _ := cmd.StderrPipe()
|
|
stdin, _ := cmd.StdinPipe()
|
|
|
|
serverStdin = stdin
|
|
serverCmd = cmd
|
|
|
|
// Mark STARTING (not running)
|
|
state.SetState(state.StateStarting)
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
serverCmd = nil
|
|
return fmt.Errorf("start server: %w", err)
|
|
}
|
|
|
|
/* -------------------------
|
|
Log pumps
|
|
--------------------------*/
|
|
go pumpOutput(stdout, os.Stdout)
|
|
go pumpOutput(stderr, os.Stderr)
|
|
|
|
/* -------------------------
|
|
Detect "Done" → running
|
|
--------------------------*/
|
|
go detectMinecraftReady(cfg)
|
|
|
|
/* -------------------------
|
|
Crash watcher
|
|
--------------------------*/
|
|
go func() {
|
|
err := cmd.Wait()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if err != nil {
|
|
state.RecordCrash(err)
|
|
serverCmd = nil
|
|
serverStdin = nil
|
|
return
|
|
}
|
|
|
|
// Normal stop
|
|
state.SetState(state.StateIdle)
|
|
serverCmd = nil
|
|
serverStdin = nil
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
/* helper to pump logs */
|
|
func pumpOutput(r io.Reader, w *os.File) {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := r.Read(buf)
|
|
if n > 0 {
|
|
w.Write(buf[:n])
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Detects Minecraft "Done" and updates state */
|
|
func detectMinecraftReady(cfg *state.Config) {
|
|
dir := provision.ServerDir(*cfg)
|
|
logPath := filepath.Join(dir, "logs", "latest.log")
|
|
|
|
deadline := time.Now().Add(5 * time.Minute) // FORGE NEEDS MORE TIME
|
|
|
|
lastSize := int64(0)
|
|
|
|
for time.Now().Before(deadline) {
|
|
|
|
// Wait for log file to appear
|
|
st, err := os.Stat(logPath)
|
|
if err == nil {
|
|
// ensure file is growing
|
|
if st.Size() != lastSize {
|
|
lastSize = st.Size()
|
|
|
|
b, _ := os.ReadFile(logPath)
|
|
s := string(b)
|
|
|
|
// UNIVERSAL READY MATCHES
|
|
if strings.Contains(s, "Done (") ||
|
|
strings.Contains(s, "For help, type \"help\"") ||
|
|
strings.Contains(s, "Successfully loaded forge") ||
|
|
strings.Contains(s, "Preparing spawn area: 100%") {
|
|
|
|
state.SetState(state.StateRunning)
|
|
state.SetError(nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
state.SetState(state.StateError)
|
|
state.SetError(fmt.Errorf("server failed to reach running state before timeout"))
|
|
}
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------------------
|
|
StopServer
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func StopServer() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if serverCmd == nil {
|
|
return fmt.Errorf("server not running")
|
|
}
|
|
|
|
state.SetState(state.StateStopping)
|
|
|
|
// Try graceful stop
|
|
if serverStdin != nil {
|
|
_, _ = serverStdin.Write([]byte("save-all\n"))
|
|
time.Sleep(2 * time.Second)
|
|
_, _ = serverStdin.Write([]byte("stop\n"))
|
|
}
|
|
|
|
// Wait a moment
|
|
time.Sleep(4 * time.Second)
|
|
|
|
// If still running, force kill
|
|
if serverCmd.Process != nil {
|
|
_ = serverCmd.Process.Kill()
|
|
}
|
|
|
|
state.SetState(state.StateIdle)
|
|
serverCmd = nil
|
|
serverStdin = nil
|
|
return nil
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
RestartServer
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func RestartServer(cfg *state.Config) error {
|
|
if err := StopServer(); err != nil {
|
|
// ignore if not running
|
|
}
|
|
|
|
return StartServer(cfg)
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
SendConsoleCommand
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func SendConsoleCommand(cmd string) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if serverStdin == nil {
|
|
return fmt.Errorf("server console not available")
|
|
}
|
|
|
|
_, err := serverStdin.Write([]byte(cmd + "\n"))
|
|
return err
|
|
}
|