From 6bd2c5e74cb05f8e7cddc3975eae77fd120cd777 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Apr 2026 23:07:38 -0500 Subject: [PATCH 1/3] fix(tunnel): stop gRPC server on context cancel to prevent RunWithResult hang --- pkg/agent/tunnelserver/tunnelserver.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/agent/tunnelserver/tunnelserver.go b/pkg/agent/tunnelserver/tunnelserver.go index 22a6bad66..e08d56dc6 100644 --- a/pkg/agent/tunnelserver/tunnelserver.go +++ b/pkg/agent/tunnelserver/tunnelserver.go @@ -128,9 +128,16 @@ func (t *tunnelServer) RunWithResult( go func() { errChan <- s.Serve(lis) }() + go func() { + <-ctx.Done() + s.Stop() + }() select { case err := <-errChan: + if t.result != nil { + return t.result, nil + } return nil, err case <-ctx.Done(): return t.result, nil From 75c128f902b11a936453d36e4a9a0af2a3404c34 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 26 Apr 2026 23:15:13 -0500 Subject: [PATCH 2/3] fix(tunnel): prevent goroutine leak in RunWithResult stop goroutine Use a derived context for the s.Stop() goroutine so it exits when RunWithResult returns, not only when the parent context is cancelled. --- pkg/agent/tunnelserver/tunnelserver.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/agent/tunnelserver/tunnelserver.go b/pkg/agent/tunnelserver/tunnelserver.go index e08d56dc6..39b24e9df 100644 --- a/pkg/agent/tunnelserver/tunnelserver.go +++ b/pkg/agent/tunnelserver/tunnelserver.go @@ -128,8 +128,11 @@ func (t *tunnelServer) RunWithResult( go func() { errChan <- s.Serve(lis) }() + + stopCtx, stopCancel := context.WithCancel(ctx) + defer stopCancel() go func() { - <-ctx.Done() + <-stopCtx.Done() s.Stop() }() From 6c04cc87ba3a6fc3cf9295193f4978ebc3ece0f2 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 27 Apr 2026 00:00:53 -0500 Subject: [PATCH 3/3] fix(tunnel): cancel context in awaitPair when tunnel finishes first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the tunnel goroutine finishes before the handler, awaitPair must cancel the context before waiting for the handler to return. Without this, the gRPC server inside RunWithResult blocks forever on StdioListener.Accept() because nothing signals it to stop. The previous commit added s.Stop() on ctx.Done() in RunWithResult, but that only fires when the context is cancelled — which never happened because awaitPair blocked waiting for the handler (which was RunWithResult) without cancelling the context first. --- pkg/tunnel/pipebridge.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/tunnel/pipebridge.go b/pkg/tunnel/pipebridge.go index 4562d74a6..ce4e82ec7 100644 --- a/pkg/tunnel/pipebridge.go +++ b/pkg/tunnel/pipebridge.go @@ -62,10 +62,11 @@ func (pb *PipeBridge) RunPair( handlerChan <- handlerFn(cancelCtx, pb.StdoutReader, pb.StdinWriter) }() - return awaitPair(tunnelChan, handlerChan, pb.StdoutWriter, pb.StdinWriter) + return awaitPair(cancel, tunnelChan, handlerChan, pb.StdoutWriter, pb.StdinWriter) } func awaitPair( + cancel context.CancelFunc, tunnelChan, handlerChan <-chan error, stdoutWriter, stdinWriter *os.File, ) error { @@ -78,6 +79,7 @@ func awaitPair( default: } case tunnelErr = <-tunnelChan: + cancel() _ = stdoutWriter.Close() _ = stdinWriter.Close() handlerErr = <-handlerChan