fix shutdown bug

main
DustyP 9 months ago
parent fbaf70bafc
commit 3ad2c294d2

@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"context"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@ -49,12 +50,16 @@ func main() {
close(eventBroadcaster) close(eventBroadcaster)
}() }()
// Create a context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start web interface if enabled // Start web interface if enabled
if !*noWeb { if !*noWeb {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
startWebInterface(clock, eventBroadcaster, *webPort, sigChan) startWebInterface(clock, eventBroadcaster, *webPort, ctx)
}() }()
} }
@ -63,26 +68,30 @@ func main() {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
startTerminalInterface(clock, eventBroadcaster, sigChan) startTerminalInterface(clock, eventBroadcaster, ctx)
}() }()
} }
// Wait for signal to exit // Wait for signal to exit
<-sigChan <-sigChan
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
time.Sleep(500 * time.Millisecond) // Give a moment for any pending operations to complete
// Cancel context to signal all components to shut down
cancel()
// Give a moment for any pending operations to complete
time.Sleep(500 * time.Millisecond)
// Wait for all interfaces to shut down // Wait for all interfaces to shut down
wg.Wait() wg.Wait()
} }
// startWebInterface initializes and runs the web interface // startWebInterface initializes and runs the web interface
func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, sigChan chan os.Signal) { func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, ctx context.Context) {
// Create and start the web server // Create and start the web server
server, err := web.NewServer(clock, events, webPort) server, err := web.NewServer(clock, events, webPort)
if err != nil { if err != nil {
fmt.Printf("Error creating web server: %v\n", err) fmt.Printf("Error creating web server: %v\n", err)
sigChan <- syscall.SIGTERM
return return
} }
@ -91,18 +100,26 @@ func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPo
// Start the web server // Start the web server
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
fmt.Printf("Web server error: %v\n", err) fmt.Printf("Web server error: %v\n", err)
sigChan <- syscall.SIGTERM return
}
// Wait for context cancellation
<-ctx.Done()
// Gracefully shut down the server
fmt.Println("Shutting down web server...")
if err := server.Stop(); err != nil {
fmt.Printf("Error shutting down web server: %v\n", err)
} }
} }
// startTerminalInterface initializes and runs the terminal interface // startTerminalInterface initializes and runs the terminal interface
func startTerminalInterface(clock *derby.DerbyClock, events <-chan derby.Event, sigChan chan os.Signal) { func startTerminalInterface(clock *derby.DerbyClock, events <-chan derby.Event, ctx context.Context) {
fmt.Println("Terminal interface started") fmt.Println("Terminal interface started")
// Reset the clock to start fresh // Reset the clock to start fresh
if err := clock.Reset(); err != nil { if err := clock.Reset(); err != nil {
fmt.Printf("Error resetting clock: %v\n", err) fmt.Printf("Error resetting clock: %v\n", err)
sigChan <- syscall.SIGTERM
return return
} }
fmt.Println("Clock reset. Ready to start race.") fmt.Println("Clock reset. Ready to start race.")
@ -117,25 +134,33 @@ func startTerminalInterface(clock *derby.DerbyClock, events <-chan derby.Event,
// Process events from the clock // Process events from the clock
go func() { go func() {
raceResults := make([]*derby.Result, 0) raceResults := make([]*derby.Result, 0)
for event := range events { for {
switch event.Type { select {
case derby.EventRaceStart: case event, ok := <-events:
fmt.Println("\n🏁 Race started!") if !ok {
return
case derby.EventLaneFinish: }
result := event.Result switch event.Type {
fmt.Printf("🚗 Lane %d finished in place %d with time %.4f seconds\n", case derby.EventRaceStart:
result.Lane, result.FinishPlace, result.Time) fmt.Println("\n🏁 Race started!")
raceResults = append(raceResults, result)
case derby.EventLaneFinish:
case derby.EventRaceComplete: result := event.Result
fmt.Println("\n🏆 Race complete! Final results:") fmt.Printf("🚗 Lane %d finished in place %d with time %.4f seconds\n",
for _, result := range raceResults { result.Lane, result.FinishPlace, result.Time)
fmt.Printf("Place %d: Lane %d - %.4f seconds\n", raceResults = append(raceResults, result)
result.FinishPlace, result.Lane, result.Time)
case derby.EventRaceComplete:
fmt.Println("\n🏆 Race complete! Final results:")
for _, result := range raceResults {
fmt.Printf("Place %d: Lane %d - %.4f seconds\n",
result.FinishPlace, result.Lane, result.Time)
}
fmt.Println("\nEnter command (r/f/q/?):")
raceResults = nil
} }
fmt.Println("\nEnter command (r/f/q/?):") case <-ctx.Done():
raceResults = nil return
} }
} }
}() }()
@ -143,46 +168,50 @@ func startTerminalInterface(clock *derby.DerbyClock, events <-chan derby.Event,
// Handle keyboard input // Handle keyboard input
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
fmt.Print("Enter command (r/f/q/?): ") select {
input, err := reader.ReadString('\n') case <-ctx.Done():
if err != nil { return
fmt.Printf("Error reading input: %v\n", err) default:
continue fmt.Print("Enter command (r/f/q/?): ")
} input, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("Error reading input: %v\n", err)
continue
}
// Trim whitespace and convert to lowercase // Trim whitespace and convert to lowercase
command := strings.TrimSpace(strings.ToLower(input)) command := strings.TrimSpace(strings.ToLower(input))
switch command { switch command {
case "r": case "r":
fmt.Println("Resetting clock...") fmt.Println("Resetting clock...")
if err := clock.Reset(); err != nil { if err := clock.Reset(); err != nil {
fmt.Printf("Error resetting clock: %v\n", err) fmt.Printf("Error resetting clock: %v\n", err)
} else { } else {
fmt.Println("Clock reset. Ready to start race.") fmt.Println("Clock reset. Ready to start race.")
} }
case "f": case "f":
fmt.Println("Forcing race to end...") fmt.Println("Forcing race to end...")
if err := clock.ForceEnd(); err != nil { if err := clock.ForceEnd(); err != nil {
fmt.Printf("Error forcing race end: %v\n", err) fmt.Printf("Error forcing race end: %v\n", err)
} }
case "q": case "q":
fmt.Println("Quitting...") fmt.Println("Quitting...")
sigChan <- syscall.SIGTERM return
return
case "?": case "?":
fmt.Println("\nCommands:") fmt.Println("\nCommands:")
fmt.Println(" r - Reset the clock") fmt.Println(" r - Reset the clock")
fmt.Println(" f - Force end the race") fmt.Println(" f - Force end the race")
fmt.Println(" q - Quit the program") fmt.Println(" q - Quit the program")
fmt.Println(" ? - Show this help message") fmt.Println(" ? - Show this help message")
default: default:
if command != "" { if command != "" {
fmt.Println("Unknown command. Type ? for help.") fmt.Println("Unknown command. Type ? for help.")
}
} }
} }
} }

@ -1,10 +1,12 @@
package web package web
import ( import (
"context"
"embed" "embed"
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -19,22 +21,27 @@ var content embed.FS
// Server represents the web server for the derby clock // Server represents the web server for the derby clock
type Server struct { type Server struct {
router *chi.Mux router *chi.Mux
clock *derby.DerbyClock clock *derby.DerbyClock
events <-chan derby.Event events <-chan derby.Event
clients map[chan string]bool clients map[chan string]bool
port int clientsMux sync.Mutex
port int
server *http.Server
shutdown chan struct{}
} }
// NewServer creates a new web server // NewServer creates a new web server
func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, port int) (*Server, error) { func NewServer(clock *derby.DerbyClock, events <-chan derby.Event, port int) (*Server, error) {
// Create server // Create server
s := &Server{ s := &Server{
router: chi.NewRouter(), router: chi.NewRouter(),
clock: clock, clock: clock,
events: events, events: events,
clients: make(map[chan string]bool), clients: make(map[chan string]bool),
port: port, clientsMux: sync.Mutex{},
port: port,
shutdown: make(chan struct{}),
} }
// Set up routes // Set up routes
@ -69,43 +76,90 @@ func (s *Server) routes() {
// Start starts the web server // Start starts the web server
func (s *Server) Start() error { func (s *Server) Start() error {
fmt.Printf("Starting web server on port %d...\n", s.port) addr := fmt.Sprintf(":%d", s.port)
return http.ListenAndServe(fmt.Sprintf(":%d", s.port), s.router) s.server = &http.Server{
Addr: addr,
Handler: s.router,
}
// Start server in a goroutine
go func() {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("HTTP server error: %v\n", err)
}
}()
return nil
}
// Stop gracefully shuts down the server
func (s *Server) Stop() error {
// Signal event forwarder to stop
close(s.shutdown)
// Close all client connections
s.clientsMux.Lock()
for clientChan := range s.clients {
delete(s.clients, clientChan)
close(clientChan)
}
s.clientsMux.Unlock()
// Create a context with timeout for shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Shutdown the HTTP server
if s.server != nil {
return s.server.Shutdown(ctx)
}
return nil
} }
// forwardEvents forwards derby events to SSE clients // forwardEvents forwards derby events to SSE clients
func (s *Server) forwardEvents() { func (s *Server) forwardEvents() {
for event := range s.events { for {
// Store the event for new clients select {
// s.raceEvents <- event case event, ok := <-s.events:
if !ok {
// Create the SSE message based on the event type return
var message string }
switch event.Type { // Process the event and send to clients
case derby.EventRaceStart: s.broadcastEvent(event)
message = "event: race-start\ndata: {\"status\": \"running\"}\n\n" case <-s.shutdown:
return
case derby.EventLaneFinish:
result := event.Result
message = fmt.Sprintf("event: lane-finish\ndata: {\"lane\": %d, \"place\": %d, \"time\": %.4f}\n\n",
result.Lane, result.FinishPlace, result.Time)
case derby.EventRaceComplete:
message = "event: race-complete\ndata: {\"status\": \"finished\"}\n\n"
} }
}
}
// Send to all connected clients // broadcastEvent sends an event to all connected clients
for clientChan := range s.clients { func (s *Server) broadcastEvent(event derby.Event) {
select { var message string
case clientChan <- message: switch event.Type {
// Message sent successfully case derby.EventRaceStart:
default: message = "event: race-start\ndata: {\"status\": \"running\"}\n\n"
// Client is not receiving, remove it
delete(s.clients, clientChan) case derby.EventLaneFinish:
close(clientChan) result := event.Result
} message = fmt.Sprintf("event: lane-finish\ndata: {\"lane\": %d, \"place\": %d, \"time\": %.4f}\n\n",
result.Lane, result.FinishPlace, result.Time)
case derby.EventRaceComplete:
message = "event: race-complete\ndata: {\"status\": \"finished\"}\n\n"
}
// Send to all clients
s.clientsMux.Lock()
for clientChan := range s.clients {
// Non-blocking send to avoid slow clients blocking others
select {
case clientChan <- message:
default:
// Client channel is full, could log this or take other action
} }
} }
s.clientsMux.Unlock()
} }
// handleIndex handles the index page // handleIndex handles the index page
@ -161,7 +215,7 @@ func (s *Server) handleStatus() http.HandlerFunc {
} }
} }
// handleEvents handles the SSE events endpoint // handleEvents handles SSE events
func (s *Server) handleEvents() http.HandlerFunc { func (s *Server) handleEvents() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// Set headers for SSE // Set headers for SSE
@ -172,11 +226,17 @@ func (s *Server) handleEvents() http.HandlerFunc {
// Create a channel for this client // Create a channel for this client
clientChan := make(chan string, 10) clientChan := make(chan string, 10)
// Add client to map with mutex protection
s.clientsMux.Lock()
s.clients[clientChan] = true s.clients[clientChan] = true
s.clientsMux.Unlock()
// Clean up when the client disconnects // Remove client when connection is closed
defer func() { defer func() {
s.clientsMux.Lock()
delete(s.clients, clientChan) delete(s.clients, clientChan)
s.clientsMux.Unlock()
close(clientChan) close(clientChan)
}() }()

Loading…
Cancel
Save