From 047cc958b55d0a931c6a8e870951f096b630949d Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Sun, 17 May 2026 08:23:34 +0200 Subject: [PATCH] Throttle capture-failure log to once per 5s while capturer is down --- client/vnc/server/session.go | 40 ++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/client/vnc/server/session.go b/client/vnc/server/session.go index 9fb21eeb9..1185360f3 100644 --- a/client/vnc/server/session.go +++ b/client/vnc/server/session.go @@ -64,6 +64,12 @@ type session struct { curFrame *image.RGBA idleFrames int + // captureErrLast throttles "capture (transient)" logs while the + // capturer is in a sustained failure state (e.g. X server died but a + // noVNC tab is still open). Owned by the encoder goroutine. + captureErrLast time.Time + captureErrSeen bool + // encodeCh carries framebuffer-update requests from the read loop to the // encoder goroutine. Buffered size 1: RFB clients have one outstanding // request at a time, so a new request always replaces any pending one. @@ -409,13 +415,16 @@ func (s *session) processFBRequest(req fbRequest) error { // Capture failures are transient on Windows: a Ctrl+Alt+Del or // sign-out switches the OS to the secure desktop, and the DXGI // duplicator on the previous desktop returns an error until the - // capturer reattaches on the new desktop. Don't tear down the - // session. Back off briefly and reply with an empty update so - // the client keeps re-requesting. - s.log.Debugf("capture (transient): %v", err) + // capturer reattaches on the new desktop. On Linux the X server + // behind a virtual session may exit and the capturer reports + // "unavailable" on every retry tick. Don't tear down the session + // and don't spam the log: emit one line on the first failure, then + // throttle further "still failing" lines to once per 5 s. + s.captureErrorLog(err) time.Sleep(100 * time.Millisecond) return s.sendEmptyUpdate() } + s.captureRecovered() if req.incremental && s.prevFrame != nil { tiles := diffTiles(s.prevFrame, img, s.serverW, s.serverH, tileSize) @@ -471,6 +480,29 @@ func (s *session) processFBRequest(req fbRequest) error { return nil } +// captureErrorLog emits one log line on the first failure after success, +// then at most once every captureErrThrottle while the capturer keeps +// failing. The "recovered" transition is logged once when err is nil and +// captureErrSeen was set. +func (s *session) captureErrorLog(err error) { + const captureErrThrottle = 5 * time.Second + now := time.Now() + if !s.captureErrSeen || now.Sub(s.captureErrLast) >= captureErrThrottle { + s.log.Debugf("capture (transient): %v", err) + s.captureErrLast = now + } + s.captureErrSeen = true +} + +// captureRecovered emits a one-shot debug line when capture works again +// after a failure streak. Called by the success paths. +func (s *session) captureRecovered() { + if s.captureErrSeen { + s.log.Debugf("capture recovered") + s.captureErrSeen = false + } +} + // refreshCopyRectIndex does a full hash sweep of the just-swapped prevFrame. // Used after full-frame sends, where we don't have a per-tile dirty list to // drive an incremental update.