feat(dvc): add DvcChannelListener for multi-instance DVC support#1142
feat(dvc): add DvcChannelListener for multi-instance DVC support#1142Benoît Cortier (CBenoit) merged 9 commits intoDevolutions:masterfrom
Conversation
|
uchouT (@uchouT) lgtm, perhaps squash or reorganize some of your changes (PERF, ..) |
7bc14b1 to
71d1115
Compare
Squashed, thanks for reminding. I found that ironrdp-session would be affected by this, but I didn't go further. IronRDP/crates/ironrdp-session/src/x224/mod.rs Lines 98 to 100 in db2f40b 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, |
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. |
There was a problem hiding this comment.
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
DvcChannelListenertrait andOnceListeneradapter, refactorsDynamicChannelSet(moved fromlib.rstoclient.rs) to key active channels byDynamicChannelIdrather than by name - Adds
DrdynvcServer::create_channel()for mid-session server-side DVC channel creation - Removes the now-obsolete
DynamicChannelSetstruct fromlib.rs, replacingDynamicVirtualChannel::newwithfrom_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.
There was a problem hiding this comment.
Addressed Copilot's review. Benoît Cortier (@CBenoit)
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>
a6fffe4 to
5f199ea
Compare
|
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>
5f199ea to
ea96578
Compare
|
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! |
There was a problem hiding this comment.
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>
uchouT (uchouT)
left a comment
There was a problem hiding this comment.
Thanks for the detailed review. I've applied the suggestions and addressed the comments in the latest commits.
|
Are you sure this was a problem? It seems the compilation was successful |
Signed-off-by: uchouT <i@uchout.moe>
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>
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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_idcallsTypeId::of::<T>(), which requiresT: 'static, but the current bounds only requireT: DvcProcessor. This won’t compile for non-'staticprocessor types; add a+ 'staticbound (orT: 'static) to the method’swhereclause to satisfyTypeId::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.
| 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) | ||
| } |
|
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? |
|
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 Besides, I noticed that DrdynvcClient silently overwrites the existing listener/channel if one is attached with the same name. The existing IronRDP/crates/ironrdp-dvc/src/client.rs Lines 65 to 70 in 9a1ac30 Should I add some documentation to clarify this before we merge? |
Signed-off-by: uchouT <i@uchout.moe>
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
LGTM! Excellent work!
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.
I see you added it already. Thank you! |
28e8628
into
Devolutions:master
Resolves #1135
OnceListenerwrapper for pre-registered DVCs.create_channelforDrdynvcServer.older discussion
I try to implement the client-side without breaking the current API signatures by introducing
OnceListenerwrapper for pre-registered DVCs. Butget_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?