Skip to content

feat(dvc): add DvcChannelListener for multi-instance DVC support#1142

Merged
Benoît Cortier (CBenoit) merged 9 commits intoDevolutions:masterfrom
uchouT:refactor/dvc
Mar 13, 2026
Merged

feat(dvc): add DvcChannelListener for multi-instance DVC support#1142
Benoît Cortier (CBenoit) merged 9 commits intoDevolutions:masterfrom
uchouT:refactor/dvc

Conversation

@uchouT
Copy link
Contributor

@uchouT uchouT (uchouT) commented Mar 3, 2026

Resolves #1135

  • implement the client-side without breaking the current API signatures by introducing OnceListener wrapper for pre-registered DVCs.
  • implement server side with a new method create_channel for DrdynvcServer.

older discussion

I try to implement the client-side without breaking the current API signatures by introducing OnceListener wrapper for pre-registered DVCs. But get_dvc_by_type_id's behavior has changed, it returns None until CreateRequest arrives.

This is unavoidable — the old 1:1 name→channel mapping cannot support multiple channels with the same name. The ChannelId is assigned by the server at CreateRequest time, so the DVC cannot exist before that point. And this behavior follows the RDPEDYC's expectation.

Marc-Andre Lureau (@elmarco) Benoît Cortier (@CBenoit) Does this approach acceptable and make sense?

@uchouT uchouT (uchouT) changed the title Refactor/dvc feat(dvc): add DvcChannelListener for multi-instance DVC support Mar 3, 2026
@elmarco
Copy link
Contributor

uchouT (@uchouT) lgtm, perhaps squash or reorganize some of your changes (PERF, ..)

@uchouT uchouT (uchouT) force-pushed the refactor/dvc branch 2 times, most recently from 7bc14b1 to 71d1115 Compare March 3, 2026 12:21
@uchouT
Copy link
Contributor Author

uchouT (uchouT (@uchouT)) lgtm, perhaps squash or reorganize some of your changes (PERF, ..)

Squashed, thanks for reminding.

I found that ironrdp-session would be affected by this, but I didn't go further.

pub fn get_dvc<T: DvcProcessor + 'static>(&self) -> Option<&DynamicVirtualChannel> {
self.get_svc_processor::<DrdynvcClient>()?.get_dvc_by_type_id::<T>()
}

If this is acceptable, I will try the server-side implementation.

@glamberson
Copy link
Contributor

If this is acceptable, I will try the server-side implementation.

This is good work. The listener/factory pattern is exactly the right approach for multi-instance DVCs. The OnceListener wrapper preserving backward compatibility for single-instance channels is a nice touch.

We're using IronRDP on the server side (lamco-rdp-server) and this direction aligns well with what we'll need for URBDRC support down the road. The encode_dvc_messages() and DvcProcessor trait being untouched means our current code should be unaffected by the client-side refactor.

Looking forward to the server-side implementation with take_new_channels(). Happy to help test once that lands.

Regards,
Greg Lamberson
Lamco Development

@uchouT
Copy link
Contributor Author

Looking forward to the server-side implementation with take_new_channels(). Happy to help test once that lands.

Thanks for the review, the positive feedback, and your willingness to help test, Greg Lamberson (@glamberson)!

Regarding the take_new_channels() approach, I still have some hesitations about the implementation details. I left my thoughts here #1135 (comment) , would love to hear your input.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements multi-instance DVC (Dynamic Virtual Channel) support by introducing a DvcChannelListener factory trait that produces a new DvcClientProcessor instance per DYNVC_CREATE_REQ. It preserves the existing single-instance API by internally wrapping pre-registered DvcProcessor instances in an OnceListener. On the server side, a new create_channel method is added to DrdynvcServer to allow opening new DVC channels mid-session.

Changes:

  • Introduces DvcChannelListener trait and OnceListener adapter, refactors DynamicChannelSet (moved from lib.rs to client.rs) to key active channels by DynamicChannelId rather than by name
  • Adds DrdynvcServer::create_channel() for mid-session server-side DVC channel creation
  • Removes the now-obsolete DynamicChannelSet struct from lib.rs, replacing DynamicVirtualChannel::new with from_boxed

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
crates/ironrdp-dvc/src/client.rs Adds DvcChannelListener trait + OnceListener, refactors DynamicChannelSet to support multi-instance channels, updates process() to use new listener-based creation flow
crates/ironrdp-dvc/src/lib.rs Removes DynamicChannelSet, changes DynamicVirtualChannel constructor to from_boxed
crates/ironrdp-dvc/src/server.rs Adds create_channel() for mid-session DVC channel creation; removes stale FIXME comment

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Contributor Author

@uchouT uchouT (uchouT) left a comment

Choose a reason for hiding this comment

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

Addressed Copilot's review. Benoît Cortier (@CBenoit)

uchouT (uchouT) and others added 3 commits March 10, 2026 02:01
Signed-off-by: uchouT <i@uchout.moe>
Introduce DvcChannelListener trait and OnceListener to support same-name
multi-instance dynamic virtual channels. DVCs are now created on-demand
via try_create_channel() when the server sends a CreateRequest, rather
than pre-allocated at registration. The existing with_dynamic_channel()
API is preserved via OnceListener.

BREAKING: get_dvc_by_type_id() now returns None until the server sends a
CreateRequest for the channel. Previously it returned Some immediately
after registration.

Signed-off-by: uchouT <i@uchout.moe>

perf(dvc): reduce another btreemap search

Signed-off-by: uchouT <i@uchout.moe>

perf(dvc): reduce needless allocation in OnceListener

Signed-off-by: uchouT <i@uchout.moe>
Unlike `with_dynamic_channel`, this method is designed for runtime use
and doesn't record a TypeId mapping.

Signed-off-by: uchouT <i@uchout.moe>

chore(dvc): docs modified and new fn attach_listener

Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@uchouT
Copy link
Contributor Author

Benoît Cortier (@CBenoit) resolved merge conflicts and rebased.

Signed-off-by: uchouT <i@uchout.moe>

chore(dvc): docs update, clarify limitation

Signed-off-by: uchouT <i@uchout.moe>

chore(dvc): clean style for edition 2024

Signed-off-by: uchouT <i@uchout.moe>

chore: sytle fmt

Signed-off-by: uchouT <i@uchout.moe>
@uchouT
Copy link
Contributor Author

Sorry about the formatting issue! My local cargo fmt was unusable due to the transition from Rust edition 2021 to 2024, so I had to fix it manually.

It should be good now. Benoît Cortier (@CBenoit) , could you please re-trigger the CI when you have a chance? Thanks!

Copy link
Member

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

Choose a reason for hiding this comment

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

Great work! I think this is going in the right direction, thank you!

I’m being a little bit more picky because this is a fundamental building block for every dynamic virtual channel 🙂

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Signed-off-by: uchouT <i@uchout.moe>
Copy link
Contributor Author

@uchouT uchouT (uchouT) left a comment

Choose a reason for hiding this comment

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

Thanks for the detailed review. I've applied the suggestions and addressed the comments in the latest commits.

@CBenoit
Copy link
Member

Are you sure this was a problem? It seems the compilation was successful

Signed-off-by: uchouT <i@uchout.moe>
@uchouT
Copy link
Contributor Author

Are you sure this was a problem? It seems the compilation was successful

Yes, the compilation was actually fine. I updated it mostly to keep the signature consistent with other methods. I removed the Send bound as you mentioned, and added the 'static bound (which Copilot also suggested, and it makes sense here).

Also, regarding this discussion, does my explanation make sense to you?

Signed-off-by: uchouT <i@uchout.moe>
Copy link
Member

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

Choose a reason for hiding this comment

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

At this point, this is looking good to me. Let’s address the last nits just because it’s a fundamental building blocks + add one last pass of Copilot (you don’t have to address the "useless" suggestions of course!) just to be sure, and merge 🚀

@uchouT
Copy link
Contributor Author

At this point, this is looking good to me. Let’s address the last nits just because it’s a fundamental building blocks + add one last pass of Copilot (you don’t have to address the "useless" suggestions of course!) just to be sure, and merge 🚀

Nice to hear about that! I've updated. Please check, and if this is Ok I'll squash and rebase and ready for merge.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

crates/ironrdp-dvc/src/client.rs:133

  • get_dvc_by_type_id calls TypeId::of::<T>(), which requires T: 'static, but the current bounds only require T: DvcProcessor. This won’t compile for non-'static processor types; add a + 'static bound (or T: 'static) to the method’s where clause to satisfy TypeId::of’s requirements.
    pub fn get_dvc_by_type_id<T>(&self) -> Option<&DynamicVirtualChannel>
    where
        T: DvcProcessor,
    {
        self.dynamic_channels.get_by_type_id(TypeId::of::<T>())
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +277 to +299
fn try_create_channel(
&mut self,
name: &DynamicChannelName,
channel_id: DynamicChannelId,
) -> Option<&mut DynamicVirtualChannel> {
let entry = self.listeners.get_mut(name)?;
let processor = entry.listener.create()?;

if let Some(type_id) = entry.type_id {
self.type_id_to_channel_id.insert(type_id, channel_id);
}

let mut dvc = DynamicVirtualChannel::from_boxed(processor);
dvc.channel_id = Some(channel_id);
let dvc = match self.active_channels.entry(channel_id) {
alloc::collections::btree_map::Entry::Occupied(mut e) => {
e.insert(dvc);
e.into_mut()
}
alloc::collections::btree_map::Entry::Vacant(e) => e.insert(dvc),
};
Some(dvc)
}
@CBenoit
Copy link
Member

I think the last two Copilot suggestions are valid. Do you think you could look into this before we merge, or you prefer to send a follow up PR?

@uchouT
Copy link
Contributor Author

uchouT (uchouT) commented Mar 13, 2026

Agreed. I'd prefer to handle the integration tests in a follow-up PR, if that works for you. cc Greg Lamberson (@glamberson)

I believe the current state is safe to merge because the only potential panic risk would be calling OnceListener::channel_name() twice, but that instance is fully encapsulated and never exposed to users. ( DrdynvcClient doesn't have a get_listener API.

Besides, I noticed that DrdynvcClient silently overwrites the existing listener/channel if one is attached with the same name. The existing attach_dynamic_channel API actually shares this behavior.

pub fn attach_dynamic_channel<T>(&mut self, channel: T)
where
T: DvcProcessor + 'static,
{
self.dynamic_channels.insert(channel);
}

Should I add some documentation to clarify this before we merge?

Signed-off-by: uchouT <i@uchout.moe>
Copy link
Member

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

Choose a reason for hiding this comment

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

LGTM! Excellent work!

@CBenoit
Copy link
Member

I believe the current state is safe to merge because the only potential panic risk would be calling OnceListener::channel_name() twice, but that instance is fully encapsulated and never exposed to users. ( DrdynvcClient doesn't have a get_listener API.

I’m not really worried about panics indeed. And we could wait for the RDPEUSB implementation to perform an integration test close to real-world usecases.

Should I add some documentation to clarify this before we merge?

I see you added it already. Thank you!

@CBenoit Benoît Cortier (CBenoit) enabled auto-merge (squash) March 13, 2026 13:37
@CBenoit Benoît Cortier (CBenoit) merged commit 28e8628 into Devolutions:master Mar 13, 2026
10 checks passed
@uchouT uchouT (uchouT) deleted the refactor/dvc branch March 13, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[ironrdp-dvc] Support multiple DVC channel instances with the same name

5 participants