Skip to content

Fail self.deferred in connectionLost instead of hanging forever - #543

Merged
sibson merged 3 commits into
mainfrom
fix/deferred-fails-on-disconnect
Sep 30, 2026
Merged

sibson merged 3 commits into
mainfrom
fix/deferred-fails-on-disconnect

Conversation

@sibson

@sibson sibson commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

capture, expect, refreshScreen and stable all wait on self.deferred, and none of them learned about a dropped connection, so a disconnect mid-call hung the caller. connectionLost now errbacks a pending self.deferred with a new ConnectionLostError.

_StableWatch hands its caller a separate Deferred, fired by a timer rather than by an update, so _request also errbacks that one and cancels the timer. Review renamed it result → settled and dropped the bool that duplicated settled.called.

resizeScreen keeps its own _PendingResize Deferred: it completes on the server's ExtendedDesktopSize reply, not on commitUpdate, so it can't share the slot. connectionLost checks both, which is where this conflicted with #497.

🤖 Generated with Claude Code

Comment thread vncdotool/client.py Outdated
captureScreen/captureRegion, expectScreen/expectRegion, refreshScreen
and stableScreen/stableRegion each park one pending operation in
self.deferred (or, for the stable-watch commands, ride on it
indirectly). None of them learned about a dropped connection: the
Deferred a caller was waiting on simply never fired, so a disconnect
mid-capture hung the caller rather than failing it. connectionLost now
errbacks self.deferred with a new ConnectionLostError when one is
pending -- ProtocolError and DesktopResizeError already name a
specific cause, neither of which is "the socket went away", so a new
exception says that plainly.

_StableWatch is the one case that needed more than an errback on
self.deferred: refreshScreen's Deferred that _request chains off is
one round of the watch, not the Deferred that stableScreen/stableRegion
handed to their caller (_StableWatch.result, only ever fired from
_settle()). Failing self.deferred alone would have hit "Unhandled
error in Deferred" and left the caller hanging exactly as before, just
one layer down. _request now adds an errback alongside its callback
that fails self.result directly, cancelling the pending settle timer
so a stale timer cannot later call back an already-errored Deferred.

For refreshScreen/_capture/expectScreen, no such wiring was needed:
those hand the caller self.deferred itself (or a Deferred Twisted
chains onto it across rounds), so errbacking self.deferred already
reaches the caller through Twisted's ordinary Deferred-returning-
Deferred chaining.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9XU3qUSZYysSFCZNaRpQc
@sibson
sibson force-pushed the fix/deferred-fails-on-disconnect branch from c8bcfa7 to 0ee30c2 Compare September 22, 2026 23:49

sibson commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

Windows - UltraVNC, TightVNC, TigerVNC failed on 0ee30c2: TestServer_tightvnc.test_capture -- "capture is a single flat colour, no screen content was decoded". Not this PR's failure: the diff here only touches vncdotool/client.py, tests/unit/test_client.py and CHANGELOG.md, none of which this test or the Windows server setup depend on.

Cause, measured since on draft #546: TightVNC serves an all-black frame (every tile a Tight fill of 000000) on a regular fraction of fresh connections -- 5 of 20 in steady state, while UltraVNC and TigerVNC-win served 0 of 40. test_capture fails whenever its single connection lands on one. No fix has landed yet; it belongs in its own PR, not this one.

I can't re-run the failed job (403), so this PR stays watched.


Generated by Claude Code

Comment thread vncdotool/client.py Outdated
sibson and others added 2 commits September 23, 2026 07:12
The bool flipped to True in exactly the two places the caller's Deferred
fired (_settle's callback, _disconnected's errback), so it restated
Deferred.called under a second name. Keep one: the Deferred is now
`settled`, and the re-arm guards test `settled.called`. "Settled" in the
promise sense covers both outcomes, which is what the disconnect path
added. `result` was too generic a name for it, per review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sibson
sibson merged commit 56ee4bb into main Sep 30, 2026
15 checks passed
@sibson
sibson deleted the fix/deferred-fails-on-disconnect branch September 30, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants