refactor: rework extension API for iron-remote-desktop (#762)

This commit is contained in:
Benoît Cortier
2025-04-21 09:33:12 -04:00
committed by GitHub
parent 45884c5d38
commit f287e168a8
17 changed files with 159 additions and 143 deletions
Generated
-13
View File
@@ -2827,8 +2827,6 @@ dependencies = [
"resize",
"rgb",
"semver",
"serde",
"serde-wasm-bindgen",
"smallvec",
"softbuffer",
"tap",
@@ -4716,17 +4714,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_bytes"
version = "0.11.17"
@@ -0,0 +1,15 @@
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct DesktopSize {
pub width: u16,
pub height: u16,
}
#[wasm_bindgen]
impl DesktopSize {
pub fn init(width: u16, height: u16) -> Self {
DesktopSize { width, height }
}
}
@@ -0,0 +1,68 @@
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
#[macro_export]
macro_rules! extension_match {
( @ $jsval:expr, $value:ident, String, $operation:block ) => {{
if let Some($value) = $jsval.as_string() {
$operation
} else {
warn!("Unexpected value for extension {}", stringify!($ident));
}
}};
( @ $jsval:expr, $value:ident, f64, $operation:block ) => {{
if let Some($value) = $jsval.as_f64() {
$operation
} else {
warn!("Unexpected value for extension {}", stringify!($ident));
}
}};
( @ $jsval:expr, $value:ident, bool, $operation:block ) => {{
if let Some($value) = $jsval.as_bool() {
$operation
} else {
warn!("Unexpected value for extension {}", stringify!($ident));
}
}};
( @ $jsval:expr, $value:ident, JsValue, $operation:block ) => {{
let $value = $jsval;
$operation
}};
( match $ext:ident ; $( | $value:ident : $ty:ident | $operation:block ; )* ) => {
let ident = $ext.ident();
match ident {
$( stringify!($value) => $crate::extension_match!( @ $ext.into_value(), $value, $ty, $operation ), )*
unknown_extension => ::tracing::warn!("Unknown extension: {unknown_extension}"),
}
};
}
#[wasm_bindgen]
pub struct Extension {
ident: String,
value: JsValue,
}
#[wasm_bindgen]
impl Extension {
#[wasm_bindgen(constructor)]
pub fn new(ident: String, value: JsValue) -> Self {
Self { ident, value }
}
}
impl Extension {
pub fn ident(&self) -> &str {
self.ident.as_str()
}
pub fn value(&self) -> &JsValue {
&self.value
}
pub fn into_value(self) -> JsValue {
self.value
}
}
+8 -19
View File
@@ -1,29 +1,18 @@
mod clipboard;
mod cursor;
mod desktop_size;
mod error;
mod extension;
mod input;
mod session;
pub use clipboard::{ClipboardContent, ClipboardTransaction};
pub use cursor::CursorStyle;
pub use desktop_size::DesktopSize;
pub use error::{IronError, IronErrorKind};
pub use extension::Extension;
pub use input::{DeviceEvent, InputTransaction};
pub use session::{Session, SessionBuilder, SessionTerminationInfo};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct DesktopSize {
pub width: u16,
pub height: u16,
}
#[wasm_bindgen]
impl DesktopSize {
pub fn init(width: u16, height: u16) -> Self {
DesktopSize { width, height }
}
}
pub trait RemoteDesktopApi {
type Session: Session;
@@ -194,8 +183,8 @@ macro_rules! export {
self.0.supports_unicode_keyboard_shortcuts()
}
pub fn extension_call(value: JsValue) -> Result<JsValue, IronError> {
<<$api as RemoteDesktopApi>::Session>::extension_call(value).map_err(IronError)
pub fn extension_call(ext: $crate::Extension) -> Result<JsValue, IronError> {
<<$api as RemoteDesktopApi>::Session>::extension_call(ext).map_err(IronError)
}
}
@@ -260,8 +249,8 @@ macro_rules! export {
Self(self.0.force_clipboard_update_callback(callback))
}
pub fn extension(&self, value: JsValue) -> Self {
Self(self.0.extension(value))
pub fn extension(&self, ext: $crate::Extension) -> Self {
Self(self.0.extension(ext))
}
pub async fn connect(&self) -> Result<Session, IronError> {
+3 -3
View File
@@ -4,7 +4,7 @@ use web_sys::{js_sys, HtmlCanvasElement};
use crate::clipboard::ClipboardTransaction;
use crate::error::IronError;
use crate::input::InputTransaction;
use crate::DesktopSize;
use crate::{DesktopSize, Extension};
pub trait SessionBuilder {
type Session: Session;
@@ -38,7 +38,7 @@ pub trait SessionBuilder {
#[must_use]
fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self;
#[must_use]
fn extension(&self, value: JsValue) -> Self;
fn extension(&self, ext: Extension) -> Self;
#[expect(async_fn_in_trait)]
async fn connect(&self) -> Result<Self::Session, Self::Error>;
}
@@ -74,7 +74,7 @@ pub trait Session {
physical_height: Option<u32>,
);
fn supports_unicode_keyboard_shortcuts(&self) -> bool;
fn extension_call(value: JsValue) -> Result<JsValue, Self::Error>;
fn extension_call(ext: Extension) -> Result<JsValue, Self::Error>;
}
pub trait SessionTerminationInfo {
-2
View File
@@ -46,8 +46,6 @@ web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
js-sys = "0.3"
gloo-net = { version = "0.6", default-features = false, features = ["websocket", "http", "io-util"] }
gloo-timers = { version = "0.3", default-features = false, features = ["futures"] }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
# Rendering
softbuffer = { version = "0.4", default-features = false }
+14 -26
View File
@@ -1,6 +1,3 @@
// https://github.com/rustwasm/wasm-bindgen/issues/4080
#![allow(non_snake_case)]
use core::cell::RefCell;
use core::num::NonZeroU32;
use core::time::Duration;
@@ -15,7 +12,7 @@ use futures_util::io::{ReadHalf, WriteHalf};
use futures_util::{select, AsyncWriteExt as _, FutureExt as _, StreamExt as _};
use gloo_net::websocket;
use gloo_net::websocket::futures::WebSocket;
use iron_remote_desktop::{CursorStyle, DesktopSize, IronErrorKind};
use iron_remote_desktop::{CursorStyle, DesktopSize, Extension, IronErrorKind};
use ironrdp::cliprdr::backend::ClipboardMessage;
use ironrdp::cliprdr::CliprdrClient;
use ironrdp::connector::connection_activation::ConnectionActivationState;
@@ -31,7 +28,6 @@ use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDiscon
use ironrdp_core::WriteBuf;
use ironrdp_futures::{single_sequence_step_read, FramedWrite};
use rgb::AsPixels as _;
use serde::{Deserialize, Serialize};
use tap::prelude::*;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::spawn_local;
@@ -208,16 +204,12 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
self.clone()
}
fn extension(&self, value: JsValue) -> Self {
match serde_wasm_bindgen::from_value::<Extension>(value) {
Ok(value) => match value {
Extension::KdcProxyUrl(kdc_proxy_url) => self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url),
Extension::Pcb(pcb) => self.0.borrow_mut().pcb = Some(pcb),
Extension::DisplayControl(use_display_control) => {
self.0.borrow_mut().use_display_control = use_display_control
}
},
Err(error) => error!(%error, "Unsupported extension value"),
fn extension(&self, ext: Extension) -> Self {
iron_remote_desktop::extension_match! {
match ext;
|pcb: String| { self.0.borrow_mut().pcb = Some(pcb) };
|kdc_proxy_url: String| { self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url) };
|display_control: bool| { self.0.borrow_mut().use_display_control = display_control };
}
self.clone()
@@ -355,13 +347,6 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
}
}
#[derive(Debug, Serialize, Deserialize)]
enum Extension {
KdcProxyUrl(String),
Pcb(String),
DisplayControl(bool),
}
pub(crate) type FastPathInputEvents = smallvec::SmallVec<[FastPathInputEvent; 2]>;
#[derive(Debug)]
@@ -814,13 +799,16 @@ impl iron_remote_desktop::Session for Session {
}
fn supports_unicode_keyboard_shortcuts(&self) -> bool {
// RDP does not support Unicode keyboard shortcuts (When key combinations are executed, only
// plain scancode events are allowed to function correctly).
// RDP does not support Unicode keyboard shortcuts.
// When key combinations are executed, only plain scancode events are allowed to function correctly.
false
}
fn extension_call(_value: JsValue) -> Result<JsValue, Self::Error> {
Ok(JsValue::null())
fn extension_call(ext: Extension) -> Result<JsValue, Self::Error> {
Err(
IronError::from(anyhow::Error::msg(format!("unknown extension: {}", ext.ident())))
.with_kind(IronErrorKind::General),
)
}
}
@@ -1,28 +0,0 @@
type ExtensionValue = { Pcb: string } | { KdcProxyUrl: string } | { DisplayControl: boolean };
export class Extension {
static init(ident: string, value: unknown): ExtensionValue {
switch (ident) {
case 'Pcb':
if (typeof value === 'string') {
return { Pcb: value };
} else {
throw new Error('Pcb must be a string');
}
case 'KdcProxyUrl':
if (typeof value === 'string') {
return { KdcProxyUrl: value };
} else {
throw new Error('KdcProxyUrl must be a string');
}
case 'DisplayControl':
if (typeof value === 'boolean') {
return { DisplayControl: value };
} else {
throw new Error('DisplayControl must be a boolean');
}
default:
throw new Error(`Invalid extension type: ${ident}`);
}
}
}
+13 -1
View File
@@ -9,8 +9,8 @@ import init, {
SessionTerminationInfo,
ClipboardTransaction,
ClipboardContent,
Extension,
} from '../../../crates/ironrdp-web/pkg/ironrdp_web';
import { Extension } from './interfaces/Extension';
export default {
init,
@@ -26,3 +26,15 @@ export default {
SessionTerminationInfo,
Extension,
};
export function preConnectionBlob(pcb: string): Extension {
return new Extension('pcb', pcb);
}
export function displayControl(enable: boolean): Extension {
return new Extension('display_control', enable);
}
export function kdcProxyUrl(url: string): Extension {
return new Extension('kdc_proxy_url', url);
}
@@ -1,5 +1 @@
import type { ExtensionValue } from './ExtensionValue';
export interface Extension {
init(ident: string, value: unknown): ExtensionValue;
}
export type Extension = unknown;
@@ -1 +0,0 @@
export type ExtensionValue = unknown;
@@ -7,7 +7,6 @@ import type { SessionBuilder } from './SessionBuilder';
import type { SessionTerminationInfo } from './SessionTerminationInfo';
import type { ClipboardTransaction } from './ClipboardTransaction';
import type { ClipboardContent } from './ClipboardContent';
import type { Extension } from './Extension';
export interface RemoteDesktopModule {
init: () => Promise<unknown>;
@@ -21,5 +20,4 @@ export interface RemoteDesktopModule {
SessionTerminationInfo: SessionTerminationInfo;
ClipboardTransaction: ClipboardTransaction;
ClipboardContent: ClipboardContent;
Extension: Extension;
}
@@ -1,5 +1,5 @@
import type { DesktopSize } from '../interfaces/DesktopSize';
import type { ExtensionValue } from '../interfaces/ExtensionValue';
import type { Extension } from '../interfaces/Extension';
export class Config {
readonly username: string;
@@ -9,7 +9,7 @@ export class Config {
readonly serverDomain: string;
readonly authToken: string;
readonly desktopSize?: DesktopSize;
readonly extensions: ExtensionValue[];
readonly extensions: Extension[];
constructor(
userData: { username: string; password: string },
@@ -17,7 +17,7 @@ export class Config {
configOptions: {
destination: string;
serverDomain: string;
extensions: ExtensionValue[];
extensions: Extension[];
desktopSize?: DesktopSize;
},
) {
@@ -1,8 +1,6 @@
import type { DesktopSize } from '../interfaces/DesktopSize';
import { Config } from './Config';
import type { ExtensionValue } from '../interfaces/ExtensionValue';
type ExtensionConstructor = (ident: string, value: unknown) => ExtensionValue;
import type { Extension } from '../interfaces/Extension';
/**
* Builder class for creating Config objects with a fluent interface.
@@ -19,8 +17,6 @@ type ExtensionConstructor = (ident: string, value: unknown) => ExtensionValue;
* ```
*/
export class ConfigBuilder {
private extensionConstructor: ExtensionConstructor;
private username: string = '';
private password: string = '';
private destination: string = '';
@@ -28,17 +24,12 @@ export class ConfigBuilder {
private serverDomain: string = '';
private authToken: string = '';
private desktopSize?: DesktopSize;
private extensions: ExtensionValue[] = [];
private extensions: Extension[] = [];
/**
* Creates a new ConfigBuilder instance.
*
* @param extensionConstructor - Function that creates extension values from identifiers and values.
*/
constructor(extensionConstructor: ExtensionConstructor) {
this.extensionConstructor = extensionConstructor;
}
constructor() {}
/**
* Optional parameter
@@ -109,12 +100,11 @@ export class ConfigBuilder {
/**
* Optional parameter
*
* @param ident - The identifier for the extension
* @param value - The value for the extension
* @param ext - The extension
* @returns The builder instance for method chaining
*/
withExtension(ident: string, value: unknown): ConfigBuilder {
this.extensions.push(this.extensionConstructor(ident, value));
withExtension(ext: Extension): ConfigBuilder {
this.extensions.push(ext);
return this;
}
@@ -137,7 +137,7 @@ export class RemoteDesktopService {
}
configBuilder(): ConfigBuilder {
return new ConfigBuilder(this.module.Extension.init);
return new ConfigBuilder();
}
connect(config: Config): Observable<NewSessionInfo> {
@@ -2,6 +2,7 @@
import { currentSession, userInteractionService } from '../../services/session.service';
import { catchError, filter } from 'rxjs/operators';
import type { UserInteraction, NewSessionInfo } from '../../../static/iron-remote-desktop';
import { preConnectionBlob, displayControl, kdcProxyUrl } from '../../../static/iron-remote-desktop-rdp';
import { from, of } from 'rxjs';
import { toast } from '$lib/messages/message-store';
import { showLogin } from '$lib/login/login-store';
@@ -125,15 +126,16 @@
.withServerDomain(domain)
.withAuthToken(authtoken)
.withDesktopSize(desktopSize)
.withExtension('DisplayControl', true);
.withExtension(displayControl(true));
if (pcb !== '') {
configBuilder.withExtension('Pcb', pcb);
configBuilder.withExtension(preConnectionBlob(pcb));
}
if (kdc_proxy_url !== '') {
configBuilder.withExtension('KdcProxyUrl', kdc_proxy_url);
configBuilder.withExtension(kdcProxyUrl(kdc_proxy_url));
}
const config = configBuilder.build();
from(userInteraction.connect(config))
@@ -3,17 +3,18 @@
import { setCurrentSessionActive, userInteractionService } from '../../services/session.service';
import type { UserInteraction } from '../../../static/iron-remote-desktop';
import IronRdp from '../../../static/iron-remote-desktop-rdp';
import { preConnectionBlob, displayControl, kdcProxyUrl } from '../../../static/iron-remote-desktop-rdp';
let uiService: UserInteraction;
let userInteraction: UserInteraction;
let cursorOverrideActive = false;
let showUtilityBar = false;
userInteractionService.subscribe((uis) => {
if (uis != null) {
uiService = uis;
uiService.onSessionEvent((event) => {
userInteractionService.subscribe((val) => {
if (val != null) {
userInteraction = val;
userInteraction.onSessionEvent((event) => {
if (event.type === 0) {
uiService.setVisibility(true);
userInteraction.setVisibility(true);
} else if (event.type === 1) {
setCurrentSessionActive(false);
}
@@ -23,7 +24,7 @@
userInteractionService.subscribe((uis) => {
if (uis != null) {
uiService = uis;
userInteraction = uis;
//read query params named data
const urlParams = new URLSearchParams(window.location.search);
const data = urlParams.get('data');
@@ -36,7 +37,7 @@
const { hostname, gatewayAddress, domain, username, password, authtoken, kdc_proxy_url, pcb, desktopSize } =
parsedData;
const configBuilder = uiService
const configBuilder = userInteraction
.configBuilder()
.withUsername(username)
.withPassword(password)
@@ -45,19 +46,20 @@
.withServerDomain(domain)
.withAuthToken(authtoken)
.withDesktopSize(desktopSize)
.withExtension('DisplayControl', true);
.withExtension(displayControl(true));
if (pcb !== '') {
configBuilder.withExtension('Pcb', pcb);
configBuilder.withExtension(preConnectionBlob(pcb));
}
if (kdc_proxy_url !== '') {
configBuilder.withExtension('KdcProxyUrl', kdc_proxy_url);
configBuilder.withExtension(kdcProxyUrl(kdc_proxy_url));
}
const config = configBuilder.build();
uiService.connect(config).then(() => {
uiService.setVisibility(true);
userInteraction.connect(config).then(() => {
userInteraction.setVisibility(true);
window.onresize = onWindowResize;
});
}
@@ -66,7 +68,7 @@
function onWindowResize() {
const innerWidth = window.innerWidth;
const innerHeight = window.innerHeight;
uiService.resize(innerWidth, innerHeight);
userInteraction.resize(innerWidth, innerHeight);
}
function onUnicodeModeChange(e: MouseEvent) {
@@ -80,14 +82,14 @@
return;
}
uiService.setKeyboardUnicodeMode(element.checked);
userInteraction.setKeyboardUnicodeMode(element.checked);
}
function toggleCursorKind() {
if (cursorOverrideActive) {
uiService.setCursorStyleOverride(null);
userInteraction.setCursorStyleOverride(null);
} else {
uiService.setCursorStyleOverride('url("crosshair.png") 7 7, default');
userInteraction.setCursorStyleOverride('url("crosshair.png") 7 7, default');
}
cursorOverrideActive = !cursorOverrideActive;
@@ -125,8 +127,8 @@
<div class="tool-bar" class:hidden={!showUtilityBar}>
<div class="toolbar-container">
<button on:click={() => toggleFullScreen()}>Full Screen</button>
<button on:click={() => uiService.ctrlAltDel()}>Ctrl+Alt+Del</button>
<button on:click={() => uiService.metaKey()}>
<button on:click={() => userInteraction.ctrlAltDel()}>Ctrl+Alt+Del</button>
<button on:click={() => userInteraction.metaKey()}>
Meta
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 512 512">
<title>ionicons-v5_logos</title>
@@ -137,7 +139,7 @@
</svg>
</button>
<button on:click={() => toggleCursorKind()}>Toggle cursor kind</button>
<button on:click={() => uiService.shutdown()}>Terminate Session</button>
<button on:click={() => userInteraction.shutdown()}>Terminate Session</button>
<label style="color: white;">
<input on:click={(e) => onUnicodeModeChange(e)} type="checkbox" />
Unicode keyboard mode