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 <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-31 16:40:22 +02:00
parent 0eea27a876
commit e4ce18edd8
4 changed files with 102 additions and 5 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -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":

75
goTool/shutdown.go Normal file
View File

@ -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)
}
}