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 }