Skip to content

Commit 3a5821f

Browse files
committed
Stop untracked games by configured process name
1 parent 28351d8 commit 3a5821f

2 files changed

Lines changed: 120 additions & 23 deletions

File tree

internal/mcp/game_stop_fix_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package mcp
33
import (
44
"encoding/json"
55
"os"
6+
"os/exec"
67
"path/filepath"
8+
"runtime"
79
"strings"
810
"testing"
11+
"time"
912

1013
"github.com/pardeike/gabs/internal/config"
1114
"github.com/pardeike/gabs/internal/util"
@@ -361,6 +364,69 @@ func TestGameStopFix(t *testing.T) {
361364
})
362365
}
363366

367+
func TestStopUntrackedGameUsesStopProcessName(t *testing.T) {
368+
if runtime.GOOS == "windows" {
369+
t.Skip("test uses Unix process-name lookup")
370+
}
371+
372+
tempDir, err := os.MkdirTemp("", "gabs_untracked_stop_test")
373+
if err != nil {
374+
t.Fatal(err)
375+
}
376+
defer os.RemoveAll(tempDir)
377+
378+
processName := "gabs-stop-fallback"
379+
processPath := filepath.Join(tempDir, processName)
380+
if err := os.Symlink("/bin/sleep", processPath); err != nil {
381+
t.Fatalf("failed to create process-name symlink: %v", err)
382+
}
383+
384+
cmd := exec.Command(processPath, "30")
385+
if err := cmd.Start(); err != nil {
386+
t.Fatalf("failed to start test process: %v", err)
387+
}
388+
t.Cleanup(func() {
389+
_ = cmd.Process.Kill()
390+
_, _ = cmd.Process.Wait()
391+
})
392+
393+
deadline := time.Now().Add(2 * time.Second)
394+
for {
395+
if out, err := exec.Command("pgrep", "-x", processName).Output(); err == nil && strings.TrimSpace(string(out)) != "" {
396+
break
397+
}
398+
if time.Now().After(deadline) {
399+
t.Fatalf("test process %q never became visible to pgrep", processName)
400+
}
401+
time.Sleep(25 * time.Millisecond)
402+
}
403+
404+
logger := util.NewLogger("info")
405+
server := NewServerForTesting(logger)
406+
game := config.GameConfig{
407+
ID: "untracked-steam-game",
408+
Name: "Untracked Steam Game",
409+
LaunchMode: "SteamAppId",
410+
Target: "123456",
411+
StopProcessName: processName,
412+
}
413+
414+
if err := server.stopGame(game, false); err != nil {
415+
t.Fatalf("expected untracked game stop to use stopProcessName, got: %v", err)
416+
}
417+
418+
deadline = time.Now().Add(2 * time.Second)
419+
for {
420+
if out, err := exec.Command("pgrep", "-x", processName).Output(); err != nil || strings.TrimSpace(string(out)) == "" {
421+
return
422+
}
423+
if time.Now().After(deadline) {
424+
t.Fatalf("test process %q was still running after stopGame fallback", processName)
425+
}
426+
time.Sleep(25 * time.Millisecond)
427+
}
428+
}
429+
364430
// TestImprovedStatusReporting verifies the enhanced status descriptions
365431
func TestImprovedStatusReporting(t *testing.T) {
366432
logger := util.NewLogger("info")

internal/mcp/stdio_server.go

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2948,7 +2948,7 @@ func (s *Server) checkGameStatus(gameID string) string {
29482948
game := s.getGameFromController(controller)
29492949
if game != nil && game.StopProcessName != "" {
29502950
// We have tracking capability but game is not running
2951-
s.cleanupStoppedGame(gameID)
2951+
s.cleanupStoppedGameLocked(gameID)
29522952
return "stopped"
29532953
} else {
29542954
// We don't have tracking capability, so we can't know the real status
@@ -2966,12 +2966,12 @@ func (s *Server) checkGameStatus(gameID string) string {
29662966
}
29672967

29682968
// Process is dead, clean up
2969-
s.cleanupStoppedGame(gameID)
2969+
s.cleanupStoppedGameLocked(gameID)
29702970
return "stopped"
29712971
}
29722972

2973-
// cleanupStoppedGame centralizes the cleanup logic for stopped games
2974-
func (s *Server) cleanupStoppedGame(gameID string) {
2973+
// cleanupStoppedGameLocked centralizes cleanup when s.mu is already held.
2974+
func (s *Server) cleanupStoppedGameLocked(gameID string) {
29752975
// Remove from games map - no need for complex cleanup in stateless approach
29762976
delete(s.games, gameID)
29772977

@@ -2984,18 +2984,16 @@ func (s *Server) cleanupStoppedGame(gameID string) {
29842984
s.log.Debugw("cleaned up dead game process and resources", "gameId", gameID)
29852985
}
29862986

2987+
func (s *Server) cleanupStoppedGame(gameID string) {
2988+
s.mu.Lock()
2989+
defer s.mu.Unlock()
2990+
s.cleanupStoppedGameLocked(gameID)
2991+
}
2992+
29872993
// startGame starts a game process using the serialized starter approach
29882994
// This implements @pardeike's requirements for serialized, verified process starting
29892995
func (s *Server) startGame(game config.GameConfig, gamesConfig *config.GamesConfig, backoffMin, backoffMax time.Duration) (*process.ProcessStartResult, error) {
2990-
// Convert GameConfig to LaunchSpec
2991-
launchSpec := process.LaunchSpec{
2992-
GameId: game.ID,
2993-
Mode: game.LaunchMode,
2994-
PathOrId: game.Target,
2995-
Args: game.Args,
2996-
WorkingDir: game.WorkingDir,
2997-
StopProcessName: game.StopProcessName,
2998-
}
2996+
launchSpec := launchSpecFromGame(game)
29992997

30002998
// Create and configure controller
30012999
controller := process.NewController()
@@ -3304,13 +3302,24 @@ func (s *Server) exposeGABPResources(client *gabp.Client, gameID string) error {
33043302
return nil
33053303
}
33063304

3305+
func launchSpecFromGame(game config.GameConfig) process.LaunchSpec {
3306+
return process.LaunchSpec{
3307+
GameId: game.ID,
3308+
Mode: game.LaunchMode,
3309+
PathOrId: game.Target,
3310+
Args: game.Args,
3311+
WorkingDir: game.WorkingDir,
3312+
StopProcessName: game.StopProcessName,
3313+
}
3314+
}
3315+
33073316
// stopGame stops a game process gracefully or by force
33083317
func (s *Server) stopGame(game config.GameConfig, force bool) error {
33093318
s.mu.Lock()
33103319
controller, exists := s.games[game.ID]
33113320
if !exists {
33123321
s.mu.Unlock()
3313-
return fmt.Errorf("game %s is not running (no process tracked)", game.ID)
3322+
return s.stopUntrackedGame(game, force)
33143323
}
33153324

33163325
launchMode := controller.GetLaunchMode()
@@ -3319,6 +3328,8 @@ func (s *Server) stopGame(game config.GameConfig, force bool) error {
33193328
delete(s.games, game.ID)
33203329
s.mu.Unlock()
33213330

3331+
defer s.cleanupStoppedGame(game.ID)
3332+
33223333
// Handle different launch modes differently
33233334
if launchMode == "SteamAppId" || launchMode == "EpicAppId" {
33243335
// For Steam/Epic games, try to use stopProcessName first if available
@@ -3364,18 +3375,38 @@ func (s *Server) stopGame(game config.GameConfig, force bool) error {
33643375
s.log.Infow("game stopped", "gameId", game.ID, "pid", controller.GetPID())
33653376
}
33663377

3367-
// Cleanup GABP connections and mirrored tools when game stops
3368-
// This involves:
3369-
// 1. Disconnecting any active GABP client for this game
3370-
// 2. Unregistering all game-specific tools (gameId.* tools)
3371-
// 3. Cleaning up bridge configuration files
3372-
s.CleanupGABPConnection(game.ID)
3373-
s.CleanupGameResources(game.ID)
3374-
s.CleanupBridgeConfig(game.ID)
3375-
33763378
return err
33773379
}
33783380

3381+
func (s *Server) stopUntrackedGame(game config.GameConfig, force bool) error {
3382+
if game.StopProcessName == "" {
3383+
return fmt.Errorf("game %s is not running (no process tracked)", game.ID)
3384+
}
3385+
3386+
controller := process.NewController()
3387+
if err := controller.Configure(launchSpecFromGame(game)); err != nil {
3388+
return fmt.Errorf("failed to configure fallback stop controller for %s: %w", game.ID, err)
3389+
}
3390+
3391+
if !controller.IsRunning() {
3392+
return fmt.Errorf("game %s is not running (no process tracked; no process named %q found)", game.ID, game.StopProcessName)
3393+
}
3394+
3395+
var err error
3396+
if force {
3397+
err = controller.Kill()
3398+
} else {
3399+
err = controller.Stop(3 * time.Second)
3400+
}
3401+
if err != nil {
3402+
return err
3403+
}
3404+
3405+
s.log.Infow("untracked game stopped via configured process name", "gameId", game.ID, "processName", game.StopProcessName, "force", force)
3406+
s.cleanupStoppedGame(game.ID)
3407+
return nil
3408+
}
3409+
33793410
func (s *Server) ServeStdio(ctx context.Context) error {
33803411
return s.Serve(os.Stdin, os.Stdout)
33813412
}

0 commit comments

Comments
 (0)