From e4ce18edd8d62c5f4cda4a4f93dcdf492c594db3 Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 31 May 2026 16:40:22 +0200 Subject: [PATCH] Close UART gracefully on SIGINT/SIGTERM in goTool. Go exits on Ctrl+C without running defers, leaving the serial port locked; register shutdown hooks for serve and CLI commands. Co-authored-by: Cursor --- goTool/cmd_autotest.go | 2 ++ goTool/cmd_serve.go | 28 +++++++++++++--- goTool/main.go | 2 ++ goTool/shutdown.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 goTool/shutdown.go diff --git a/goTool/cmd_autotest.go b/goTool/cmd_autotest.go index 8aea123..bd6798d 100644 --- a/goTool/cmd_autotest.go +++ b/goTool/cmd_autotest.go @@ -62,6 +62,8 @@ func runTest(portOverride string, baudOverride int, args []string) error { if err != nil { return fmt.Errorf("open %s: %w", port, err) } + registerShutdown(func() { _ = sp.Close() }) + enableShutdownOnInterrupt() defer sp.Close() if !*verbose { diff --git a/goTool/cmd_serve.go b/goTool/cmd_serve.go index 1bd4450..6421987 100644 --- a/goTool/cmd_serve.go +++ b/goTool/cmd_serve.go @@ -2,6 +2,7 @@ package main import ( "embed" + "errors" "flag" "fmt" "io/fs" @@ -34,21 +35,29 @@ func runServe(portName string, baud int, args []string) error { link := newManagedSerial(portName, baud) link.quiet = true - defer link.Close() hub := newWSHub() streamCtl := newAccelStreamCtl() tapCtl := newTapNotifyCtl() stop := make(chan struct{}) - defer close(stop) + + var dashSrv *http.Server + var apiSrv *http.Server + registerShutdown(func() { + close(stop) + shutdownHTTPServer(dashSrv) + shutdownAPIServer(apiSrv) + if err := link.Close(); err != nil { + log.Printf("UART close: %v", err) + } + }) + go runPoller(link, portName, hub, streamCtl, tapCtl, *interval, stop) go runBatteryPoller(link, hub, 5*time.Second, stop) go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop) - var apiSrv *http.Server if *apiAddr != "" { apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop) - defer shutdownAPIServer(apiSrv) } mux := http.NewServeMux() @@ -81,5 +90,14 @@ func runServe(portName string, baud int, args []string) error { if *apiAddr == "" { log.Printf("external API disabled (-api-addr \"\")") } - return http.ListenAndServe(*addr, mux) + + dashSrv = &http.Server{Addr: *addr, Handler: mux} + go func() { + if err := dashSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Printf("dashboard server: %v", err) + } + }() + + waitForShutdown() + return nil } diff --git a/goTool/main.go b/goTool/main.go index 109c4e6..46c1671 100644 --- a/goTool/main.go +++ b/goTool/main.go @@ -62,6 +62,8 @@ func main() { if err != nil { log.Fatalf("open serial: %v", err) } + registerShutdown(func() { _ = sp.Close() }) + enableShutdownOnInterrupt() defer sp.Close() switch cmd { case "version": diff --git a/goTool/shutdown.go b/goTool/shutdown.go new file mode 100644 index 0000000..96704a7 --- /dev/null +++ b/goTool/shutdown.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" +) + +var ( + shutdownMu sync.Mutex + shutdownFns []func() + hooksOnce sync.Once + bgHandlerOnce sync.Once +) + +// registerShutdown runs fn on SIGINT/SIGTERM (LIFO order). +func registerShutdown(fn func()) { + shutdownMu.Lock() + shutdownFns = append(shutdownFns, fn) + shutdownMu.Unlock() +} + +func runShutdownHooks() { + hooksOnce.Do(func() { + shutdownMu.Lock() + fns := shutdownFns + shutdownMu.Unlock() + for i := len(fns) - 1; i >= 0; i-- { + fns[i]() + } + }) +} + +// enableShutdownOnInterrupt listens for SIGINT/SIGTERM in the background and exits +// after running shutdown hooks. Use for one-shot CLI commands (OTA, etc.). +func enableShutdownOnInterrupt() { + bgHandlerOnce.Do(func() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + go func() { + sig := <-ch + signal.Stop(ch) + log.Printf("received %v, shutting down…", sig) + runShutdownHooks() + os.Exit(0) + }() + }) +} + +// waitForShutdown blocks until SIGINT/SIGTERM, runs hooks, and returns. +// Use for long-running servers (serve/dashboard). +func waitForShutdown() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + sig := <-ch + signal.Stop(ch) + log.Printf("received %v, shutting down…", sig) + runShutdownHooks() +} + +func shutdownHTTPServer(srv *http.Server) { + if srv == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Printf("HTTP shutdown: %v", err) + } +}