From 81bbfb82633c0c0c489e15b91a3577194ac53b9d Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Tue, 7 Apr 2026 17:15:38 -0400 Subject: [PATCH 1/2] Fix deadlock when UDP port 7734 is already in use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When listenUDP failed silently and FSWatch was disabled, the Run() select loop had no way to receive events — causing a deadlock panic on kill or hanging forever. Now listenUDP signals startup success/failure via a buffered channel; if the port is already bound (watch already running), we return a clear error immediately instead of hanging. Co-Authored-By: Claude Sonnet 4.6 --- internal/files/daemon.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/files/daemon.go b/internal/files/daemon.go index 8cc1f74..3353751 100644 --- a/internal/files/daemon.go +++ b/internal/files/daemon.go @@ -84,7 +84,14 @@ func (d *Daemon) Run(ctx context.Context) error { d.logf("[step:2] Starting listeners") if d.cfg.NotifyPort > 0 { - go d.listenUDP(ctx) + udpReady := make(chan error, 1) + go d.listenUDP(ctx, udpReady) + if err := <-udpReady; err != nil { + if !d.cfg.FSWatch { + return fmt.Errorf("UDP port %d already in use — is `supermodel watch` already running? (%w)", d.cfg.NotifyPort, err) + } + d.logf("Warning: UDP listener failed (FSWatch active, continuing): %v", err) + } } if d.cfg.FSWatch { @@ -525,14 +532,15 @@ func (d *Daemon) computeAffectedFiles(changedFiles []string) []string { return daemonSortedKeys(affected) } -func (d *Daemon) listenUDP(ctx context.Context) { +func (d *Daemon) listenUDP(ctx context.Context, ready chan<- error) { addr := fmt.Sprintf("127.0.0.1:%d", d.cfg.NotifyPort) conn, err := net.ListenPacket("udp", addr) if err != nil { - d.logf("UDP listener failed: %v", err) + ready <- err return } defer conn.Close() + ready <- nil d.logf("UDP listener on %s", addr) go func() { From d6d4e3631b075b154a549b93e3b8c6dddd726966 Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Tue, 7 Apr 2026 17:21:23 -0400 Subject: [PATCH 2/2] Distinguish EADDRINUSE from other UDP bind errors Co-Authored-By: Claude Sonnet 4.6 --- internal/files/daemon.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/files/daemon.go b/internal/files/daemon.go index 3353751..dff58f2 100644 --- a/internal/files/daemon.go +++ b/internal/files/daemon.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/json" + "errors" "fmt" "net" "os" @@ -11,6 +12,7 @@ import ( "sort" "strings" "sync" + "syscall" "time" "github.com/supermodeltools/cli/internal/api" @@ -88,7 +90,10 @@ func (d *Daemon) Run(ctx context.Context) error { go d.listenUDP(ctx, udpReady) if err := <-udpReady; err != nil { if !d.cfg.FSWatch { - return fmt.Errorf("UDP port %d already in use — is `supermodel watch` already running? (%w)", d.cfg.NotifyPort, err) + if errors.Is(err, syscall.EADDRINUSE) { + return fmt.Errorf("UDP port %d already in use — is `supermodel watch` already running?", d.cfg.NotifyPort) + } + return fmt.Errorf("failed to start UDP listener on port %d: %w", d.cfg.NotifyPort, err) } d.logf("Warning: UDP listener failed (FSWatch active, continuing): %v", err) }