From 95b1967e9bc2e40ee64818f94e48752e9a040d2d Mon Sep 17 00:00:00 2001 From: Harry Bayliss Date: Wed, 27 May 2026 13:59:47 +0100 Subject: [PATCH] Fix daemon shutdown hang and concurrent-send race - daemon_net: close the client transport on context cancellation so the per-connection Recv loop unblocks; otherwise wg.Wait() in the accept loop hung on a still-connected client and the daemon never stopped. - protocol: guard ConnTransport.Send with a mutex so the subscriber pump and command handlers can push frames concurrently without racing the bufio.Writer. Fixes TestDaemonDetachReattachPreservesProcess (now passes under -race). --- internal/app/daemon_net.go | 8 ++++++++ internal/protocol/transport.go | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/app/daemon_net.go b/internal/app/daemon_net.go index 48b0f17..2e7a2a4 100644 --- a/internal/app/daemon_net.go +++ b/internal/app/daemon_net.go @@ -232,6 +232,14 @@ func handleDaemonAttach(ctx context.Context, registry *ProjectRegistry, t protoc _ = sendSnapshot(t, project, view.FocusedID) } + // Close the transport when the daemon context is cancelled (shutdown or + // `daemon stop`). Without this the t.Recv() loop below blocks forever on a + // still-connected client and the accept loop's wg.Wait() never returns. + go func() { + <-ctx.Done() + _ = t.Close() + }() + done := make(chan struct{}) go func() { defer close(done) diff --git a/internal/protocol/transport.go b/internal/protocol/transport.go index 8d431b5..45bf531 100644 --- a/internal/protocol/transport.go +++ b/internal/protocol/transport.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "sync" ) var ErrTransportClosed = errors.New("protocol: transport closed") @@ -18,10 +19,14 @@ type Transport interface { Close() error } -// ConnTransport is a JSON-lines implementation over a stream connection. +// ConnTransport is a JSON-lines implementation over a stream connection. Send +// is guarded by a mutex so the daemon can push frames from its subscriber pump +// and its command handlers concurrently; Close may be called from any goroutine +// (e.g. on context cancellation) to unblock a pending Recv. type ConnTransport struct { conn net.Conn r *bufio.Reader + wmu sync.Mutex w *bufio.Writer } @@ -41,6 +46,8 @@ func (t *ConnTransport) Send(f Frame) error { if err != nil { return fmt.Errorf("protocol: encode frame: %w", err) } + t.wmu.Lock() + defer t.wmu.Unlock() if _, err := t.w.Write(append(b, '\n')); err != nil { return err }