From 55e09c02b17fa85196339408ce06ae0d70f401de Mon Sep 17 00:00:00 2001 From: yusing Date: Sun, 25 Jan 2026 12:28:51 +0800 Subject: [PATCH] fix(proxmox): prevent goroutine leaks by closing idle HTTP connections Added a function to close idle HTTP connections in the LXCCommand method. This addresses potential goroutine leaks caused by the go-proxmox library's TermWebSocket not closing underlying HTTP/2 connections. The websocket closer is now wrapped to ensure proper cleanup of transport connections when the command execution is finished. --- internal/proxmox/lxc_command.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/proxmox/lxc_command.go b/internal/proxmox/lxc_command.go index 37f3a497..7db451c9 100644 --- a/internal/proxmox/lxc_command.go +++ b/internal/proxmox/lxc_command.go @@ -5,12 +5,22 @@ import ( "context" "fmt" "io" + "net/http" "github.com/gorilla/websocket" ) var ErrNoSession = fmt.Errorf("no session found, make sure username and password are set") +// closeTransportConnections forces close idle HTTP connections to prevent goroutine leaks. +// This is needed because the go-proxmox library's TermWebSocket closer doesn't close +// the underlying HTTP/2 connections, leaving goroutines stuck in writeLoop/readLoop. +func closeTransportConnections(httpClient *http.Client) { + if tr, ok := httpClient.Transport.(*http.Transport); ok { + tr.CloseIdleConnections() + } +} + // LXCCommand connects to the Proxmox VNC websocket and streams command output. // It returns an io.ReadCloser that streams the command output. func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.ReadCloser, error) { @@ -37,11 +47,19 @@ func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.Rea return nil, fmt.Errorf("failed to get term proxy: %w", err) } - send, recv, errs, close, err := node.TermWebSocket(term) + send, recv, errs, closeWS, err := node.TermWebSocket(term) if err != nil { return nil, fmt.Errorf("failed to connect to term websocket: %w", err) } + // Wrap the websocket closer to also close HTTP transport connections. + // This prevents goroutine leaks when streaming connections are interrupted. + httpClient := n.client.GetHTTPClient() + closeFn := func() error { + closeTransportConnections(httpClient) + return closeWS() + } + handleSend := func(data []byte) error { select { case <-ctx.Done(): @@ -67,7 +85,7 @@ func (n *Node) LXCCommand(ctx context.Context, vmid int, command string) (io.Rea // Start a goroutine to read from websocket and write to pipe go func() { - defer close() + defer closeFn() defer pw.Close() seenCommand := false