Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(web-client): refactor iron-remote-gui into iron-remote-desktop #722

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

RRRadicalEdward
Copy link
Collaborator

@RRRadicalEdward RRRadicalEdward commented Mar 25, 2025

This is the first part of the "Modularize the remote desktop WebComponent" ticket.
PR changes the web-client of the IronRDP:

  • Renamed some of the structures on the Rust side
  • Removed the iron-remote-gui package
  • Added the iron-remote-desktop package with the TypeScript interfaces and exposed more interfaces from the WASM bindings
  • Added iron-remote-desktop-rdp package which combines WASM bindings and the TypeScript interfaces from the iron-remote-desktop to create an iron-remote-desktop HTML tag which is responsible for the RDP connection.
  • Updated xtask

Although GitHub says that there are 5K changes, most of the things are just file moves/renaming from iron-remote-gui to iron-remote-desktop and iron-remote-desktop-rdp. It doesn't seem to be able to diff it properly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This file and sessions.rs contain a good amount of duplicates with IronVNC logic.
Does it make sense to add a new crate(like iron-remote-desktop-rs) that would describe the interface of the Rust part of the project? It would include all the sharable structures and logic between IronVNC and IronRDP parts.
The components chain would look like this way:

  • VNC:
    UI (TS) -> iron-remote-desktop-vnc (TS) -> iron-remote-desktop (TS) -> iron-remote-desktop-rs (Rust) -> ironvnc-web (Rust)
  • RDP:
    UI (TS) -> iron-remote-desktop-rdp (TS) -> iron-remote-desktop (TS) -> iron-remote-desktop-rs (Rust) -> ironrdp-web (Rust)

Copy link
Member

Choose a reason for hiding this comment

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

I agree that we could provide a companion crate to avoid further code duplication! 👍
That would be a good follow up!
Let’s call this crate iron-remote-desktop, and make it live in the IronRDP repository.
Is it possible to export WASM functions from a dependency? My expectation is that it should be possible?
We could also provide a trait + macro helpers to define proper interfaces. Let’s discuss that on Slack!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Decided to do it in a follow-up PR

@RRRadicalEdward RRRadicalEdward self-assigned this Mar 25, 2025
@RRRadicalEdward RRRadicalEdward requested a review from CBenoit March 25, 2025 10:41
@CBenoit
Copy link
Member

CBenoit commented Mar 26, 2025

I think you are aware of this yourself as far as I can see in your own comments, but this is not really what I imagined when I talked about dependency injection.

To clarify: the new iron-remote-desktop package should contain ALL the logic that is found in the previous iron-remote-gui package.
The iron-remote-desktop-rdp package should only re-export the WASM module API, and nothing more. No additional logic. (Mostly? It’s fine to have a few helpers in addition to the rest.)
The architectural invariant is that these packages do not contain any common logic, and no code is duplicated.

I’m not very good at JavaScript/TypeScript, so let me illustrate what I mean using Rust:

// -- iron-remote-desktop --
// Implements all the backend-agnostic logic, based on a given "remote desktop interface".

trait RemoteDesktopApi {
    fn remote_desktop_init(&self);
}

fn run(api: Box<dyn RemoteDesktopApi>) {
    // The logic is calling the remote desktop API, and does not know whether this is RDP or VNC.
    api.remote_desktop_init();
}

// -- ironrdp-web --
// This is what actually implements the interface.
// But TypeScript is kind of working in a duck-typed way,
// so unlike Rust you don’t need to import the interface definition in order to implement it!

pub struct Api;

impl RemoteDesktopApi for Api {
    fn remote_desktop_init(&self) {
        // do stuff
    }
}

// -- iron-remote-desktop-rdp --
// The purpose is to inline the WASM module in base64,
// but essentially, it exposes the exact same things as ironrdp-web.

pub use ironrdp_web::Api;

// -- iron-svelte-client --
// Injects the dependency (IronRDP) into the web component (iron-remote-desktop), and uses it.
// It knows that its IronRDP behind the scenes, and may inject additional extensions (future improvement).

iron_remote_desktop::run(Box::new(iron_remote_desktop_rdp::Api));

Because of the way JavaScript/TypeScript works, we don’t need iron-remote-desktop-rdp to know about the interface defined in iron-remote-desktop.

Keep in mind that I’m only trying to illustrate what I meant for dependency injection. I do not ask you to modify drastically any Rust code (ideally ironrdp-web should stay the way it is, and there is no issue with the way you updated it in this PR.)
It’s not possible to directly translate this Rust code into TypeScript, and you will likely not define a "TypeScript interface" for the module itself.
My understanding is that when building ironrdp_web using wasmpack, we get a module exporting a few items such as SessionBuilder. SessionBuilder in turns implements a specific "typescript interface" (as defined in the Slack canvas).

My expectation is that iron-remote-desktop-rdp should basically end up being this:

export * as default from './path/to/wasm/module'

= Re-export the whole interface of the WASM module.

You can then import this module in iron-svelte-client, and inject it into the iron-remote-desktop component.

import * as rdp from '@devolutions/iron-remote-desktop-rdp'

// I think we have something called "userInteraction" somewhere?
// Use it to inject the module ("dependency").
userInteraction.setBackend(rdp);

// Or maybe it’s `userInteractionService` which wraps the `userInteraction`?
// Try to look for the `onMount` function.
// We retrieve the `userInteraction` (`event.detail.irgUserInteraction`) and
// store it in a `writeable` that is accessible globally.

(Also refer to this section.)

If you hit a problem, let me know in Slack so we can think through this together. I never implemented such a thing myself, so I don’t know all the problems you may encounter beforehand in details, but I have a solid idea of what it should looks like.

@RRRadicalEdward
Copy link
Collaborator Author

RRRadicalEdward commented Mar 27, 2025

#722 (comment)

Let's clarify: iron-remote-desktop-rdp has to re-export WASM and provide extension types. The next step is to modify the UserInteraction to accept the "backend" which will be either RDP or VNC and implements an interface (I have really missed the fact that JS/TS has a duck types). The next step should be the refactoring of the connect function into a builder (or I will leave it to a follow-up PR if there won't be a direct need in it right now).

@CBenoit
Copy link
Member

CBenoit commented Mar 27, 2025

#722 (comment)

Let's clarify: iron-remote-desktop-rdp has to re-export WASM and provide extension types. The next step is to modify the UserInteraction to accept the "backend" which will be either RDP or VNC and implements an interface (I have really missed the fact that JS/TS has a duck types). The next step should be the refactoring of the connect function into a builder (or I will leave it to a follow-up PR if there won't be a direct need in it right now).

Exactly!
For this PR, really focus on the core architectural refactoring.
Anything that can be a follow up should be a follow up 🙂

import type { ClipboardTransaction as IClipboardTransaction } from './interfaces/ClipboardTransaction';
import { ClipboardContent, ClipboardTransaction } from './../../iron-remote-desktop-rdp/src/main';
import * as remote_desktop from './../../iron-remote-desktop-rdp/src/main';
// import { ClipboardContent, ClipboardTransaction } from './../../../../ironvnc/web-client/iron-remote-desktop-vnc/src/main';
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here as you can see, we can import VNC instead of an RDP and this will inject selected module into a WasmBridgeService

Copy link
Member

Choose a reason for hiding this comment

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

Wonderful!

Copy link
Member

Choose a reason for hiding this comment

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

Wait, I was expecting this logic to be in iron-svelte-client, not iron-remote-desktop.
Is it possible to choose between injecting IronRDP or injecting IronVNC from iron-svelte-client?

@RRRadicalEdward RRRadicalEdward force-pushed the modularize-remote-desktop branch from 3a4a47a to f7a4b86 Compare March 28, 2025 13:25
@RRRadicalEdward RRRadicalEdward requested a review from CBenoit March 28, 2025 13:27
Copy link

github-actions bot commented Mar 28, 2025

Coverage Report 🤖 ⚙️

Past:
Total lines: 30336
Covered lines: 19473 (64.19%)

New:
Total lines: 30336
Covered lines: 19473 (64.19%)

Diff: +0.00%

[this comment will be updated automatically]

@RRRadicalEdward RRRadicalEdward force-pushed the modularize-remote-desktop branch from f7a4b86 to b5d6fdf Compare March 28, 2025 16:02
Comment on lines -171 to 180
#### [`web-client/iron-remote-gui`](./web-client/iron-remote-gui)
#### [`web-client/iron-remote-desktop`](./web-client/iron-remote-desktop)

TypeScript interfaces exposed by WebAssembly bindings from `ironrdp-web` and used by `iron-remote-desktop-rdp`.

This crate is an **API Boundary**.

#### [`web-client/iron-remote-desktop-rdp`](./web-client/iron-remote-desktop-rdp)

Core frontend UI used by `iron-svelte-client` as a Web Component.

Copy link
Member

Choose a reason for hiding this comment

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

note: This needs to be updated

pub fn new() -> Self {
pub fn construct() -> Self {
Copy link
Member

@CBenoit CBenoit Mar 28, 2025

Choose a reason for hiding this comment

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

question: Is there a reason for moving away from new here?

self.0.borrow_mut().use_display_control = use_display_control
}
},
Err(err) => error!("Provided JsValue is not a valid extension value: {err:?}"),
Copy link
Member

Choose a reason for hiding this comment

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

https://github.com/Devolutions/IronRDP/blob/401cedf01014d8400b460911764d9ebc837900fc/STYLE.md#logging

Suggested change
Err(err) => error!("Provided JsValue is not a valid extension value: {err:?}"),
Err(error) => error!(%error, "Provided JsValue is not a valid extension value"),

@@ -297,11 +301,11 @@ impl SessionBuilder {
loop {
match ws.state() {
websocket::State::Closing | websocket::State::Closed => {
return Err(IronRdpError::from(anyhow::anyhow!(
return Err(RemoteDesktopError::from(anyhow::anyhow!(
"Failed to connect to {proxy_address} (WebSocket is `{:?}`)",
Copy link
Member

Choose a reason for hiding this comment

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

Not something you added, but this should be:

Suggested change
"Failed to connect to {proxy_address} (WebSocket is `{:?}`)",
"failed to connect to {proxy_address} (WebSocket is `{:?}`)",

Comment on lines +834 to +835
match serde_wasm_bindgen::from_value::<SessionExtensionCall>(value)
.map_err(|err| anyhow::anyhow!("provided JsValue is not a valid extension call: {err:?}"))?
Copy link
Member

Choose a reason for hiding this comment

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

style: Can be simplified:

Suggested change
match serde_wasm_bindgen::from_value::<SessionExtensionCall>(value)
.map_err(|err| anyhow::anyhow!("provided JsValue is not a valid extension call: {err:?}"))?
match serde_wasm_bindgen::from_value::<SessionExtensionCall>(value)
.context("provided JsValue is not a valid extension call")?

Comment on lines +1080 to +1081
RemoteDesktopError::from(anyhow::anyhow!("received an RDCleanPath error: {error}"))
.with_kind(RemoteDesktopErrorKind::RDCleanPath),
Copy link
Member

Choose a reason for hiding this comment

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

note: Again, not your code, but it’s better to do this:

Suggested change
RemoteDesktopError::from(anyhow::anyhow!("received an RDCleanPath error: {error}"))
.with_kind(RemoteDesktopErrorKind::RDCleanPath),
RemoteDesktopError::from(anyhow::Error::new(error).context("received an RDCleanPath error"))
.with_kind(RemoteDesktopErrorKind::RDCleanPath),

pub fn ironrdp_init(log_level: &str) {
pub fn remote_desktop_init(log_level: &str) {
Copy link
Member

@CBenoit CBenoit Mar 28, 2025

Choose a reason for hiding this comment

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

naming: iron_init

@@ -2,7 +2,7 @@

IronRDP also supports the web browser as a first class target.

See the [iron-remote-gui](./iron-remote-gui) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.
See the [iron-remote-desktop-rdp](./iron-remote-desktop-rdp) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.
Copy link
Member

@CBenoit CBenoit Mar 28, 2025

Choose a reason for hiding this comment

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

issue: The reusable web component is iron-remote-desktop.

Suggested change
See the [iron-remote-desktop-rdp](./iron-remote-desktop-rdp) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.
See the [iron-remote-desktop](./iron-remote-desktop) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.

iron-remote-desktop-rdp would be the RDP backend based on IronRDP that we inject (or not) into iron-remote-desktop.

Copy link
Member

Choose a reason for hiding this comment

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

This should be the README of iron-remote-desktop, not iron-remote-desktop-rdp I think

Copy link
Member

@CBenoit CBenoit Mar 28, 2025

Choose a reason for hiding this comment

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

issue: I think pre-build.js should be in iron-remote-desktop, not iron-remote-desktop-rdp

suggestion: Maybe double check you didn’t needlessly copied too many files into iron-remote-desktop-rdp?

new_key_released(scancode: number): DeviceEvent;
new_unicode_pressed(unicode: string): DeviceEvent;
new_unicode_released(unicode: string): DeviceEvent;
free(): void;
Copy link
Member

@CBenoit CBenoit Mar 28, 2025

Choose a reason for hiding this comment

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

question: Do we need functions such as free in these interfaces?

This is a simple wrapper around the `iron-remote-gui` Web Component demonstrating how to use the API.
This is a simple wrapper around the `iron-remote-desktop-rdp` Web Component demonstrating how to use the API.
Copy link
Member

Choose a reason for hiding this comment

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

issue: Needs update. The web component is from iron-remote-desktop. You can also mention iron-remote-desktop-rdp as the backend. My expectation is that iron-svelte-client has a dependency on both iron-remote-desktop and iron-remote-desktop-rdp.

Comment on lines +3 to +4
const IRON_REMOTE_DESKTOP_PATH: &str = "./web-client/iron-remote-desktop";
const IRON_REMOTE_DESKTOP_PATH_RDP: &str = "./web-client/iron-remote-desktop-rdp";
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const IRON_REMOTE_DESKTOP_PATH: &str = "./web-client/iron-remote-desktop";
const IRON_REMOTE_DESKTOP_PATH_RDP: &str = "./web-client/iron-remote-desktop-rdp";
const IRON_REMOTE_DESKTOP_PATH: &str = "./web-client/iron-remote-desktop";
const IRON_REMOTE_DESKTOP_RDP_PATH: &str = "./web-client/iron-remote-desktop-rdp";

@CBenoit
Copy link
Member

CBenoit commented Mar 28, 2025

Good work! I like the direction!

Let me clarify my expectations further:

  • I want to publish three NPM packages: iron-remote-desktop, iron-remote-desktop-rdp and iron-remote-desktop-vnc
  • iron-svelte-client should have a dependency on both iron-remote-desktop and iron-remote-desktop-rdp.
    • Dependency injection happens in iron-svelte-client, not in iron-remote-desktop. iron-remote-desktop does not know about iron-remote-desktop-rdp or iron-remote-desktop-vnc.
    • We could even rename iron-svelte-client into ironrdp-svelte-client, to clarify that it is only used as a testing web client for the IronRDP backend. (In a follow up PR.)
    • Based on the above point, we could have ironvnc-svelte-client in the IronVNC repository. A similar test web client for IronVNC, using iron-remote-desktop-vnc. (Let’s discuss before going ahead with that.)
  • In the Devolutions Gateway standalone, we have a dependency on all the NPM packages: iron-remote-desktop, iron-remote-desktop-rdp and iron-remote-desktop-vnc.
    • We inject either iron-remote-desktop-rdp or iron-remote-desktop-vnc depending on whether we want to connect to an RDP server, or a VNC server.
  • Only the consumer knows about which backend is loaded, and can use extensions as appropriate. iron-remote-desktop does not know that.

Comment on lines -429 to +452
this.session?.synchronize_lock_keys(
syncScrollLockActive,
syncNumsLockActive,
syncCapsLockActive,
syncKanaModeActive,
);
this.session?.extension_call({
SynchronizeLockKeys: {
scroll_lock: syncScrollLockActive,
num_lock: syncNumsLockActive,
caps_lock: syncCapsLockActive,
kana_lock: syncKanaModeActive,
},
});
Copy link
Member

Choose a reason for hiding this comment

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

thought: Maybe it’s simpler to keep synchronize_lock_keys in the common API, the same way as previously. It’s fine if it’s a no-op in ironvnc-web.

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.

2 participants