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.
This commit is contained in:
yusing
2026-01-25 12:28:51 +08:00
parent 9adeb3e3dd
commit 55e09c02b1

View File

@@ -5,12 +5,22 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var ErrNoSession = fmt.Errorf("no session found, make sure username and password are set") 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. // LXCCommand connects to the Proxmox VNC websocket and streams command output.
// It returns an io.ReadCloser that streams the 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) { 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) 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 { if err != nil {
return nil, fmt.Errorf("failed to connect to term websocket: %w", err) 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 { handleSend := func(data []byte) error {
select { select {
case <-ctx.Done(): 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 // Start a goroutine to read from websocket and write to pipe
go func() { go func() {
defer close() defer closeFn()
defer pw.Close() defer pw.Close()
seenCommand := false seenCommand := false