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

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
}