zlh-agent/internal/system/process.go

305 lines
5.9 KiB
Go
Executable File

package system
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"zlh-agent/internal/provision"
"zlh-agent/internal/runtime"
"zlh-agent/internal/state"
)
/* --------------------------------------------------------------------------
GLOBAL PROCESS STATE
----------------------------------------------------------------------------*/
var (
mu sync.Mutex
serverCmd *exec.Cmd
serverPTY *os.File
devCmd *exec.Cmd
devPTY *os.File
)
func GetServerPID() (int, bool) {
mu.Lock()
defer mu.Unlock()
if serverCmd == nil || serverCmd.Process == nil {
return 0, false
}
return serverCmd.Process.Pid, true
}
/* --------------------------------------------------------------------------
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
ptmx, err := runtime.CreatePTY(cmd)
if err != nil {
return fmt.Errorf("start server: %w", err)
}
serverCmd = cmd
serverPTY = ptmx
state.ClearIntentionalStop()
state.SetState(state.StateRunning)
state.SetError(nil)
state.SetReadyState(false, "", "")
go func() {
err := cmd.Wait()
mu.Lock()
defer mu.Unlock()
if serverPTY != nil {
_ = serverPTY.Close()
}
if state.IsIntentionalStop() {
state.ClearIntentionalStop()
state.SetState(state.StateIdle)
state.SetReadyState(false, "", "")
} else if err != nil {
state.RecordCrash(err)
} else {
state.SetState(state.StateIdle)
state.SetReadyState(false, "", "")
}
serverCmd = nil
serverPTY = nil
}()
return nil
}
/* --------------------------------------------------------------------------
StopServer
----------------------------------------------------------------------------*/
func StopServer() error {
mu.Lock()
defer mu.Unlock()
if serverCmd == nil {
return fmt.Errorf("server not running")
}
state.SetState(state.StateStopping)
state.MarkIntentionalStop()
state.SetReadyState(false, "", "")
// Try graceful stop
if serverPTY != nil {
_ = runtime.Write(serverPTY, []byte("save-all\n"))
time.Sleep(2 * time.Second)
_ = runtime.Write(serverPTY, []byte("stop\n"))
}
// Wait a moment
time.Sleep(4 * time.Second)
// If still running, force kill
if serverCmd.Process != nil {
_ = serverCmd.Process.Kill()
}
return nil
}
func WaitForServerExit(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for {
mu.Lock()
running := serverCmd != nil
mu.Unlock()
if !running {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for server process to exit")
}
time.Sleep(200 * time.Millisecond)
}
}
/* --------------------------------------------------------------------------
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 serverPTY == nil {
return fmt.Errorf("server console not available")
}
return runtime.Write(serverPTY, []byte(cmd+"\n"))
}
/* --------------------------------------------------------------------------
Dev Shell PTY
----------------------------------------------------------------------------*/
func StartDevShell() (*os.File, error) {
mu.Lock()
defer mu.Unlock()
if devPTY != nil && devCmd != nil {
return devPTY, nil
}
shell := "/bin/bash"
if _, err := os.Stat(shell); err != nil {
shell = "/bin/sh"
}
var cmd *exec.Cmd
if shell == "/bin/bash" {
cmd = exec.Command(shell, "-l", "-i")
} else {
cmd = exec.Command(shell, "-i")
}
cmd.Dir = "/opt"
ptmx, err := runtime.CreatePTY(cmd)
if err != nil {
return nil, fmt.Errorf("start dev shell: %w", err)
}
devCmd = cmd
devPTY = ptmx
state.SetState(state.StateRunning)
state.SetError(nil)
go func() {
err := cmd.Wait()
mu.Lock()
defer mu.Unlock()
if devPTY != nil {
_ = devPTY.Close()
}
if err != nil {
state.RecordCrash(err)
} else {
state.SetState(state.StateIdle)
}
devCmd = nil
devPTY = nil
}()
return devPTY, nil
}
func GetConsolePTY(cfg *state.Config) (*os.File, error) {
if cfg.ContainerType == "dev" {
return StartDevShell()
}
mu.Lock()
defer mu.Unlock()
if serverPTY == nil {
return nil, fmt.Errorf("server console not available")
}
return serverPTY, nil
}
func WriteConsoleInput(cfg *state.Config, input string) error {
if strings.HasSuffix(input, "\n") {
input = strings.TrimSuffix(input, "\n")
}
payload := []byte(input + "\n")
if cfg.ContainerType == "dev" {
mu.Lock()
defer mu.Unlock()
if devPTY == nil {
return fmt.Errorf("dev shell not available")
}
return runtime.Write(devPTY, payload)
}
mu.Lock()
defer mu.Unlock()
if serverPTY == nil {
return fmt.Errorf("server console not available")
}
return runtime.Write(serverPTY, payload)
}
/* --------------------------------------------------------------------------
Stop Dev Shell
----------------------------------------------------------------------------*/
func StopDevShell() error {
mu.Lock()
defer mu.Unlock()
if devCmd == nil {
return nil
}
if devPTY != nil {
_ = runtime.Write(devPTY, []byte("exit\n"))
}
time.Sleep(1 * time.Second)
if devCmd.Process != nil {
_ = devCmd.Process.Kill()
}
if devPTY != nil {
_ = devPTY.Close()
devPTY = nil
}
devCmd = nil
state.SetState(state.StateIdle)
return nil
}