diff --git a/pkg/daemon/ipc.go b/pkg/daemon/ipc.go index 6e40cedf..fa1400e5 100644 --- a/pkg/daemon/ipc.go +++ b/pkg/daemon/ipc.go @@ -876,7 +876,9 @@ func (s *IPCServer) handleBind(conn *ipcConn, reqID uint64, payload []byte) { // Start pushing accepted connections to this client. CmdAccept frames // are server-pushed (reqID=0) and demuxed by the driver on local port. + s.daemon.bgWG.Add(1) go func() { + defer s.daemon.bgWG.Done() for c := range ln.AcceptCh { conn.trackConn(c.ID) // H12 fix: include local port for per-port demux diff --git a/pkg/daemon/services.go b/pkg/daemon/services.go index eaa7b316..1cd4ec81 100644 --- a/pkg/daemon/services.go +++ b/pkg/daemon/services.go @@ -145,7 +145,9 @@ func (d *Daemon) startEchoService() error { if err != nil { return err } + d.bgWG.Add(1) go func() { + defer d.bgWG.Done() for { select { case conn, ok := <-ln.AcceptCh: diff --git a/pkg/daemon/zz_services_bootstrap_test.go b/pkg/daemon/zz_services_bootstrap_test.go index a80482d7..1e064c0f 100644 --- a/pkg/daemon/zz_services_bootstrap_test.go +++ b/pkg/daemon/zz_services_bootstrap_test.go @@ -179,3 +179,36 @@ func TestStartEchoServiceAcceptLoopExitsOnUnbind(t *testing.T) { // detached goroutine but Sleep gives it a chance to drain. time.Sleep(100 * time.Millisecond) } + +// --- startEchoService increments bgWG and stopCh drains it --- + +func TestStartEchoServiceBGWaitGroupTracksAcceptLoop(t *testing.T) { + t.Parallel() + d := New(Config{}) + + if err := d.startEchoService(); err != nil { + t.Fatalf("startEchoService: %v", err) + } + + // Close stopCh — accept loop should exit via <-d.stopCh and + // defer d.bgWG.Done() fires, draining the WaitGroup. + close(d.stopCh) + + if !wgDrained(&d.bgWG, 2*time.Second) { + t.Fatal("bgWG not drained within 2s after stopCh close") + } +} + +func wgDrained(wg *sync.WaitGroup, timeout time.Duration) bool { + ch := make(chan struct{}) + go func() { + wg.Wait() + close(ch) + }() + select { + case <-ch: + return true + case <-time.After(timeout): + return false + } +}