package game // The shutdown command schedules a graceful server shutdown. Signals and // this command converge on Game.RequestShutdown, so both paths drain work, // resolve outstanding wagers, and save owned state before transports close. import ( "fmt" "strconv" "time" "thehouseoficarus/internal/net" ) // scheduledShutdown carries one countdown run's captured cancel channel, so // a later run (or cancel) can never close or read a replacement run's // channel. type scheduledShutdown struct { cancel chan struct{} } func (g *Game) executeShutdown(sess *net.Session, args []string, rawInput string) { if !g.checkAdmin(sess) { sess.WriteLine("Unknown command.") return } if len(args) == 0 { sess.WriteLine("Usage: shutdown or shutdown cancel") return } if args[0] == "cancel" { g.shutdownMu.Lock() run := g.shutdownRun g.shutdownMu.Unlock() if run == nil { sess.WriteLine("No shutdown in progress.") return } close(run.cancel) g.shutdownMu.Lock() g.shutdownRun = nil g.shutdownMu.Unlock() g.broadcastLoggedIn("[Server] Shutdown cancelled.") return } minutes, err := strconv.Atoi(args[0]) if err != nil || minutes <= 0 { sess.WriteLine("Invalid minutes.") return } g.shutdownMu.Lock() if g.shutdownRun != nil { g.shutdownMu.Unlock() sess.WriteLine("A shutdown is already in progress.") return } run := &scheduledShutdown{cancel: make(chan struct{})} g.shutdownRun = run g.shutdownMu.Unlock() go g.runShutdownCountdown(run, minutes) sess.WriteLine(fmt.Sprintf("Shutdown scheduled in %d minute(s).", minutes)) } // runShutdownCountdown announces the countdown and requests the shutdown // at expiry. Cancellation is handled through the run's captured channel; // nothing reads a replaceable shared field here. func (g *Game) runShutdownCountdown(run *scheduledShutdown, minutes int) { remaining := minutes * 60 ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() g.broadcastLoggedIn(fmt.Sprintf("[Server] Shutting down in %d minute(s).", minutes)) for { select { case <-ticker.C: remaining -= 30 if remaining <= 0 { g.broadcastLoggedIn("[Server] Shutting down NOW.") if g.shutdownHook != nil { g.shutdownHook("scheduled shutdown") } return } mins := remaining / 60 secs := remaining % 60 if secs == 0 { g.broadcastLoggedIn(fmt.Sprintf("[Server] Shutting down in %d minute(s).", mins)) } else { g.broadcastLoggedIn(fmt.Sprintf("[Server] Shutting down in %d minute(s) %d seconds.", mins, secs)) } case <-run.cancel: return } } } // broadcastLoggedIn writes one line to every logged-in player. Read-only // over Hub membership (mutex-protected); writes go through outbound queues. func (g *Game) broadcastLoggedIn(msg string) { if g.Hub == nil { return } for _, other := range g.Hub.AllSessions() { if other.Player != nil { other.WriteLine(msg) } } }