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

This commit is contained in:
Alex Yusiuk 2025-04-11 15:28:27 +03:00 committed by GitHub
parent 184cfd24ae
commit 0ff1ed8de5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 4950 additions and 330 deletions

View file

@ -168,12 +168,18 @@ WebAssembly high-level bindings targeting web browsers.
This crate is an **API Boundary** (WASM module).
#### [`web-client/iron-remote-gui`](./web-client/iron-remote-gui)
#### [`web-client/iron-remote-desktop`](./web-client/iron-remote-desktop)
Core frontend UI used by `iron-svelte-client` as a Web Component.
This crate is an **API Boundary**.
#### [`web-client/iron-remote-desktop-rdp`](./web-client/iron-remote-desktop-rdp)
Implementation of the TypeScript interfaces exposed by WebAssembly bindings from `ironrdp-web` and used by `iron-svelte-client`.
This crate is an **API Boundary**.
#### [`web-client/iron-svelte-client`](./web-client/iron-svelte-client)
Web-based frontend using `Svelte` and `Material` frameworks.

13
Cargo.lock generated
View file

@ -2809,6 +2809,8 @@ dependencies = [
"resize",
"rgb",
"semver",
"serde",
"serde-wasm-bindgen",
"smallvec",
"softbuffer",
"tap",
@ -4668,6 +4670,17 @@ 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"

View file

@ -45,6 +45,8 @@ 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"
tracing-web = "0.1"
# Rendering

View file

@ -137,7 +137,7 @@ impl WasmClipboard {
pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self {
Self {
local_clipboard: None,
remote_clipboard: ClipboardTransaction::new(),
remote_clipboard: ClipboardTransaction::construct(),
proxy: message_proxy,
js_callbacks,
@ -505,7 +505,7 @@ impl WasmClipboard {
} else {
// If no initial clipboard callback was set, send empty format list instead
return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged(
ClipboardTransaction::new(),
ClipboardTransaction::construct(),
));
}
}

View file

@ -19,7 +19,7 @@ impl ClipboardTransaction {
#[wasm_bindgen]
impl ClipboardTransaction {
pub fn new() -> Self {
pub fn construct() -> Self {
Self { contents: Vec::new() }
}

View file

@ -3,7 +3,7 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum IronRdpErrorKind {
pub enum RemoteDesktopErrorKind {
/// Catch-all error kind
General,
/// Incorrect password used
@ -19,30 +19,30 @@ pub enum IronRdpErrorKind {
}
#[wasm_bindgen]
pub struct IronRdpError {
kind: IronRdpErrorKind,
pub struct RemoteDesktopError {
kind: RemoteDesktopErrorKind,
source: anyhow::Error,
}
impl IronRdpError {
pub fn with_kind(mut self, kind: IronRdpErrorKind) -> Self {
impl RemoteDesktopError {
pub fn with_kind(mut self, kind: RemoteDesktopErrorKind) -> Self {
self.kind = kind;
self
}
}
#[wasm_bindgen]
impl IronRdpError {
impl RemoteDesktopError {
pub fn backtrace(&self) -> String {
format!("{:?}", self.source)
}
pub fn kind(&self) -> IronRdpErrorKind {
pub fn kind(&self) -> RemoteDesktopErrorKind {
self.kind
}
}
impl From<connector::ConnectorError> for IronRdpError {
impl From<connector::ConnectorError> for RemoteDesktopError {
fn from(e: connector::ConnectorError) -> Self {
use sspi::credssp::NStatusCode;
@ -50,13 +50,13 @@ impl From<connector::ConnectorError> for IronRdpError {
ConnectorErrorKind::Credssp(sspi::Error {
nstatus: Some(NStatusCode::WRONG_PASSWORD),
..
}) => IronRdpErrorKind::WrongPassword,
}) => RemoteDesktopErrorKind::WrongPassword,
ConnectorErrorKind::Credssp(sspi::Error {
nstatus: Some(NStatusCode::LOGON_FAILURE),
..
}) => IronRdpErrorKind::LogonFailure,
ConnectorErrorKind::AccessDenied => IronRdpErrorKind::AccessDenied,
_ => IronRdpErrorKind::General,
}) => RemoteDesktopErrorKind::LogonFailure,
ConnectorErrorKind::AccessDenied => RemoteDesktopErrorKind::AccessDenied,
_ => RemoteDesktopErrorKind::General,
};
Self {
@ -66,19 +66,19 @@ impl From<connector::ConnectorError> for IronRdpError {
}
}
impl From<ironrdp::session::SessionError> for IronRdpError {
impl From<ironrdp::session::SessionError> for RemoteDesktopError {
fn from(e: ironrdp::session::SessionError) -> Self {
Self {
kind: IronRdpErrorKind::General,
kind: RemoteDesktopErrorKind::General,
source: anyhow::Error::new(e),
}
}
}
impl From<anyhow::Error> for IronRdpError {
impl From<anyhow::Error> for RemoteDesktopError {
fn from(e: anyhow::Error) -> Self {
Self {
kind: IronRdpErrorKind::General,
kind: RemoteDesktopErrorKind::General,
source: e,
}
}

View file

@ -8,7 +8,7 @@ pub struct DeviceEvent(pub(crate) Operation);
#[wasm_bindgen]
impl DeviceEvent {
pub fn new_mouse_button_pressed(button: u8) -> Self {
pub fn mouse_button_pressed(button: u8) -> Self {
match MouseButton::from_web_button(button) {
Some(button) => Self(Operation::MouseButtonPressed(button)),
None => {
@ -18,7 +18,7 @@ impl DeviceEvent {
}
}
pub fn new_mouse_button_released(button: u8) -> Self {
pub fn mouse_button_released(button: u8) -> Self {
match MouseButton::from_web_button(button) {
Some(button) => Self(Operation::MouseButtonReleased(button)),
None => {
@ -28,30 +28,30 @@ impl DeviceEvent {
}
}
pub fn new_mouse_move(x: u16, y: u16) -> Self {
pub fn mouse_move(x: u16, y: u16) -> Self {
Self(Operation::MouseMove(MousePosition { x, y }))
}
pub fn new_wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
pub fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
Self(Operation::WheelRotations(WheelRotations {
is_vertical: vertical,
rotation_units,
}))
}
pub fn new_key_pressed(scancode: u16) -> Self {
pub fn key_pressed(scancode: u16) -> Self {
Self(Operation::KeyPressed(Scancode::from_u16(scancode)))
}
pub fn new_key_released(scancode: u16) -> Self {
pub fn key_released(scancode: u16) -> Self {
Self(Operation::KeyReleased(Scancode::from_u16(scancode)))
}
pub fn new_unicode_pressed(unicode: char) -> Self {
pub fn unicode_pressed(unicode: char) -> Self {
Self(Operation::UnicodeKeyPressed(unicode))
}
pub fn new_unicode_released(unicode: char) -> Self {
pub fn unicode_released(unicode: char) -> Self {
Self(Operation::UnicodeKeyReleased(unicode))
}
}
@ -61,7 +61,7 @@ pub struct InputTransaction(pub(crate) SmallVec<[Operation; 3]>);
#[wasm_bindgen]
impl InputTransaction {
pub fn new() -> Self {
pub fn construct() -> Self {
Self(SmallVec::new())
}

View file

@ -23,7 +23,7 @@ mod session;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn ironrdp_init(log_level: &str) {
pub fn iron_init(log_level: &str) {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
@ -69,7 +69,7 @@ pub struct DesktopSize {
#[wasm_bindgen]
impl DesktopSize {
pub fn new(width: u16, height: u16) -> Self {
pub fn construct(width: u16, height: u16) -> Self {
DesktopSize { width, height }
}
}

View file

@ -29,6 +29,7 @@ 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::prelude::*;
use wasm_bindgen_futures::spawn_local;
@ -36,7 +37,7 @@ use web_sys::HtmlCanvasElement;
use crate::canvas::Canvas;
use crate::clipboard::{ClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::error::{IronRdpError, IronRdpErrorKind};
use crate::error::{RemoteDesktopError, RemoteDesktopErrorKind};
use crate::image::extract_partial_image;
use crate::input::InputTransaction;
use crate::network_client::WasmNetworkClient;
@ -102,7 +103,7 @@ impl Default for SessionBuilderInner {
#[wasm_bindgen]
impl SessionBuilder {
pub fn new() -> SessionBuilder {
pub fn construct() -> SessionBuilder {
Self(Rc::new(RefCell::new(SessionBuilderInner::default())))
}
@ -146,18 +147,6 @@ impl SessionBuilder {
self.clone()
}
/// Optional
pub fn pcb(&self, pcb: String) -> SessionBuilder {
self.0.borrow_mut().pcb = Some(pcb);
self.clone()
}
/// Optional
pub fn kdc_proxy_url(&self, kdc_proxy_url: Option<String>) -> SessionBuilder {
self.0.borrow_mut().kdc_proxy_url = kdc_proxy_url;
self.clone()
}
/// Optional
pub fn desktop_size(&self, desktop_size: DesktopSize) -> SessionBuilder {
self.0.borrow_mut().desktop_size = desktop_size;
@ -216,13 +205,22 @@ impl SessionBuilder {
self.clone()
}
/// Optional
pub fn use_display_control(&self) -> SessionBuilder {
self.0.borrow_mut().use_display_control = true;
pub fn extension(&self, value: JsValue) -> SessionBuilder {
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"),
}
self.clone()
}
pub async fn connect(&self) -> Result<Session, IronRdpError> {
pub async fn connect(&self) -> Result<Session, RemoteDesktopError> {
let (
username,
destination,
@ -297,11 +295,11 @@ impl SessionBuilder {
loop {
match ws.state() {
websocket::State::Closing | websocket::State::Closed => {
return Err(IronRdpError::from(anyhow::anyhow!(
"Failed to connect to {proxy_address} (WebSocket is `{:?}`)",
return Err(RemoteDesktopError::from(anyhow::anyhow!(
"failed to connect to {proxy_address} (WebSocket is `{:?}`)",
ws.state()
))
.with_kind(IronRdpErrorKind::ProxyConnect));
.with_kind(RemoteDesktopErrorKind::ProxyConnect));
}
websocket::State::Connecting => {
trace!("WebSocket is connecting to proxy at {proxy_address}...");
@ -354,6 +352,13 @@ impl SessionBuilder {
}
}
#[derive(Debug, Serialize, Deserialize)]
enum Extension {
KdcProxyUrl(String),
Pcb(String),
DisplayControl(bool),
}
pub(crate) type FastPathInputEvents = smallvec::SmallVec<[FastPathInputEvent; 2]>;
#[derive(Debug)]
@ -412,7 +417,7 @@ pub struct Session {
#[wasm_bindgen]
impl Session {
pub async fn run(&self) -> Result<SessionTerminationInfo, IronRdpError> {
pub async fn run(&self) -> Result<SessionTerminationInfo, RemoteDesktopError> {
let rdp_reader = self
.rdp_reader
.borrow_mut()
@ -707,17 +712,17 @@ impl Session {
}
}
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronRdpError> {
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), RemoteDesktopError> {
let inputs = self.input_database.borrow_mut().apply(transaction);
self.h_send_inputs(inputs)
}
pub fn release_all_inputs(&self) -> Result<(), IronRdpError> {
pub fn release_all_inputs(&self) -> Result<(), RemoteDesktopError> {
let inputs = self.input_database.borrow_mut().release_all();
self.h_send_inputs(inputs)
}
fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), IronRdpError> {
fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), RemoteDesktopError> {
if !inputs.is_empty() {
trace!("Inputs: {inputs:?}");
@ -735,7 +740,7 @@ impl Session {
num_lock: bool,
caps_lock: bool,
kana_lock: bool,
) -> Result<(), IronRdpError> {
) -> Result<(), RemoteDesktopError> {
use ironrdp::pdu::input::fast_path::FastPathInput;
let event = ironrdp::input::synchronize_event(scroll_lock, num_lock, caps_lock, kana_lock);
@ -750,7 +755,7 @@ impl Session {
Ok(())
}
pub fn shutdown(&self) -> Result<(), IronRdpError> {
pub fn shutdown(&self) -> Result<(), RemoteDesktopError> {
self.input_events_tx
.unbounded_send(RdpInputEvent::TerminateSession)
.context("failed to send terminate session event to writer task")?;
@ -758,7 +763,7 @@ impl Session {
Ok(())
}
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronRdpError> {
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), RemoteDesktopError> {
self.input_events_tx
.unbounded_send(RdpInputEvent::ClipboardBackend(
WasmClipboardBackendMessage::LocalClipboardChanged(content),
@ -768,7 +773,7 @@ impl Session {
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronRdpError> {
fn set_cursor_style(&self, style: CursorStyle) -> Result<(), RemoteDesktopError> {
let (kind, data, hotspot_x, hotspot_y) = match style {
CursorStyle::Default => ("default", None, None, None),
CursorStyle::Hidden => ("hidden", None, None, None),
@ -818,6 +823,10 @@ impl Session {
// plain scancode events are allowed to function correctly).
false
}
pub fn extension_call(_value: JsValue) -> Result<JsValue, RemoteDesktopError> {
Ok(JsValue::null())
}
}
fn build_config(
@ -913,7 +922,7 @@ async fn connect(
clipboard_backend,
use_display_control,
}: ConnectParams,
) -> Result<(connector::ConnectionResult, WebSocket), IronRdpError> {
) -> Result<(connector::ConnectionResult, WebSocket), RemoteDesktopError> {
let mut framed = ironrdp_futures::LocalFuturesFramed::new(ws);
let mut connector = ClientConnector::new(config);
@ -960,7 +969,7 @@ async fn connect_rdcleanpath<S>(
destination: String,
proxy_auth_token: String,
pcb: Option<String>,
) -> Result<(ironrdp_futures::Upgraded, Vec<u8>), IronRdpError>
) -> Result<(ironrdp_futures::Upgraded, Vec<u8>), RemoteDesktopError>
where
S: ironrdp_futures::FramedRead + FramedWrite,
{
@ -1039,10 +1048,10 @@ where
server_addr,
} => (x224_connection_response, server_cert_chain, server_addr),
ironrdp_rdcleanpath::RDCleanPath::Err(error) => {
return Err(
IronRdpError::from(anyhow::anyhow!("received an RDCleanPath error: {error}"))
.with_kind(IronRdpErrorKind::RDCleanPath),
);
return Err(RemoteDesktopError::from(
anyhow::Error::new(error).context("received an RDCleanPath error"),
)
.with_kind(RemoteDesktopErrorKind::RDCleanPath));
}
};

View file

@ -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](./iron-remote-desktop) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.
Note that the demonstration client is not intended to be used in production as-is.
Devolutions is shipping well-integrated, production-ready IronRDP web clients as part of:

View file

@ -0,0 +1,15 @@
node_modules/
.DS_Store
.env
.env.*
!.env.example
/package
/build
/static/bearcss
/static/material-icons
/dist
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -0,0 +1,19 @@
# Prettier:
# - https://prettier.io/docs/en/options
---
useTabs: false
tabWidth: 4
singleQuote: true
semi: true
trailingComma: all
printWidth: 120
overrides:
- files:
- '*.yml'
- '*.yaml'
- '*.json'
- '*.html'
- '*.md'
options:
tabWidth: 2

View file

@ -0,0 +1,23 @@
# Iron Remote Desktop RDP
This is implementation of `RemoteDesktopModule` interface from [iron-remote-desktop](../iron-remote-desktop) for RDP connection.
## Development
Make your modification in the source code then use [iron-svelte-client](../iron-svelte-client) to test.
## Build
Run `npm run build`
## Usage
As member of the Devolutions organization, you can import the Web Component from JFrog Artifactory by running the following npm command:
```shell
$ npm install @devolutions/iron-remote-desktop-rdp
```
Otherwise, you can run `npm install` targeting the `dist/` folder directly.
Import the `iron-remote-desktop-rdp.umd.cjs` from `node_modules/` folder.

View file

@ -0,0 +1,80 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: [
'**/*.cjs',
'**/.DS_Store',
'**/node_modules',
'build',
'package',
'**/.env',
'**/.env.*',
'!**/.env.example',
'**/pnpm-lock.yaml',
'**/package-lock.json',
'**/yarn.lock',
],
},
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'),
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tsParser,
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
project: './tsconfig.json',
},
},
rules: {
strict: 2,
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
},
],
'@typescript-eslint/strict-boolean-expressions': [
2,
{
allowString: false,
allowNumber: false,
},
],
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
},
];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
{
"name": "@devolutions/iron-remote-desktop-rdp",
"author": "Nicolas Girot",
"email": "ngirot@devolutions.net",
"contributors": [
"Benoit Cortier",
"Irving Ou",
"Vladislav Nikonov",
"Zacharia Ellaham",
"Alexandr Yusuk"
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "npm run pre-build && vite",
"build": "npm run pre-build && vite build",
"build-alone": "vite build",
"pre-build": "node ./pre-build.js",
"preview": "vite preview",
"check": "tsc --noEmit",
"check:dist": "tsc ./dist/index.d.ts --noEmit",
"check:watch": "tsc --watch --noEmit",
"lint": "npm run lint:prettier && npm run lint:eslint",
"lint:prettier": "prettier --check .",
"lint:eslint": "eslint src/**",
"format": "prettier --write ."
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"globals": "^16.0.0",
"prettier": "^3.1.0",
"tslib": "^2.4.1",
"typescript": "~5.7.2",
"vite": "^6.2.0",
"vite-plugin-dts": "^4.5.0",
"vite-plugin-top-level-await": "^1.2.2",
"vite-plugin-wasm": "^3.1.0"
},
"dependencies": {
"rxjs": "^6.6.7",
"ua-parser-js": "^1.0.33"
}
}

View file

@ -0,0 +1,34 @@
import { spawn } from 'child_process';
const run = async (command, cwd) => {
try {
const buildCommand = spawn(command, {
stdio: 'pipe',
shell: true,
cwd: cwd,
});
buildCommand.stdout.on('data', (data) => {
console.log(`${data}`);
});
buildCommand.stderr.on('data', (data) => {
console.error(`${data}`);
});
const exitCode = await new Promise((resolve, reject) => {
buildCommand.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Process exited with non-zero code: ${code}`));
}
resolve(code);
});
});
console.log(`Child process exited with code: ${exitCode}`);
} catch (err) {
console.error(`Process run failed: ${err}`);
}
};
await run('cargo xtask web build', '../../');

View file

@ -0,0 +1,22 @@
{
"name": "@devolutions/iron-remote-desktop-rdp",
"author": "Nicolas Girot",
"email": "ngirot@devolutions.net",
"contributors": [
"Benoit Cortier",
"Irving Ou",
"Vladislav Nikonov",
"Zacharia Ellaham"
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client.",
"version": "0.13.1",
"main": "iron-remote-desktop-rdp.js",
"types": "index.d.ts",
"files": [
"iron-remote-desktop-rdp.js",
"index.d.ts"
],
"dependencies": {
"rxjs": "^6.6.7"
}
}

View file

@ -0,0 +1,26 @@
import init, {
iron_init,
DesktopSize,
DeviceEvent,
InputTransaction,
RemoteDesktopError,
Session,
SessionBuilder,
SessionTerminationInfo,
ClipboardTransaction,
ClipboardContent,
} from '../../../crates/ironrdp-web/pkg/ironrdp_web';
export default {
init,
iron_init,
DesktopSize,
DeviceEvent,
InputTransaction,
RemoteDesktopError,
SessionBuilder,
ClipboardTransaction,
ClipboardContent,
Session,
SessionTerminationInfo,
};

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"esModuleInterop": true,
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": false,
"checkJs": false,
"isolatedModules": true,
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts", "src/**/*.js"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View file

@ -0,0 +1,25 @@
import { defineConfig } from 'vite';
import topLevelAwait from 'vite-plugin-top-level-await';
import dtsPlugin from 'vite-plugin-dts';
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: './src/main.ts',
name: 'IronRemoteDesktopRdp',
formats: ['es'],
},
},
server: {
fs: {
strict: false,
},
},
plugins: [
topLevelAwait(),
dtsPlugin({
rollupTypes: true,
}),
],
});

View file

@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -1,16 +1,16 @@
node_modules/
.DS_Store
.env
.env.*
!.env.example
/.svelte-kit
/package
/build
/static/bearcss
/static/material-icons
/dist
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
node_modules/
.DS_Store
.env
.env.*
!.env.example
/.svelte-kit
/package
/build
/static/bearcss
/static/material-icons
/dist
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View file

@ -1,6 +1,7 @@
# Iron Remote GUI
# Iron Remote Desktop
This is the core of the web client written on top of Svelte and built as a reusable Web Component.
Also, it contains the TypeScript interfaces exposed by WebAssembly bindings from `ironrdp-web` and used by `iron-svelte-client`.
## Development
@ -15,17 +16,17 @@ Run `npm run build`
As member of the Devolutions organization, you can import the Web Component from JFrog Artifactory by running the following npm command:
```shell
$ npm install @devolutions/iron-remote-gui
$ npm install @devolutions/iron-remote-desktop
```
Otherwise, you can run `npm install` targeting the `dist/` folder directly.
Import the `iron-remote-gui.umd.cjs` from `node_modules/` folder.
Import the `iron-remote-desktop.umd.cjs` from `node_modules/` folder.
Then use the HTML tag `<iron-remote-gui/>` in your page.
Then use the HTML tag `<iron-remote-desktop/>` in your page.
In your code add a listener for the `ready` event on the `iron-remote-gui` HTML element.
Get `evt.detail.irgUserInteraction` from the `Promise`, a property whose type is `UserInteractionService`.
In your code add a listener for the `ready` event on the `iron-remote-desktop` HTML element.
Get `evt.detail.irgUserInteraction` from the `Promise`, a property whose type is `UserInteraction`.
Call the `connect` method on this object.
## Limitations
@ -35,22 +36,23 @@ You need to recreate them on your application for now (it will be improved in fu
Also, even if the connection to RDP work there is still a lot of improvement to do.
As of now, you can expect, mouse movement and click (4 buttons) - no scroll, Keyboard for at least the standard.
Windows and CTRL+ALT+DEL can be called by method on `UserInteractionService`.
Windows and CTRL+ALT+DEL can be called by method on `UserInteraction`.
Lock keys (like caps lock), have a partial support.
Other advanced functionalities (sharing / copy past...) are not implemented yet.
## Component parameters
You can add some parameters for default initialization on the component `<iron-remote-gui />`.
You can add some parameters for default initialization on the component `<iron-remote-desktop />`.
> Note that due to a limitation of the framework all parameters need to be lower-cased.
- `scale`: The scaling behavior of the distant screen. Can be `fit`, `real` or `full`. Default is `real`;
- `verbose`: Show logs from `iron-remote-gui`. `true` or `false`. Default is `false`.
- `verbose`: Show logs from `iron-remote-desktop`. `true` or `false`. Default is `false`.
- `debugwasm`: Show debug info from web assembly. Can be `"OFF"`, `"ERROR"`, `"WARN"`, `"INFO"`, `"DEBUG"`, `"TRACE"`. Default is `"OFF"`.
- `flexcentre`: Helper to force `iron-remote-gui` a flex and centering the content automatically. Otherwise, you need to manage manually. Default is `true`.
- `flexcentre`: Helper to force `iron-remote-desktop` a flex and centering the content automatically. Otherwise, you need to manage manually. Default is `true`.
- `module`: An implementation of the [RemoteDesktopModule](./src/interfaces/RemoteDesktopModule.ts)
## `UserInteractionService` methods
## `UserInteraction` methods
```ts
connect(
@ -63,6 +65,7 @@ connect(
desktopSize?: DesktopSize,
preConnectionBlob?: string,
kdc_proxy_url?: string,
use_display_control: boolean,
): Observable<NewSessionInfo>;
```
@ -70,12 +73,20 @@ connect(
> `destination` refers to the Devolutions Gateway hostname and port.
> `authtoken` is the authentication token to send to the Devolutions Gateway.
> `proxyAddress` is the address of the Devolutions Gateway proxy
> `serverDomain` is the Windows domain name (if the target computer has one)
> `authtoken` is the authentication token to send to the Devolutions Gateway.
> `desktopSize` is the initial size of the desktop
> `preConnectionBlob` is the pre connection blob data
> `kdc_proxy_url` is the URL to a KDC Proxy, as specified in [MS-KKDCP documentation](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kkdcp/5bcebb8d-b747-4ee5-9453-428aec1c5c38)
> `use_display_control` is the value that defined if the Display Control Virtual Channel will be used.
> `ctrlAltDel()`
>
> Sends the ctrl+alt+del key to server.
@ -84,11 +95,31 @@ connect(
>
> Sends the meta key event to remote host (i.e.: Windows key).
> `setVisibility(value: bool)`
> `setVisibility(value: boolean)`
>
> Shows or hides rendering canvas.
> `setScale(scale: ScreenScale)`
>
> Sets the scale behavior of the canvas.
> See the [ScreenScale](./src/services/user-interaction-service.ts) enum for possible values.
> See the [ScreenScale](./src/enums/ScreenScale.ts) enum for possible values.
> `shutdown()`
>
> Shutdowns the active session.
> `setKeyboardUnicodeMode(use_unicode: boolean)`
>
> Sets the keyboard Unicode mode.
> `setCursorStyleOverride(style?: string)`
>
> Overrides the default cursor style. If `style` is `null`, the default cursor style will be used.
> `resize(width: number, height: number, scale?: number)`
>
> Resizes the screen.
> `setEnableClipboard(enable: boolean)`
>
> Enables or disable the clipboard based on the `enable` value.

View file

@ -7,10 +7,10 @@
</head>
<body>
<script type="module" src="/src/main.ts"></script>
<iron-remote-gui isvisible="false" desktopwidth="600" desktopheight="400" />
<iron-remote-desktop isvisible="false" desktopwidth="600" desktopheight="400" />
<script>
var el = document.querySelector('iron-remote-gui');
var el = document.querySelector('iron-remote-desktop');
el.addEventListener('ready', (e) => {
console.log('WebComponent Loaded');
e.detail.irgUserInteraction.setVisibility(true);

View file

@ -1,11 +1,11 @@
{
"name": "@devolutions/iron-remote-gui",
"name": "@devolutions/iron-remote-desktop",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@devolutions/iron-remote-gui",
"name": "@devolutions/iron-remote-desktop",
"version": "0.0.0",
"dependencies": {
"rxjs": "^6.6.7",

View file

@ -1,5 +1,5 @@
{
"name": "@devolutions/iron-remote-gui",
"name": "@devolutions/iron-remote-desktop",
"author": "Nicolas Girot",
"email": "ngirot@devolutions.net",
"contributors": [
@ -13,10 +13,9 @@
"type": "module",
"private": true,
"scripts": {
"dev": "npm run pre-build && vite",
"build": "npm run pre-build && vite build",
"dev": "npm run vite",
"build": "npm run vite build",
"build-alone": "vite build",
"pre-build": "node ./pre-build.js",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:dist": "tsc ./dist/index.d.ts --noEmit",

View file

@ -1,5 +1,5 @@
{
"name": "@devolutions/iron-remote-gui",
"name": "@devolutions/iron-remote-desktop",
"author": "Nicolas Girot",
"email": "ngirot@devolutions.net",
"contributors": [
@ -10,10 +10,10 @@
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client.",
"version": "0.13.1",
"main": "iron-remote-gui.js",
"main": "iron-remote-desktop.js",
"types": "index.d.ts",
"files": [
"iron-remote-gui.js",
"iron-remote-desktop.js",
"index.d.ts"
],
"dependencies": {

View file

@ -6,11 +6,11 @@
<title>Test</title>
</head>
<body>
<script type="module" src="./iron-remote-gui.js"></script>
<iron-remote-gui isvisible="false" desktopwidth="600" desktopheight="400" />
<script type="module" src="./iron-remote-desktop.js"></script>
<iron-remote-desktop isvisible="false" desktopwidth="600" desktopheight="400" />
<script>
var el = document.querySelector('iron-remote-gui');
var el = document.querySelector('iron-remote-desktop');
el.addEventListener('ready', (e) => {
e.detail.irgUserInteraction.setVisibility(true);
});

View file

@ -0,0 +1,6 @@
export interface ClipboardContent {
new_text(mime_type: string, text: string): ClipboardContent;
new_binary(mime_type: string, binary: Uint8Array): ClipboardContent;
mime_type(): string;
value(): string;
}

View file

@ -0,0 +1,8 @@
import type { ClipboardContent } from './ClipboardContent';
export interface ClipboardTransaction {
construct(): ClipboardTransaction;
add_content(content: ClipboardContent): void;
is_empty(): boolean;
content(): Array<ClipboardContent>;
}

View file

@ -1,4 +1,6 @@
export interface DesktopSize {
width: number;
height: number;
construct(width: number, height: number): DesktopSize;
}

View file

@ -0,0 +1,10 @@
export interface DeviceEvent {
mouse_button_pressed(button: number): DeviceEvent;
mouse_button_released(button: number): DeviceEvent;
mouse_move(x: number, y: number): DeviceEvent;
wheel_rotations(vertical: boolean, rotation_units: number): DeviceEvent;
key_pressed(scancode: number): DeviceEvent;
key_released(scancode: number): DeviceEvent;
unicode_pressed(unicode: string): DeviceEvent;
unicode_released(unicode: string): DeviceEvent;
}

View file

@ -0,0 +1,6 @@
import type { DeviceEvent } from './DeviceEvent';
export interface InputTransaction {
construct(): InputTransaction;
add_event(event: DeviceEvent): void;
}

View file

@ -0,0 +1,23 @@
import type { DesktopSize } from './DesktopSize';
import type { DeviceEvent } from './DeviceEvent';
import type { InputTransaction } from './InputTransaction';
import type { RemoteDesktopError } from './session-event';
import type { Session } from './Session';
import type { SessionBuilder } from './SessionBuilder';
import type { SessionTerminationInfo } from './SessionTerminationInfo';
import type { ClipboardTransaction } from './ClipboardTransaction';
import type { ClipboardContent } from './ClipboardContent';
export interface RemoteDesktopModule {
init: () => Promise<unknown>;
iron_init: (logLevel: string) => void;
DesktopSize: DesktopSize;
DeviceEvent: DeviceEvent;
InputTransaction: InputTransaction;
RemoteDesktopError: RemoteDesktopError;
Session: Session;
SessionBuilder: SessionBuilder;
SessionTerminationInfo: SessionTerminationInfo;
ClipboardTransaction: ClipboardTransaction;
ClipboardContent: ClipboardContent;
}

View file

@ -1,10 +1,8 @@
export interface ServerRect {
free(): void;
clone_buffer(): Uint8Array;
bottom: number;
left: number;
right: number;
top: number;
clone_buffer(): Uint8Array;
}

View file

@ -0,0 +1,23 @@
import type { InputTransaction } from './InputTransaction';
import type { DesktopSize } from './DesktopSize';
import type { SessionTerminationInfo } from './SessionTerminationInfo';
import type { ClipboardTransaction } from './ClipboardTransaction';
export interface Session {
run(): Promise<SessionTerminationInfo>;
desktop_size(): DesktopSize;
apply_inputs(transaction: InputTransaction): void;
release_all_inputs(): void;
synchronize_lock_keys(scroll_lock: boolean, num_lock: boolean, caps_lock: boolean, kana_lock: boolean): void;
extension_call(value: unknown): unknown;
shutdown(): void;
on_clipboard_paste(content: ClipboardTransaction): Promise<void>;
resize(
width: number,
height: number,
scale_factor?: number | null,
physical_width?: number | null,
physical_height?: number | null,
): void;
supports_unicode_keyboard_shortcuts(): boolean;
}

View file

@ -0,0 +1,90 @@
import type { Session } from './Session';
import type { DesktopSize } from './DesktopSize';
import type { ClipboardTransaction } from './ClipboardTransaction';
export interface SessionBuilder {
construct(): SessionBuilder;
/**
* Required
*/
username(username: string): SessionBuilder;
/**
* Required
*/
destination(destination: string): SessionBuilder;
/**
* Optional
*/
server_domain(server_domain: string): SessionBuilder;
/**
* Required
*/
password(password: string): SessionBuilder;
/**
* Required
*/
proxy_address(address: string): SessionBuilder;
/**
* Required
*/
auth_token(token: string): SessionBuilder;
/**
* Optional
*/
desktop_size(desktop_size: DesktopSize): SessionBuilder;
/**
* Optional
*/
render_canvas(canvas: HTMLCanvasElement): SessionBuilder;
/**
* Required.
*
* # Cursor kinds:
* - `default` (default system cursor); other arguments are `UNDEFINED`
* - `none` (hide cursor); other arguments are `UNDEFINED`
* - `url` (custom cursor data URL); `cursor_data` contains the data URL with Base64-encoded
* cursor bitmap; `hotspot_x` and `hotspot_y` are set to the cursor hotspot coordinates.
*/
set_cursor_style_callback(callback: SetCursorStyleCallback): SessionBuilder;
/**
* Required.
*/
set_cursor_style_callback_context(context: unknown): SessionBuilder;
/**
* Optional
*/
remote_clipboard_changed_callback(callback: RemoteClipboardChangedCallback): SessionBuilder;
/**
* Optional
*/
remote_received_format_list_callback(callback: RemoteReceiveForwardListCallback): SessionBuilder;
/**
* Optional
*/
force_clipboard_update_callback(callback: ForceClipboardUpdateCallback): SessionBuilder;
extension(value: unknown): SessionBuilder;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
extension_call(_ident: string, _call: Function): SessionBuilder;
connect(): Promise<Session>;
}
interface SetCursorStyleCallback {
(
cursor_kind: string,
cursor_data: string | undefined,
hotspot_x: number | undefined,
hotspot_y: number | undefined,
): void;
}
interface RemoteClipboardChangedCallback {
(transaction: ClipboardTransaction): void;
}
interface RemoteReceiveForwardListCallback {
(): void;
}
interface ForceClipboardUpdateCallback {
(): void;
}

View file

@ -0,0 +1,3 @@
export interface SessionTerminationInfo {
reason(): string;
}

View file

@ -1,6 +1,6 @@
import type { SessionEventType } from '../enums/SessionEventType';
export enum UserIronRdpErrorKind {
export enum RemoteDesktopErrorKind {
General = 0,
WrongPassword = 1,
LogonFailure = 2,
@ -8,12 +8,12 @@ export enum UserIronRdpErrorKind {
RDCleanPath = 4,
ProxyConnect = 5,
}
export interface UserIronRdpError {
export interface RemoteDesktopError {
backtrace: () => string;
kind: () => UserIronRdpErrorKind;
kind: () => RemoteDesktopErrorKind;
}
export interface SessionEvent {
type: SessionEventType;
data: UserIronRdpError | string;
data: RemoteDesktopError | string;
}

View file

@ -1,6 +1,6 @@
<svelte:options
customElement={{
tag: 'iron-remote-gui',
tag: 'iron-remote-desktop',
shadow: 'none',
extend: (elementConstructor) => {
return class extends elementConstructor {
@ -16,23 +16,26 @@
<script lang="ts">
import { onMount } from 'svelte';
import { loggingService } from './services/logging.service';
import { WasmBridgeService } from './services/wasm-bridge.service';
import { RemoteDesktopService } from './services/remote-desktop.service';
import { LogType } from './enums/LogType';
import type { ResizeEvent } from './interfaces/ResizeEvent';
import { PublicAPI } from './services/PublicAPI';
import { ScreenScale } from './enums/ScreenScale';
import { ClipboardContent, ClipboardTransaction } from '../../../crates/ironrdp-web/pkg/ironrdp_web';
import type { ClipboardTransaction } from './interfaces/ClipboardTransaction';
import type { RemoteDesktopModule } from './interfaces/RemoteDesktopModule';
let {
scale,
verbose,
debugwasm,
flexcenter,
module,
}: {
scale: string;
verbose: 'true' | 'false';
debugwasm: 'OFF' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';
flexcenter: string;
module: RemoteDesktopModule;
} = $props();
let isVisible = $state(false);
@ -51,9 +54,8 @@
let viewerStyle = $state('');
let wrapperStyle = $state('');
let wasmService = new WasmBridgeService();
let publicAPI = new PublicAPI(wasmService);
let remoteDesktopService = new RemoteDesktopService(module);
let publicAPI = new PublicAPI(remoteDesktopService);
// Firefox's clipboard API is very limited, and doesn't support reading from the clipboard
// without changing browser settings via `about:config`.
@ -102,12 +104,12 @@
}
if (isFirefox) {
wasmService.setOnRemoteClipboardChanged(ffOnRemoteClipboardChanged);
wasmService.setOnRemoteReceivedFormatList(ffOnRemoteReceivedFormatList);
wasmService.setOnForceClipboardUpdate(onForceClipboardUpdate);
remoteDesktopService.setOnRemoteClipboardChanged(ffOnRemoteClipboardChanged);
remoteDesktopService.setOnRemoteReceivedFormatList(ffOnRemoteReceivedFormatList);
remoteDesktopService.setOnForceClipboardUpdate(onForceClipboardUpdate);
} else if (isClipboardApiSupported) {
wasmService.setOnRemoteClipboardChanged(onRemoteClipboardChanged);
wasmService.setOnForceClipboardUpdate(onForceClipboardUpdate);
remoteDesktopService.setOnRemoteClipboardChanged(onRemoteClipboardChanged);
remoteDesktopService.setOnForceClipboardUpdate(onForceClipboardUpdate);
// Start the clipboard monitoring loop
setTimeout(onMonitorClipboard, CLIPBOARD_MONITORING_INTERVAL);
@ -135,12 +137,9 @@
let result = {} as Record<string, Blob>;
for (const item of transaction.content()) {
if (!(item instanceof ClipboardContent)) {
continue;
}
let mime = item.mime_type();
let value = new Blob([item.value()], { type: mime });
result[mime] = value;
}
@ -151,9 +150,9 @@
function onForceClipboardUpdate() {
try {
if (lastClientClipboardTransaction) {
wasmService.onClipboardChanged(lastClientClipboardTransaction);
remoteDesktopService.onClipboardChanged(lastClientClipboardTransaction);
} else {
wasmService.onClipboardChanged(ClipboardTransaction.new());
remoteDesktopService.onClipboardChangedEmpty();
}
} catch (err) {
console.error('Failed to send initial clipboard state: ' + err);
@ -237,7 +236,7 @@
if (!sameValue) {
lastClientClipboardItems = values;
let transaction = ClipboardTransaction.new();
let transaction = remoteDesktopService.constructClipboardTransaction();
// Iterate over `Record` type
values.forEach((value: string | Uint8Array, key: string) => {
@ -247,15 +246,15 @@
}
if (key.startsWith('text/') && typeof value === 'string') {
transaction.add_content(ClipboardContent.new_text(key, value));
transaction.add_content(remoteDesktopService.constructClipboardContentFromText(key, value));
} else if (key.startsWith('image/') && value instanceof Uint8Array) {
transaction.add_content(ClipboardContent.new_binary(key, value));
transaction.add_content(remoteDesktopService.constructClipboardContentFromBinary(key, value));
}
});
if (!transaction.is_empty()) {
lastClientClipboardTransaction = transaction;
wasmService.onClipboardChanged(transaction);
remoteDesktopService.onClipboardChanged(transaction);
}
}
} catch (err) {
@ -335,7 +334,7 @@
}
try {
let transaction = ClipboardTransaction.new();
let transaction = remoteDesktopService.constructClipboardTransaction();
if (evt.clipboardData == null) {
return;
@ -346,11 +345,11 @@
if (mime.startsWith('text/')) {
clipItem.getAsString((str: string) => {
let content = ClipboardContent.new_text(mime, str);
let content = remoteDesktopService.constructClipboardContentFromText(mime, str);
transaction.add_content(content);
if (!transaction.is_empty()) {
wasmService.onClipboardChanged(transaction);
remoteDesktopService.onClipboardChanged(transaction as ClipboardTransaction);
}
});
break;
@ -364,11 +363,11 @@
file.arrayBuffer().then((buffer: ArrayBuffer) => {
const strict_buffer = new Uint8Array(buffer);
let content = ClipboardContent.new_binary(mime, strict_buffer);
let content = remoteDesktopService.constructClipboardContentFromBinary(mime, strict_buffer);
transaction.add_content(content);
if (!transaction.is_empty()) {
wasmService.onClipboardChanged(transaction);
remoteDesktopService.onClipboardChanged(transaction);
}
});
break;
@ -454,7 +453,7 @@
}
function serverBridgeListeners() {
wasmService.resize.subscribe((evt: ResizeEvent) => {
remoteDesktopService.resize.subscribe((evt: ResizeEvent) => {
loggingService.info(`Resize canvas to: ${evt.desktop_size.width}x${evt.desktop_size.height}`);
canvas.width = evt.desktop_size.width;
canvas.height = evt.desktop_size.height;
@ -467,17 +466,17 @@
scaleSession(scale);
});
wasmService.scaleObserver.subscribe((s) => {
remoteDesktopService.scaleObserver.subscribe((s) => {
loggingService.info('Change scale!');
scaleSession(s);
});
wasmService.dynamicResize.subscribe((evt) => {
remoteDesktopService.dynamicResize.subscribe((evt) => {
loggingService.info(`Dynamic resize!, width: ${evt.width}, height: ${evt.height}`);
setViewerStyle(evt.width.toString(), evt.height.toString(), true);
});
wasmService.changeVisibilityObservable.subscribe((val) => {
remoteDesktopService.changeVisibilityObservable.subscribe((val) => {
isVisible = val;
if (val) {
//Enforce first scaling and delay the call to scaleSession to ensure Dom is ready.
@ -590,7 +589,7 @@
y: Math.round((evt.clientY - rect.top) * scaleY),
};
wasmService.updateMousePosition(coord);
remoteDesktopService.updateMousePosition(coord);
}
function setMouseButtonState(state: MouseEvent, isDown: boolean) {
@ -610,20 +609,20 @@
}
}
wasmService.mouseButtonState(state, isDown, true);
remoteDesktopService.mouseButtonState(state, isDown, true);
}
function mouseWheel(evt: WheelEvent) {
wasmService.mouseWheel(evt);
remoteDesktopService.mouseWheel(evt);
}
function setMouseIn(evt: MouseEvent) {
canvas.focus();
wasmService.mouseIn(evt);
remoteDesktopService.mouseIn(evt);
}
function setMouseOut(evt: MouseEvent) {
wasmService.mouseOut(evt);
remoteDesktopService.mouseOut(evt);
}
function keyboardEvent(evt: KeyboardEvent) {
@ -639,7 +638,7 @@
ffWaitForRemoteClipboardTransactionSet();
}
wasmService.sendKeyboardEvent(evt);
remoteDesktopService.sendKeyboardEvent(evt);
// Propagate further
return true;
@ -663,8 +662,8 @@
canvas.height = 600;
const logLevel = LogType[debugwasm] ?? LogType.INFO;
await wasmService.init(logLevel);
wasmService.setCanvas(canvas);
await remoteDesktopService.init(logLevel);
remoteDesktopService.setCanvas(canvas);
initListeners();

View file

@ -0,0 +1,15 @@
export * as default from './iron-remote-desktop.svelte';
export type { ResizeEvent } from './interfaces/ResizeEvent';
export type { NewSessionInfo } from './interfaces/NewSessionInfo';
export type { ServerRect } from './interfaces/ServerRect';
export type { DesktopSize } from './interfaces/DesktopSize';
export type { SessionEvent, RemoteDesktopError, RemoteDesktopErrorKind } from './interfaces/session-event';
export type { SessionEventType } from './enums/SessionEventType';
export type { SessionTerminationInfo } from './interfaces/SessionTerminationInfo';
export type { ClipboardTransaction } from './interfaces/ClipboardTransaction';
export type { ClipboardContent } from './interfaces/ClipboardContent';
export type { DeviceEvent } from './interfaces/DeviceEvent';
export type { InputTransaction } from './interfaces/InputTransaction';
export type { Session } from './interfaces/Session';
export type { SessionBuilder } from './interfaces/SessionBuilder';
export type { UserInteraction } from './interfaces/UserInteraction';

View file

@ -1,16 +1,16 @@
import { loggingService } from './logging.service';
import type { NewSessionInfo } from '../interfaces/NewSessionInfo';
import { SpecialCombination } from '../enums/SpecialCombination';
import type { WasmBridgeService } from './wasm-bridge.service';
import { RemoteDesktopService } from './remote-desktop.service';
import type { UserInteraction } from '../interfaces/UserInteraction';
import type { ScreenScale } from '../enums/ScreenScale';
import type { DesktopSize } from '../interfaces/DesktopSize';
export class PublicAPI {
private wasmService: WasmBridgeService;
private remoteDesktopService: RemoteDesktopService;
constructor(wasmService: WasmBridgeService) {
this.wasmService = wasmService;
constructor(remoteDesktopService: RemoteDesktopService) {
this.remoteDesktopService = remoteDesktopService;
}
private connect(
@ -26,7 +26,7 @@ export class PublicAPI {
use_display_control = false,
): Promise<NewSessionInfo> {
loggingService.info('Initializing connection.');
const resultObservable = this.wasmService.connect(
const resultObservable = this.remoteDesktopService.connect(
username,
password,
destination,
@ -43,40 +43,40 @@ export class PublicAPI {
}
private ctrlAltDel() {
this.wasmService.sendSpecialCombination(SpecialCombination.CTRL_ALT_DEL);
this.remoteDesktopService.sendSpecialCombination(SpecialCombination.CTRL_ALT_DEL);
}
private metaKey() {
this.wasmService.sendSpecialCombination(SpecialCombination.META);
this.remoteDesktopService.sendSpecialCombination(SpecialCombination.META);
}
private setVisibility(state: boolean) {
loggingService.info(`Change component visibility to: ${state}`);
this.wasmService.setVisibility(state);
this.remoteDesktopService.setVisibility(state);
}
private setScale(scale: ScreenScale) {
this.wasmService.setScale(scale);
this.remoteDesktopService.setScale(scale);
}
private shutdown() {
this.wasmService.shutdown();
this.remoteDesktopService.shutdown();
}
private setKeyboardUnicodeMode(use_unicode: boolean) {
this.wasmService.setKeyboardUnicodeMode(use_unicode);
this.remoteDesktopService.setKeyboardUnicodeMode(use_unicode);
}
private setCursorStyleOverride(style: string | null) {
this.wasmService.setCursorStyleOverride(style);
this.remoteDesktopService.setCursorStyleOverride(style);
}
private resize(width: number, height: number, scale?: number) {
this.wasmService.resizeDynamic(width, height, scale);
this.remoteDesktopService.resizeDynamic(width, height, scale);
}
private setEnableClipboard(enable: boolean) {
this.wasmService.setEnableClipboard(enable);
this.remoteDesktopService.setEnableClipboard(enable);
}
getExposedFunctions(): UserInteraction {
@ -85,7 +85,7 @@ export class PublicAPI {
connect: this.connect.bind(this),
setScale: this.setScale.bind(this),
onSessionEvent: (callback) => {
this.wasmService.sessionObserver.subscribe(callback);
this.remoteDesktopService.sessionObserver.subscribe(callback);
},
ctrlAltDel: this.ctrlAltDel.bind(this),
metaKey: this.metaKey.bind(this),

View file

@ -1,15 +1,4 @@
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
import init, {
DesktopSize,
DeviceEvent,
InputTransaction,
ironrdp_init,
IronRdpError,
Session,
SessionBuilder,
ClipboardTransaction,
SessionTerminationInfo,
} from '../../../../crates/ironrdp-web/pkg/ironrdp_web';
import { loggingService } from './logging.service';
import { catchError, filter, map } from 'rxjs/operators';
import { scanCode } from '../lib/scancodes';
@ -23,14 +12,21 @@ import { SpecialCombination } from '../enums/SpecialCombination';
import type { ResizeEvent } from '../interfaces/ResizeEvent';
import { ScreenScale } from '../enums/ScreenScale';
import type { MousePosition } from '../interfaces/MousePosition';
import type { SessionEvent, UserIronRdpErrorKind } from '../interfaces/session-event';
import type { DesktopSize as IDesktopSize } from '../interfaces/DesktopSize';
import type { SessionEvent, RemoteDesktopErrorKind, RemoteDesktopError } from '../interfaces/session-event';
import type { DesktopSize } from '../interfaces/DesktopSize';
import type { ClipboardTransaction } from '../interfaces/ClipboardTransaction';
import type { ClipboardContent } from '../interfaces/ClipboardContent';
import type { Session } from '../interfaces/Session';
import type { DeviceEvent } from '../interfaces/DeviceEvent';
import type { SessionTerminationInfo } from '../interfaces/SessionTerminationInfo';
import type { RemoteDesktopModule } from '../interfaces/RemoteDesktopModule';
type OnRemoteClipboardChanged = (transaction: ClipboardTransaction) => void;
type OnRemoteReceivedFormatsList = () => void;
type OnForceClipboardUpdate = () => void;
export class WasmBridgeService {
export class RemoteDesktopService {
private module: RemoteDesktopModule;
private _resize: Subject<ResizeEvent> = new Subject<ResizeEvent>();
private mousePosition: BehaviorSubject<MousePosition> = new BehaviorSubject<MousePosition>({
x: 0,
@ -62,16 +58,29 @@ export class WasmBridgeService {
height: number;
}>();
constructor() {
constructor(module: RemoteDesktopModule) {
this.resize = this._resize.asObservable();
this.module = module;
loggingService.info('Web bridge initialized.');
}
constructClipboardTransaction(): ClipboardTransaction {
return this.module.ClipboardTransaction.construct();
}
constructClipboardContentFromText(mime_type: string, text: string): ClipboardContent {
return this.module.ClipboardContent.new_text(mime_type, text);
}
constructClipboardContentFromBinary(mime_type: string, binary: Uint8Array): ClipboardContent {
return this.module.ClipboardContent.new_binary(mime_type, binary);
}
async init(debug: LogType) {
loggingService.info('Loading wasm file.');
await init();
await this.module.init();
loggingService.info('Initializing IronRDP.');
ironrdp_init(LogType[debug]);
this.module.iron_init(LogType[debug]);
}
// If set to false, the clipboard will not be enabled and the callbacks will not be registered to the Rust side
@ -115,12 +124,14 @@ export class WasmBridgeService {
if (preventDefault) {
event.preventDefault(); // prevent default behavior (context menu, etc)
}
const mouseFnc = isDown ? DeviceEvent.new_mouse_button_pressed : DeviceEvent.new_mouse_button_released;
const mouseFnc = isDown
? this.module.DeviceEvent.mouse_button_pressed
: this.module.DeviceEvent.mouse_button_released;
this.doTransactionFromDeviceEvents([mouseFnc(event.button)]);
}
updateMousePosition(position: MousePosition) {
this.doTransactionFromDeviceEvents([DeviceEvent.new_mouse_move(position.x, position.y)]);
this.doTransactionFromDeviceEvents([this.module.DeviceEvent.mouse_move(position.x, position.y)]);
this.mousePosition.next(position);
}
@ -131,12 +142,13 @@ export class WasmBridgeService {
proxyAddress: string,
serverDomain: string,
authToken: string,
desktopSize?: IDesktopSize,
desktopSize?: DesktopSize,
preConnectionBlob?: string,
kdc_proxy_url?: string,
use_display_control = true,
): Observable<NewSessionInfo> {
const sessionBuilder = SessionBuilder.new();
const sessionBuilder = this.module.SessionBuilder.construct();
sessionBuilder.proxy_address(proxyAddress);
sessionBuilder.destination(destination);
sessionBuilder.server_domain(serverDomain);
@ -146,13 +158,13 @@ export class WasmBridgeService {
sessionBuilder.render_canvas(this.canvas!);
sessionBuilder.set_cursor_style_callback_context(this);
sessionBuilder.set_cursor_style_callback(this.setCursorStyleCallback);
sessionBuilder.kdc_proxy_url(kdc_proxy_url);
if (use_display_control) {
sessionBuilder.use_display_control();
}
sessionBuilder.extension({ DisplayControl: use_display_control });
if (preConnectionBlob != null) {
sessionBuilder.pcb(preConnectionBlob);
sessionBuilder.extension({ Pcb: preConnectionBlob });
}
if (kdc_proxy_url != null) {
sessionBuilder.extension({ KdcProxyUrl: kdc_proxy_url });
}
if (this.onRemoteClipboardChanged != null && this.enableClipboard) {
sessionBuilder.remote_clipboard_changed_callback(this.onRemoteClipboardChanged);
@ -165,21 +177,22 @@ export class WasmBridgeService {
}
if (desktopSize != null) {
sessionBuilder.desktop_size(DesktopSize.new(desktopSize.width, desktopSize.height));
sessionBuilder.desktop_size(this.module.DesktopSize.construct(desktopSize.width, desktopSize.height));
}
// Type guard to filter out errors
function isSession(result: IronRdpError | Session): result is Session {
return result instanceof Session;
function isSession(result: RemoteDesktopError | Session): result is Session {
// Check whether function exists. To make it more robust we can check every method.
return (<Session>result).run !== undefined;
}
return from(sessionBuilder.connect()).pipe(
catchError((err: IronRdpError) => {
catchError((err: RemoteDesktopError) => {
this.raiseSessionEvent({
type: SessionEventType.ERROR,
data: {
backtrace: () => err.backtrace(),
kind: () => err.kind() as number as UserIronRdpErrorKind,
kind: () => err.kind() as number as RemoteDesktopErrorKind,
},
});
return of(err);
@ -188,7 +201,7 @@ export class WasmBridgeService {
map((session: Session) => {
from(session.run())
.pipe(
catchError((err) => {
catchError((err: RemoteDesktopError) => {
this.setVisibility(false);
this.raiseSessionEvent({
type: SessionEventType.ERROR,
@ -245,7 +258,7 @@ export class WasmBridgeService {
mouseWheel(event: WheelEvent) {
const vertical = event.deltaY !== 0;
const rotation = vertical ? event.deltaY : event.deltaX;
this.doTransactionFromDeviceEvents([DeviceEvent.new_wheel_rotations(vertical, -rotation)]);
this.doTransactionFromDeviceEvents([this.module.DeviceEvent.wheel_rotations(vertical, -rotation)]);
}
setVisibility(state: boolean) {
@ -274,6 +287,13 @@ export class WasmBridgeService {
return onClipboardChangedPromise();
}
onClipboardChangedEmpty(): Promise<void> {
const onClipboardChangedPromise = async () => {
await this.session?.on_clipboard_paste(this.module.ClipboardTransaction.construct());
};
return onClipboardChangedPromise();
}
setKeyboardUnicodeMode(use_unicode: boolean) {
this.keyboardUnicodeMode = use_unicode;
}
@ -314,11 +334,11 @@ export class WasmBridgeService {
let unicodeEvent;
if (evt.type === 'keydown') {
keyEvent = DeviceEvent.new_key_pressed;
unicodeEvent = DeviceEvent.new_unicode_pressed;
keyEvent = this.module.DeviceEvent.key_pressed;
unicodeEvent = this.module.DeviceEvent.unicode_pressed;
} else if (evt.type === 'keyup') {
keyEvent = DeviceEvent.new_key_released;
unicodeEvent = DeviceEvent.new_unicode_released;
keyEvent = this.module.DeviceEvent.key_released;
unicodeEvent = this.module.DeviceEvent.unicode_released;
}
let sendAsUnicode = true;
@ -449,7 +469,7 @@ export class WasmBridgeService {
}
private doTransactionFromDeviceEvents(deviceEvents: DeviceEvent[]) {
const transaction = InputTransaction.new();
const transaction = this.module.InputTransaction.construct();
deviceEvents.forEach((event) => transaction.add_event(event));
this.session?.apply_inputs(transaction);
}
@ -460,18 +480,21 @@ export class WasmBridgeService {
const suppr = parseInt('0xE053', 16);
this.doTransactionFromDeviceEvents([
DeviceEvent.new_key_pressed(ctrl),
DeviceEvent.new_key_pressed(alt),
DeviceEvent.new_key_pressed(suppr),
DeviceEvent.new_key_released(ctrl),
DeviceEvent.new_key_released(alt),
DeviceEvent.new_key_released(suppr),
this.module.DeviceEvent.key_pressed(ctrl),
this.module.DeviceEvent.key_pressed(alt),
this.module.DeviceEvent.key_pressed(suppr),
this.module.DeviceEvent.key_released(ctrl),
this.module.DeviceEvent.key_released(alt),
this.module.DeviceEvent.key_released(suppr),
]);
}
private sendMeta() {
const meta = parseInt('0xE05B', 16);
this.doTransactionFromDeviceEvents([DeviceEvent.new_key_pressed(meta), DeviceEvent.new_key_released(meta)]);
this.doTransactionFromDeviceEvents([
this.module.DeviceEvent.key_pressed(meta),
this.module.DeviceEvent.key_released(meta),
]);
}
}

View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
build: {
lib: {
entry: './src/main.ts',
name: 'IronRemoteGui',
name: 'IronRemoteDesktop',
formats: ['es'],
},
},

View file

@ -1,27 +0,0 @@
import { spawn } from 'child_process';
let run = async function (command, cwd) {
return new Promise((resolve) => {
const buildCommand = spawn(command, {
stdio: 'pipe',
shell: true,
cwd: cwd,
env: { ...process.env, RUSTFLAGS: '-Ctarget-feature=+simd128' },
});
buildCommand.stdout.on('data', (data) => {
console.log(`${data}`);
});
buildCommand.stderr.on('data', (data) => {
console.error(`${data}`);
});
buildCommand.on('close', (code) => {
console.log(`child process exited with code ${code}`);
resolve();
});
});
};
await run('wasm-pack build --target web', '../../crates/ironrdp-web');

View file

@ -1,9 +0,0 @@
export * as default from './iron-remote-gui.svelte';
export type { UserInteraction } from './interfaces/UserInteraction';
export type { ResizeEvent } from './interfaces/ResizeEvent';
export type { NewSessionInfo } from './interfaces/NewSessionInfo';
export type { ServerRect } from './interfaces/ServerRect';
export type { DesktopSize } from './interfaces/DesktopSize';
export type { SessionEvent, UserIronRdpError, UserIronRdpErrorKind } from './interfaces/session-event';
export type { SessionEventType } from './enums/SessionEventType';

View file

@ -3,4 +3,5 @@
.env
.env.*
!.env.example
/static/iron-remote-gui/
/static/iron-remote-desktop-rdp/
/static/iron-remote-desktop/

View file

@ -1,7 +1,7 @@
# SvelteKit UI for IronRDP
Web-based frontend using [`SvelteKit`](https://kit.svelte.dev/) and [`Material`](https://material.io) frameworks.
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` Web Component demonstrating how to use the API.
Note that this demonstration client is not intended to be used in production as-is.
Devolutions is shipping well-integrated, production-ready IronRDP web clients as part of:
@ -111,13 +111,13 @@ If you have a Rust toolchain available, you can use the [`tokengen`][tokengen] t
## Run in development mode
First, run `npm install` in the [iron-remote-gui](../iron-remote-gui/) folder, and then `npm install` in [iron-svelte-client](./) folder.
First, run `npm install` in [iron-remote-desktop](../iron-remote-desktop) and [iron-remote-desktop-rdp](../iron-remote-desktop-rdp) folders, and then `npm install` in [iron-svelte-client](./) folder.
You can then start the dev server with either:
- `npm run dev` - Runs only the final application.
- `npm run dev-all` - Builds WASM module and `iron-remote-gui` prior to starting the dev server.
- `npm run dev-no-wasm` - Only builds `iron-remote-gui` prior to starting the dev server.
- `npm run dev-all` - Builds WASM module and `iron-remote-desktop` prior to starting the dev server.
- `npm run dev-no-wasm` - Only builds `iron-remote-desktop` prior to starting the dev server.
You can build distribution files with `npm run build`.
Files are to be found in `./iron-svelte-client/build/browser`.

View file

@ -1,15 +1,11 @@
import * as fs from 'fs-extra';
import { spawn } from 'child_process';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { argv } from 'node:process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let noWasm = false;
let assetIronRemoteGuiFolder = './static/iron-remote-gui';
const assetIronRemoteDesktopFolder = './static/iron-remote-desktop';
const assetIronRemoteDesktopRdpFolder = './static/iron-remote-desktop-rdp';
argv.forEach((val, index) => {
if (index === 2 && val === 'no-wasm') {
@ -17,8 +13,8 @@ argv.forEach((val, index) => {
}
});
let run = async function (command, cwd) {
return new Promise((resolve) => {
const run = async (command, cwd) => {
try {
const buildCommand = spawn(command, { stdio: 'pipe', shell: true, cwd: cwd });
buildCommand.stdout.on('data', (data) => {
@ -29,29 +25,36 @@ let run = async function (command, cwd) {
console.error(`${data}`);
});
buildCommand.on('close', (code) => {
console.log(`child process exited with code ${code}`);
resolve();
return new Promise((resolve, reject) => {
buildCommand.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Process exited with code ${code}`));
} else {
console.log(`Child process exited successfully with code ${code}`);
resolve();
}
});
});
});
} catch (err) {
console.error(`Failed to execute the process: ${err}`);
}
};
let copyCoreFiles = async function () {
console.log('Copying core files…');
await fs.remove(assetIronRemoteGuiFolder);
return new Promise((resolve) => {
let source = '../iron-remote-gui/dist';
let destination = assetIronRemoteGuiFolder;
const copyCoreFiles = async () => {
try {
console.log('Copying core files…');
await fs.remove(assetIronRemoteDesktopFolder);
await fs.remove(assetIronRemoteDesktopRdpFolder);
fs.copy(source, destination, function (err) {
if (err) {
console.log('An error occurred while copying core files.');
return console.error(err);
}
console.log('Core files were copied successfully');
resolve();
});
});
const source = '../iron-remote-desktop/dist';
const sourceRdp = '../iron-remote-desktop-rdp/dist';
await fs.copy(source, assetIronRemoteDesktopFolder);
await fs.copy(sourceRdp, assetIronRemoteDesktopRdpFolder);
console.log('Core files were copied successfully');
} catch (err) {
console.error(`An error occurred while copying core files: ${err}`);
}
};
let buildCommand = 'npm run build';
@ -59,5 +62,6 @@ if (noWasm) {
buildCommand = 'npm run build-alone';
}
await run(buildCommand, '../iron-remote-gui');
await run(buildCommand, '../iron-remote-desktop');
await run(buildCommand, '../iron-remote-desktop-rdp');
await copyCoreFiles();

View file

@ -9,7 +9,7 @@
<link href="%sveltekit.assets%/beercss/beer.min.css" rel="stylesheet" />
<link href="%sveltekit.assets%/theme.css" rel="stylesheet" />
<script src="%sveltekit.assets%/beercss/beer.min.js" type="text/javascript"></script>
<script type="module" src="%sveltekit.assets%/iron-remote-gui/iron-remote-gui.js"></script>
<script type="module" src="%sveltekit.assets%/iron-remote-desktop/iron-remote-desktop.js"></script>
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>

View file

@ -1,11 +1,11 @@
<script lang="ts">
import { currentSession, userInteractionService } from '../../services/session.service';
import { catchError, filter } from 'rxjs/operators';
import type { UserInteraction, NewSessionInfo } from '../../../static/iron-remote-gui';
import type { UserInteraction, NewSessionInfo } from '../../../static/iron-remote-desktop';
import { from, of } from 'rxjs';
import { toast } from '$lib/messages/message-store';
import { showLogin } from '$lib/login/login-store';
import type { DesktopSize } from '../../models/desktop-size';
import { DesktopSize } from '../../models/desktop-size';
let username = 'Administrator';
let password = 'DevoLabs123!';
@ -14,10 +14,7 @@
let domain = '';
let authtoken = '';
let kdc_proxy_url = '';
let desktopSize: DesktopSize = {
width: 1280,
height: 768,
};
let desktopSize = new DesktopSize(1280, 768);
let pcb: string;
let pop_up = false;
let enable_clipboard = true;

View file

@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { setCurrentSessionActive, userInteractionService } from '../../services/session.service';
import type { UserInteraction } from '../../../static/iron-remote-gui';
import type { UserInteraction } from '../../../static/iron-remote-desktop';
import IronRdp from '../../../static/iron-remote-desktop-rdp';
let uiService: UserInteraction;
let cursorOverrideActive = false;
@ -93,10 +94,10 @@
}
onMount(async () => {
const el = document.querySelector('iron-remote-gui');
const el = document.querySelector('iron-remote-desktop');
if (el == null) {
throw '`iron-remote-gui` element not found';
throw '`iron-remote-desktop` element not found';
}
el.addEventListener('ready', (e) => {
@ -110,11 +111,7 @@
id="popup-screen"
style="display: flex; height: 100%; flex-direction: column; background-color: #2e2e2e; position: relative"
on:mousemove={(event) => {
if (event.clientY < 100) {
showUtilityBar = true;
} else {
showUtilityBar = false;
}
showUtilityBar = event.clientY < 100;
}}
>
<div class="tool-bar" class:hidden={!showUtilityBar}>
@ -139,7 +136,7 @@
</label>
</div>
</div>
<iron-remote-gui debugwasm="INFO" verbose="true" scale="fit" flexcenter="true" />
<iron-remote-desktop debugwasm="INFO" verbose="true" scale="fit" flexcenter="true" module={IronRdp} />
</div>
<style>

View file

@ -2,7 +2,8 @@
import { onMount } from 'svelte';
import { setCurrentSessionActive, userInteractionService } from '../../services/session.service';
import { showLogin } from '$lib/login/login-store';
import type { UserInteraction } from '../../../static/iron-remote-gui';
import type { UserInteraction } from '../../../static/iron-remote-desktop';
import IronRdp from '../../../static/iron-remote-desktop-rdp';
let uiService: UserInteraction;
let cursorOverrideActive = false;
@ -47,10 +48,10 @@
}
onMount(async () => {
let el = document.querySelector('iron-remote-gui');
let el = document.querySelector('iron-remote-desktop');
if (el == null) {
throw '`iron-remote-gui` element not found';
throw '`iron-remote-desktop` element not found';
}
el.addEventListener('ready', (e) => {
@ -100,7 +101,7 @@
</div>
{/if}
</div>
<iron-remote-gui debugwasm="INFO" verbose="true" scale="fit" flexcenter="true" />
<iron-remote-desktop debugwasm="INFO" verbose="true" scale="fit" flexcenter="true" module={IronRdp} />
</div>
<style>

View file

@ -1,4 +1,14 @@
export class DesktopSize {
width!: number;
height!: number;
import type { DesktopSize as IDesktopSize } from './../../../iron-remote-desktop/src/interfaces/DesktopSize';
export class DesktopSize implements IDesktopSize {
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
construct(width: number, height: number): DesktopSize {
return new DesktopSize(width, height);
}
width: number;
height: number;
}

View file

@ -1,7 +0,0 @@
export class Rect {
top!: number;
left!: number;
width!: number;
height!: number;
buffer!: ArrayBuffer;
}

View file

@ -2,7 +2,7 @@ import type { Guid } from 'guid-typescript';
import type { Writable } from 'svelte/store';
import { writable } from 'svelte/store';
import { Session } from '../models/session';
import type { UserInteraction } from '../../static/iron-remote-gui';
import type { UserInteraction } from '../../static/iron-remote-desktop';
export const userInteractionService: Writable<UserInteraction> = writable();
export const currentSession: Writable<Session> = writable();

View file

@ -6,6 +6,11 @@ import topLevelAwait from 'vite-plugin-top-level-await';
const config: UserConfig = {
mode: 'process.env.MODE' || 'development',
plugins: [sveltekit(), wasm(), topLevelAwait()],
server: {
fs: {
allow: ['./static'],
},
},
};
export default config;

View file

@ -76,7 +76,8 @@ pub fn lock_files(sh: &Shell) -> anyhow::Result<()> {
const LOCK_FILES: &[&str] = &[
"Cargo.lock",
"fuzz/Cargo.lock",
"web-client/iron-remote-gui/package-lock.json",
"web-client/iron-remote-desktop/package-lock.json",
"web-client/iron-remote-desktop-rdp/package-lock.json",
"web-client/iron-svelte-client/package-lock.json",
];

View file

@ -8,8 +8,10 @@ pub fn workspace(sh: &Shell) -> anyhow::Result<()> {
println!("Done.");
println!("Remove npm folders…");
sh.remove_path("./web-client/iron-remote-gui/node_modules")?;
sh.remove_path("./web-client/iron-remote-gui/dist")?;
sh.remove_path("./web-client/iron-remote-desktop/node_modules")?;
sh.remove_path("./web-client/iron-remote-desktop/dist")?;
sh.remove_path("./web-client/iron-remote-desktop-rdp/node_modules")?;
sh.remove_path("./web-client/iron-remote-desktop-rdp/dist")?;
sh.remove_path("./web-client/iron-svelte-client/node_modules")?;
println!("Done.");

View file

@ -35,6 +35,7 @@ TASKS:
wasm install Install dependencies required to build the WASM target
web check Ensure Web Client is building without error
web install Install dependencies required to build and run Web Client
web build Build the Web Client
web run Run SvelteKit-based standalone Web Client
ffi install Install all requirements for ffi tasks
ffi build [--release] Build DLL for FFI (default is debug)
@ -88,6 +89,7 @@ pub enum Action {
WasmInstall,
WebCheck,
WebInstall,
WebBuild,
WebRun,
FfiInstall,
FfiBuildDll {
@ -159,6 +161,7 @@ pub fn parse_args() -> anyhow::Result<Args> {
Some("web") => match args.subcommand()?.as_deref() {
Some("check") => Action::WebCheck,
Some("install") => Action::WebInstall,
Some("build") => Action::WebBuild,
Some("run") => Action::WebRun,
Some(unknown) => anyhow::bail!("unknown web action: {unknown}"),
None => Action::ShowHelp,

View file

@ -110,6 +110,7 @@ fn main() -> anyhow::Result<()> {
Action::WasmCheck => wasm::check(&sh)?,
Action::WasmInstall => wasm::install(&sh)?,
Action::WebCheck => web::check(&sh)?,
Action::WebBuild => web::build(&sh, false)?,
Action::WebInstall => web::install(&sh)?,
Action::WebRun => web::run(&sh)?,
Action::FfiInstall => ffi::install(&sh)?,

View file

@ -1,8 +1,11 @@
use crate::prelude::*;
use std::fs;
const IRON_REMOTE_GUI_PATH: &str = "./web-client/iron-remote-gui";
const IRON_REMOTE_DESKTOP_PATH: &str = "./web-client/iron-remote-desktop";
const IRON_REMOTE_DESKTOP_RDP_PATH: &str = "./web-client/iron-remote-desktop-rdp";
const IRON_SVELTE_CLIENT_PATH: &str = "./web-client/iron-svelte-client";
const IRONRDP_WEB_PATH: &str = "./crates/ironrdp-web";
const IRONRDP_WEB_PACKAGE_JS_PATH: &str = "./crates/ironrdp-web/pkg/ironrdp_web.js";
#[cfg(not(target_os = "windows"))]
const NPM: &str = "npm";
@ -12,7 +15,8 @@ const NPM: &str = "npm.cmd";
pub fn install(sh: &Shell) -> anyhow::Result<()> {
let _s = Section::new("WEB-INSTALL");
run_cmd_in!(sh, IRON_REMOTE_GUI_PATH, "{NPM} install")?;
run_cmd_in!(sh, IRON_REMOTE_DESKTOP_PATH, "{NPM} install")?;
run_cmd_in!(sh, IRON_REMOTE_DESKTOP_RDP_PATH, "{NPM} install")?;
run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} install")?;
cargo_install(sh, &WASM_PACK)?;
@ -25,8 +29,10 @@ pub fn check(sh: &Shell) -> anyhow::Result<()> {
build(sh, true)?;
run_cmd_in!(sh, IRON_REMOTE_GUI_PATH, "{NPM} run check")?;
run_cmd_in!(sh, IRON_REMOTE_GUI_PATH, "{NPM} run lint")?;
run_cmd_in!(sh, IRON_REMOTE_DESKTOP_PATH, "{NPM} run check")?;
run_cmd_in!(sh, IRON_REMOTE_DESKTOP_PATH, "{NPM} run lint")?;
run_cmd_in!(sh, IRON_REMOTE_DESKTOP_RDP_PATH, "{NPM} run check")?;
run_cmd_in!(sh, IRON_REMOTE_DESKTOP_RDP_PATH, "{NPM} run lint")?;
run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run check")?;
run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run lint")?;
@ -36,22 +42,34 @@ pub fn check(sh: &Shell) -> anyhow::Result<()> {
pub fn run(sh: &Shell) -> anyhow::Result<()> {
let _s = Section::new("WEB-RUN");
build(sh, false)?;
run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run dev-no-wasm")?;
Ok(())
}
fn build(sh: &Shell, wasm_pack_dev: bool) -> anyhow::Result<()> {
pub fn build(sh: &Shell, wasm_pack_dev: bool) -> anyhow::Result<()> {
if wasm_pack_dev {
run_cmd_in!(sh, IRONRDP_WEB_PATH, "wasm-pack build --dev --target web")?;
} else {
let _env_guard = sh.push_env("RUSTFLAGS", "-Ctarget-feature=+simd128");
let _env_guard = sh.push_env("RUSTFLAGS", "-Ctarget-feature=+simd128,+bulk-memory");
run_cmd_in!(sh, IRONRDP_WEB_PATH, "wasm-pack build --target web")?;
}
run_cmd_in!(sh, IRON_REMOTE_GUI_PATH, "{NPM} run build-alone")?;
let ironrdp_web_js_file_path = sh.current_dir().join(IRONRDP_WEB_PACKAGE_JS_PATH);
let ironrdp_web_js_content = fs::read_to_string(&ironrdp_web_js_file_path)?;
// Modify the js file to get rid of the `URL` object.
// Vite doesn't work properly with inlined urls in `new URL(url, import.meta.url)`.
let ironrdp_web_js_content = format!(
"import wasmUrl from './ironrdp_web_bg.wasm?url';\n\n{}",
ironrdp_web_js_content
);
let ironrdp_web_js_content =
ironrdp_web_js_content.replace("new URL('ironrdp_web_bg.wasm', import.meta.url)", "wasmUrl");
fs::write(&ironrdp_web_js_file_path, ironrdp_web_js_content)?;
run_cmd_in!(sh, IRON_SVELTE_CLIENT_PATH, "{NPM} run build-no-wasm")?;
Ok(())