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.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 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.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 }