Skip to content

fix(server): keep ConnectionHandler reachable during a Preempt race - #2065

Merged
Benoît Cortier (CBenoit) merged 3 commits into
Devolutions:masterfrom
antonmos:fix/preempt-connection-handler-hooks
Oct 1, 2026
Merged

Benoît Cortier (CBenoit) merged 3 commits into
Devolutions:masterfrom
antonmos:fix/preempt-connection-handler-hooks

Conversation

@antonmos

Copy link
Copy Markdown
Contributor

Fixes #1969.

Problem

Under ConnectionPolicy::Preempt, RdpServer::run took the connection handler out of self before building the live connection (that connection borrows &mut self for the whole race, while a candidate's on_accept needs the handler at the same time) and only put it back after the race. Every hook fired from inside the served connection therefore saw None and was skipped with nothing logged — in practice on_connection_info never fired, for every connection under Preempt, not just preempted ones. Queue and Reject were unaffected.

Fix

Share the handler rather than taking it: connection_handler is now Option<Rc<RefCell<Box<dyn ConnectionHandler>>>>. The race clones the Rc for the candidate's on_accept, while the live connection reaches the same handler through self. The take/restore plumbing through the race's return tuple is removed.

Why this shape is safe:

  • Every ConnectionHandler method is synchronous, so a borrow_mut() lives only for the call, never across an .await; with the server confined to one thread, the candidate's on_accept and the live connection's hooks can't hold it at the same time.
  • RdpServer is already !Send (it holds non-Send factories such as SoundServerFactory), so an Rc field takes nothing away from embedders. The public API (with_connection_handler(Option<Box<dyn ConnectionHandler>>)) is unchanged.

Tests

The three e2e tests from the issue (by clintcan), in ironrdp-testsuite-extra/tests/e2e.rs: a real client through the full handshake via run() under Queue, Reject and Preempt, asserting both on_accept and on_connection_info reach the handler. Without the fix the Preempt case fails with on_accept=true, on_connection_info=false; with it all three pass.

This is the prerequisite Benoît Cortier (@CBenoit) asked for on #1934 (mode-aware Preempt default under Hybrid), which would otherwise make this bug hit every Hybrid embedder out of the box.

🤖 Generated with Claude Code

Under ConnectionPolicy::Preempt, `run` took the connection handler out
of `self` for the whole race, so hooks fired from the served connection
(`on_connection_info`) found `None` and were silently skipped for every
connection. Share the handler as `Rc<RefCell<Box<dyn ConnectionHandler>>>`
instead: every hook is synchronous, so no borrow is held across an
`.await`, and `RdpServer` is already `!Send`.

Adds e2e regression tests (from the issue) asserting `on_accept` and
`on_connection_info` reach the handler under Queue, Reject and Preempt.

Fixes Devolutions#1969

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The ownership fix is sound and directly covered by regression tests.

Review effort: Balanced
Findings: None

What changed in this PR

Fixes ConnectionHandler availability during Preempt races without changing the public API.

Changes:

  • Shares the handler through Rc<RefCell<_>> during races.
  • Adds end-to-end coverage for all connection policies.

No material findings. Protocol review was unnecessary because wire behavior is unchanged.

File Description
crates/​ironrdp-server/​src/​server.rs Preserves handler access during preemption.
crates/​ironrdp-testsuite-extra/​tests/​e2e.rs Tests lifecycle hooks across policies.

@github-actions github-actions Bot added risk/medium Behavioral change that does not substantially alter a core public API size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure labels Oct 1, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core fix is sound and minimal: sharing the ConnectionHandler via Rc<RefCell<...>> under Preempt resolves the #1969 borrow conflict without changing the public API (RdpServer was already !Send since it holds non-Send dyn SoundServerFactory), and all four call sites use statement-scoped borrow_mut() consistent with the synchronous trait contract. The take/restore plumbing removal is a strict simplification. I verified the !Send claim, the synchronous trait definition, every converted call site, and that the new test's required imports already exist. The four published findings are all valid, non-blocking observations: the new Preempt e2e test covers only the single-client symptom and never exercises the takeover race where the handler is actually shared; the shared-handler safety invariant is documented but unenforced; the test helper duplicates ~50 lines of existing handshake scaffolding; and three Box::pin wrappers are unnecessary by the file's own precedent.

Comment thread crates/ironrdp-testsuite-extra/tests/e2e.rs
Comment thread crates/ironrdp-server/src/server.rs Outdated
Comment thread crates/ironrdp-testsuite-extra/tests/e2e.rs
Comment thread crates/ironrdp-testsuite-extra/tests/e2e.rs Outdated
@github-actions github-actions Bot added ai-reviewed/1 One automated review completed needs-author-action The pull request author is the current next actor labels Oct 1, 2026
…ure-scoped helper; test a real takeover

- All four handler call sites go through `with_connection_handler`, whose
  synchronous closure scopes the RefCell borrow: holding it across an
  `.await` no longer compiles, instead of resting on a convention.
- New e2e test drives an actual Preempt takeover with two clients and
  asserts every hook lands on the shared handler: both `on_accept`s, both
  `on_connection_info`s, and the evicted session's `on_disconnected`.
  Fails on master.
- Factor the client handshake into `connect_client`, shared with
  `client_server_with_connector`, and the hook-recording server setup
  into `hook_recording_server`.
- Drop the `Box::pin`s clippy's `large_futures` does not require.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure and removed needs-author-action The pull request author is the current next actor size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure labels Oct 1, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review confirms the fix for #1969: under ConnectionPolicy::Preempt the live connection and a candidate's on_accept can now both reach the shared ConnectionHandler. In pr-head, connection_handler is Option<Rc<RefCell<Box<dyn ConnectionHandler>>>>; the take/restore plumbing through the race's return tuple is deleted, and all four former access sites (accept, candidate accept, on_disconnected, on_connection_info) route through the new private with_connection_handler helper, whose RefCell borrow lives only inside a synchronous closure so it cannot span an .await. The !Send justification holds (the server already held non-Send factories) and the public builder API is unchanged; the Negotiated-entry skip preserving on_accept double-count semantics is intact. The e2e tests cover hooks under Queue/Reject/Preempt and a full preemption takeover asserting (2, 2, 1) hook counts plus eviction of the old transport. The single specialist candidate is verified accurate and accepted: a cal…

  1. [code-compressor] Touched call site still inlines the address lookup the PR just factored into local_addr_of — low 🟡 — crates/ironrdp-testsuite-extra/tests/e2e.rs
    The PR introduces local_addr_of (oneshot channel + ServerEvent::GetLocalAddr + await/unwraps) and uses it in the new hook tests, and it edits client_server_with_connector to route the handshake through the new connect_client helper. The lines immediately above that edit still carry the identical three-line inline lookup. Replacing them with let server_addr = local_addr_of(&ev).await; is behavior-identical (same send/recv and unwraps, &ev already captured by the surrounding closure) and leaves the address lookup a single home. Low severity: test-only duplication with no correctness or protocol impact. The inlined lines are pre-existing context lines in the diff, not added lines, hence null line bounds.

@github-actions github-actions Bot added ai-reviewed/2 Two automated reviews completed needs-author-action The pull request author is the current next actor and removed ai-reviewed/1 One automated review completed labels Oct 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added kind/protocol Affects RDP or related protocol behavior needs-review A human reviewer is the current next actor and removed needs-author-action The pull request author is the current next actor labels Oct 1, 2026

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, LGTM

@CBenoit
Benoît Cortier (CBenoit) merged commit 458545e into Devolutions:master Oct 1, 2026
41 checks passed
Anton Mostovoy (antonmos) added a commit to antonmos/IronRDP that referenced this pull request Oct 3, 2026
…ting-session-default

Resolves conflicts with Devolutions#2065 and the UDP-transport work:
- server.rs: keep both `ConnectionPolicy::default_for` and the new
  `MAX_EARLY_TUNNEL_PAYLOADS`; `dispatch_server_events` takes both
  `client_supports_errinfo` and `udp_transport` (with the same
  `too_many_arguments` expectation `client_loop` already carries), and the
  new unit test call passes the extra flag.
- e2e.rs: take master's shared `connect_client` / `local_addr_of` helpers
  and rebuild the Hybrid takeover test on top of them instead of keeping a
  second copy of the client handshake.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
llm-providers — 6f580143 Deployed Oct 1, 2026 by antonmos via Classify pull request #1461
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed/2 Two automated reviews completed kind/protocol Affects RDP or related protocol behavior needs-review A human reviewer is the current next actor risk/medium Behavioral change that does not substantially alter a core public API size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure

Development

Successfully merging this pull request may close these issues.

ConnectionPolicy::Preempt drops ConnectionHandler hooks fired during the served connection (on_connection_info never fires)

3 participants