Lazy clipboard init and backoff when SPICE not connected
When the SPICE clipboard channel is not available (e.g., running headless or without clipboard sharing), the vdagent would previously retry every 1 second, initializing the clipboard library each time. This caused unnecessary CPU usage. Changes: - Defer clipboard.Init() until first successful SPICE message - Add ErrSPICENotConnected error type to distinguish "never connected" from "connection lost" - Use 1-minute backoff when SPICE is not available, vs 1-second for transient failures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b8b6ab102f
commit
6bceea7928
|
|
@ -31,7 +31,13 @@ var runAgent bool
|
|||
|
||||
var debug bool
|
||||
|
||||
const componentFailedTimeout = time.Second
|
||||
const (
|
||||
// componentFailedTimeout is the default retry delay for transient failures
|
||||
componentFailedTimeout = time.Second
|
||||
// spiceNotConnectedTimeout is the retry delay when SPICE clipboard channel
|
||||
// is not available (e.g., running headless or without clipboard sharing)
|
||||
spiceNotConnectedTimeout = time.Minute
|
||||
)
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -109,12 +115,10 @@ func run(cmd *cobra.Command, args []string) error {
|
|||
if runVdagent {
|
||||
group.Go(func() error {
|
||||
for {
|
||||
if err := runVdagentOnce(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
backoff := runVdagentOnce(ctx)
|
||||
|
||||
select {
|
||||
case <-time.After(componentFailedTimeout):
|
||||
case <-time.After(backoff):
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
|
@ -152,26 +156,32 @@ func run(cmd *cobra.Command, args []string) error {
|
|||
return group.Wait()
|
||||
}
|
||||
|
||||
func runVdagentOnce(ctx context.Context) error {
|
||||
func runVdagentOnce(ctx context.Context) time.Duration {
|
||||
zap.S().Infof("initializing vdagent...")
|
||||
|
||||
vdAgent, err := vdagent.New()
|
||||
if err != nil {
|
||||
zap.S().Errorf("failed to initialize vdagent: %v", err)
|
||||
|
||||
return nil
|
||||
return componentFailedTimeout
|
||||
}
|
||||
defer vdAgent.Close()
|
||||
|
||||
zap.S().Infof("running vdagent...")
|
||||
|
||||
if err := vdAgent.Run(ctx); err != nil {
|
||||
zap.S().Errorf("failed to run vdagent: %v", err)
|
||||
// Check if SPICE clipboard channel is not available
|
||||
var spiceErr *vdagent.ErrSPICENotConnected
|
||||
if errors.As(err, &spiceErr) {
|
||||
zap.S().Infof("SPICE clipboard channel not available, will retry in %v", spiceNotConnectedTimeout)
|
||||
return spiceNotConnectedTimeout
|
||||
}
|
||||
|
||||
return nil
|
||||
zap.S().Errorf("failed to run vdagent: %v", err)
|
||||
return componentFailedTimeout
|
||||
}
|
||||
|
||||
return nil
|
||||
return componentFailedTimeout
|
||||
}
|
||||
|
||||
func runRPCOnce(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/spice/vd"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/spice/vdi"
|
||||
"go.uber.org/zap"
|
||||
|
|
@ -14,10 +15,26 @@ import (
|
|||
|
||||
const serialPortPath = "/dev/tty.com.redhat.spice.0"
|
||||
|
||||
// ErrSPICENotConnected is returned when the SPICE clipboard channel
|
||||
// is not available. This typically happens when running headless or
|
||||
// when clipboard sharing is not enabled on the host.
|
||||
type ErrSPICENotConnected struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ErrSPICENotConnected) Error() string {
|
||||
return fmt.Sprintf("SPICE clipboard channel not connected: %v", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrSPICENotConnected) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type VDAgent struct {
|
||||
serialPort *os.File
|
||||
vdi *vdi.VDI
|
||||
lastClipboardState []byte
|
||||
serialPort *os.File
|
||||
vdi *vdi.VDI
|
||||
lastClipboardState []byte
|
||||
clipboardInitialized bool
|
||||
}
|
||||
|
||||
func New() (*VDAgent, error) {
|
||||
|
|
@ -26,9 +43,8 @@ func New() (*VDAgent, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := clipboard.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Note: clipboard.Init() is deferred until we confirm SPICE is connected.
|
||||
// This avoids expensive initialization when clipboard sharing is unavailable.
|
||||
|
||||
return &VDAgent{
|
||||
serialPort: sp,
|
||||
|
|
@ -37,7 +53,14 @@ func New() (*VDAgent, error) {
|
|||
}
|
||||
|
||||
func (agent *VDAgent) Run(ctx context.Context) error {
|
||||
clipboardCh := clipboard.Watch(ctx, clipboard.FmtText)
|
||||
// clipboardCh is nil until clipboard is initialized.
|
||||
// A nil channel blocks forever on receive, which is fine for the select.
|
||||
var clipboardCh <-chan []byte
|
||||
|
||||
// Create a child context for clipboard.Watch() so we can cancel it
|
||||
// when Run() exits, preventing goroutine leaks on retry.
|
||||
clipboardCtx, clipboardCancel := context.WithCancel(ctx)
|
||||
defer clipboardCancel()
|
||||
|
||||
for {
|
||||
// Check for cancellation and clipboard changes
|
||||
|
|
@ -63,9 +86,26 @@ func (agent *VDAgent) Run(ctx context.Context) error {
|
|||
continue
|
||||
}
|
||||
|
||||
// If we haven't successfully initialized clipboard yet,
|
||||
// this means SPICE was never connected - use special error
|
||||
// to signal longer backoff.
|
||||
if !agent.clipboardInitialized {
|
||||
return &ErrSPICENotConnected{Err: err}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// First successful read from SPICE - now initialize clipboard
|
||||
if !agent.clipboardInitialized {
|
||||
if err := clipboard.Init(); err != nil {
|
||||
return fmt.Errorf("failed to initialize clipboard: %w", err)
|
||||
}
|
||||
clipboardCh = clipboard.Watch(clipboardCtx, clipboard.FmtText)
|
||||
agent.clipboardInitialized = true
|
||||
zap.S().Debug("SPICE connected, clipboard initialized")
|
||||
}
|
||||
|
||||
switch vdiAgentMessage.Type {
|
||||
case vd.VD_AGENT_ANNOUNCE_CAPABILITIES:
|
||||
vdAgentAnnounceCapabilities, err := vd.ReadVDAgentAnnounceCapabilities(vdiAgentMessage.Data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue