@@ -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
29892995func (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
33083317func (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+
33793410func (s * Server ) ServeStdio (ctx context.Context ) error {
33803411 return s .Serve (os .Stdin , os .Stdout )
33813412}
0 commit comments