package process import ( "fmt" "log" "os" "os/exec" "sync" "syscall" "time" ) // GameServer manages the game server process type GameServer struct { path string args string cmd *exec.Cmd mu sync.RWMutex startTime time.Time isRunning bool lastStatus string } // NewGameServer creates a new game server manager func NewGameServer(path, args string) *GameServer { return &GameServer{ path: path, args: args, lastStatus: "stopped", } } // Start starts the game server process func (gs *GameServer) Start() error { gs.mu.Lock() defer gs.mu.Unlock() if gs.isRunning { return fmt.Errorf("server is already running") } // Check if executable exists if _, err := os.Stat(gs.path); os.IsNotExist(err) { return fmt.Errorf("server executable not found: %s", gs.path) } log.Printf("Starting game server: %s %s", gs.path, gs.args) // Create command gs.cmd = exec.Command(gs.path) // Parse args if provided if gs.args != "" { // Simple space-split parsing (TODO: handle quoted args properly) gs.cmd.Args = append(gs.cmd.Args, splitArgs(gs.args)...) } // Set working directory to server directory gs.cmd.Dir = getDirectory(gs.path) // Redirect output to our logs gs.cmd.Stdout = os.Stdout gs.cmd.Stderr = os.Stderr // Start the process if err := gs.cmd.Start(); err != nil { gs.isRunning = false gs.lastStatus = "failed" return fmt.Errorf("failed to start server: %w", err) } gs.isRunning = true gs.startTime = time.Now() gs.lastStatus = "running" // Monitor process in background to prevent zombies go gs.monitorProcess() log.Printf("Game server started with PID %d", gs.cmd.Process.Pid) return nil } // Stop stops the game server process func (gs *GameServer) Stop() error { gs.mu.Lock() defer gs.mu.Unlock() if !gs.isRunning || gs.cmd == nil || gs.cmd.Process == nil { return fmt.Errorf("server is not running") } log.Printf("Stopping game server (PID %d)", gs.cmd.Process.Pid) // Send SIGTERM for graceful shutdown if err := gs.cmd.Process.Signal(syscall.SIGTERM); err != nil { log.Printf("Failed to send SIGTERM, forcing kill: %v", err) // Force kill if SIGTERM fails if killErr := gs.cmd.Process.Kill(); killErr != nil { return fmt.Errorf("failed to kill process: %w", killErr) } } // Wait for process to exit (with timeout) done := make(chan error, 1) go func() { done <- gs.cmd.Wait() }() select { case <-done: log.Println("Server stopped gracefully") case <-time.After(30 * time.Second): log.Println("Server did not stop gracefully, forcing kill") gs.cmd.Process.Kill() <-done } gs.isRunning = false gs.lastStatus = "stopped" gs.cmd = nil return nil } // Restart restarts the game server func (gs *GameServer) Restart() error { log.Println("Restarting game server...") // Stop if running if gs.isRunning { if err := gs.Stop(); err != nil { return fmt.Errorf("failed to stop server for restart: %w", err) } } // Wait a moment for cleanup time.Sleep(2 * time.Second) // Start again return gs.Start() } // Status returns the current server status func (gs *GameServer) Status() string { gs.mu.RLock() defer gs.mu.RUnlock() if !gs.isRunning { return "stopped" } // Check if process is actually alive if gs.cmd != nil && gs.cmd.Process != nil { // Send signal 0 to check if process exists if err := gs.cmd.Process.Signal(syscall.Signal(0)); err != nil { return "crashed" } } return gs.lastStatus } // Uptime returns how long the server has been running func (gs *GameServer) Uptime() time.Duration { gs.mu.RLock() defer gs.mu.RUnlock() if !gs.isRunning { return 0 } return time.Since(gs.startTime) } // IsRunning returns whether the server is currently running func (gs *GameServer) IsRunning() bool { gs.mu.RLock() defer gs.mu.RUnlock() return gs.isRunning } // monitorProcess waits for the process to exit and updates state // This prevents zombie processes by calling Wait() func (gs *GameServer) monitorProcess() { if gs.cmd == nil || gs.cmd.Process == nil { return } // Wait for process to exit (blocks until process dies) err := gs.cmd.Wait() gs.mu.Lock() defer gs.mu.Unlock() gs.isRunning = false if err != nil { log.Printf("Game server process exited with error: %v", err) gs.lastStatus = "crashed" } else { log.Println("Game server process exited normally") gs.lastStatus = "stopped" } // TODO: Could trigger crash recovery notification here } // Helper functions func getDirectory(path string) string { for i := len(path) - 1; i >= 0; i-- { if path[i] == '/' || path[i] == '\\' { return path[:i] } } return "." } func splitArgs(args string) []string { // Simple space-based splitting // TODO: Handle quoted strings properly for args with spaces var result []string current := "" for _, char := range args { if char == ' ' { if current != "" { result = append(result, current) current = "" } } else { current += string(char) } } if current != "" { result = append(result, current) } return result }