refactor(web): rework clipboard API (#764)

This commit is contained in:
Benoît Cortier
2025-04-21 14:29:21 +00:00
committed by GitHub
parent f287e168a8
commit e0eea449b8
18 changed files with 313 additions and 289 deletions
+7 -10
View File
@@ -1,18 +1,15 @@
use wasm_bindgen::JsValue;
use web_sys::js_sys;
pub trait ClipboardTransaction {
type ClipboardContent: ClipboardContent;
pub trait ClipboardData {
type Item: ClipboardItem;
fn init() -> Self;
fn add_content(&mut self, content: Self::ClipboardContent);
fn is_empty(&self) -> bool;
fn contents(&self) -> js_sys::Array;
fn add_text(&mut self, mime_type: &str, text: &str);
fn add_binary(&mut self, mime_type: &str, binary: &[u8]);
fn items(&self) -> &[Self::Item];
}
pub trait ClipboardContent {
fn new_text(mime_type: &str, text: &str) -> Self;
fn new_binary(mime_type: &str, binary: &[u8]) -> Self;
pub trait ClipboardItem {
fn mime_type(&self) -> &str;
fn value(&self) -> JsValue;
fn value(&self) -> impl Into<JsValue>;
}
+70 -30
View File
@@ -6,7 +6,7 @@ mod extension;
mod input;
mod session;
pub use clipboard::{ClipboardContent, ClipboardTransaction};
pub use clipboard::{ClipboardData, ClipboardItem};
pub use cursor::CursorStyle;
pub use desktop_size::DesktopSize;
pub use error::{IronError, IronErrorKind};
@@ -20,8 +20,8 @@ pub trait RemoteDesktopApi {
type SessionTerminationInfo: SessionTerminationInfo;
type DeviceEvent: DeviceEvent;
type InputTransaction: InputTransaction;
type ClipboardTransaction: ClipboardTransaction;
type ClipboardContent: ClipboardContent;
type ClipboardData: ClipboardData;
type ClipboardItem: ClipboardItem;
type Error: IronError;
/// Called before the logger is set.
@@ -38,8 +38,8 @@ macro_rules! export {
use wasm_bindgen::prelude::*;
use web_sys::{js_sys, HtmlCanvasElement};
use $crate::{
ClipboardContent as _, ClipboardTransaction as _, DeviceEvent as _, InputTransaction as _,
IronError as _, RemoteDesktopApi, Session as _, SessionBuilder as _, SessionTerminationInfo as _,
ClipboardData as _, ClipboardItem as _, DeviceEvent as _, InputTransaction as _, IronError as _,
RemoteDesktopApi, Session as _, SessionBuilder as _, SessionTerminationInfo as _,
};
#[wasm_bindgen]
@@ -52,6 +52,12 @@ macro_rules! export {
#[wasm_bindgen]
pub struct DeviceEvent(<$api as RemoteDesktopApi>::DeviceEvent);
impl From<<$api as RemoteDesktopApi>::DeviceEvent> for DeviceEvent {
fn from(value: <$api as RemoteDesktopApi>::DeviceEvent) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl DeviceEvent {
pub fn mouse_button_pressed(button: u8) -> Self {
@@ -101,6 +107,12 @@ macro_rules! export {
#[wasm_bindgen]
pub struct InputTransaction(<$api as RemoteDesktopApi>::InputTransaction);
impl From<<$api as RemoteDesktopApi>::InputTransaction> for InputTransaction {
fn from(value: <$api as RemoteDesktopApi>::InputTransaction) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl InputTransaction {
pub fn init() -> Self {
@@ -115,6 +127,12 @@ macro_rules! export {
#[wasm_bindgen]
pub struct IronError(<$api as RemoteDesktopApi>::Error);
impl From<<$api as RemoteDesktopApi>::Error> for IronError {
fn from(value: <$api as RemoteDesktopApi>::Error) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl IronError {
pub fn backtrace(&self) -> String {
@@ -129,6 +147,12 @@ macro_rules! export {
#[wasm_bindgen]
pub struct Session(<$api as RemoteDesktopApi>::Session);
impl From<<$api as RemoteDesktopApi>::Session> for Session {
fn from(value: <$api as RemoteDesktopApi>::Session) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl Session {
pub async fn run(&self) -> Result<SessionTerminationInfo, IronError> {
@@ -163,7 +187,7 @@ macro_rules! export {
self.0.shutdown().map_err(IronError)
}
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronError> {
pub async fn on_clipboard_paste(&self, content: ClipboardData) -> Result<(), IronError> {
self.0.on_clipboard_paste(content.0).await.map_err(IronError)
}
@@ -191,6 +215,12 @@ macro_rules! export {
#[wasm_bindgen]
pub struct SessionBuilder(<$api as RemoteDesktopApi>::SessionBuilder);
impl From<<$api as RemoteDesktopApi>::SessionBuilder> for SessionBuilder {
fn from(value: <$api as RemoteDesktopApi>::SessionBuilder) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl SessionBuilder {
pub fn init() -> Self {
@@ -261,6 +291,12 @@ macro_rules! export {
#[wasm_bindgen]
pub struct SessionTerminationInfo(<$api as RemoteDesktopApi>::SessionTerminationInfo);
impl From<<$api as RemoteDesktopApi>::SessionTerminationInfo> for SessionTerminationInfo {
fn from(value: <$api as RemoteDesktopApi>::SessionTerminationInfo) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl SessionTerminationInfo {
pub fn reason(&self) -> String {
@@ -269,50 +305,54 @@ macro_rules! export {
}
#[wasm_bindgen]
pub struct ClipboardTransaction(<$api as RemoteDesktopApi>::ClipboardTransaction);
pub struct ClipboardData(<$api as RemoteDesktopApi>::ClipboardData);
impl From<<$api as RemoteDesktopApi>::ClipboardData> for ClipboardData {
fn from(value: <$api as RemoteDesktopApi>::ClipboardData) -> Self {
Self(value)
}
}
#[wasm_bindgen]
impl ClipboardTransaction {
impl ClipboardData {
pub fn init() -> Self {
Self(<<$api as RemoteDesktopApi>::ClipboardTransaction>::init())
Self(<<$api as RemoteDesktopApi>::ClipboardData>::init())
}
pub fn add_content(&mut self, content: ClipboardContent) {
self.0.add_content(content.0);
pub fn add_text(&mut self, mime_type: &str, text: &str) {
self.0.add_text(mime_type, text);
}
pub fn add_binary(&mut self, mime_type: &str, binary: &[u8]) {
self.0.add_binary(mime_type, binary);
}
pub fn items(&self) -> Vec<ClipboardItem> {
self.0.items().into_iter().cloned().map(ClipboardItem).collect()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub fn content(&self) -> js_sys::Array {
iron_remote_desktop::ClipboardTransaction::contents(&self.0)
#[wasm_bindgen]
pub struct ClipboardItem(<$api as RemoteDesktopApi>::ClipboardItem);
impl From<<$api as RemoteDesktopApi>::ClipboardItem> for ClipboardItem {
fn from(value: <$api as RemoteDesktopApi>::ClipboardItem) -> Self {
Self(value)
}
}
#[wasm_bindgen]
pub struct ClipboardContent(<$api as RemoteDesktopApi>::ClipboardContent);
#[wasm_bindgen]
impl ClipboardContent {
pub fn new_text(mime_type: &str, text: &str) -> Self {
Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_text(
mime_type, text,
))
}
pub fn new_binary(mime_type: &str, binary: &[u8]) -> Self {
Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_binary(
mime_type, binary,
))
}
impl ClipboardItem {
pub fn mime_type(&self) -> String {
self.0.mime_type().to_owned()
}
pub fn value(&self) -> JsValue {
iron_remote_desktop::ClipboardContent::value(&self.0)
self.0.value().into()
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
use wasm_bindgen::JsValue;
use web_sys::{js_sys, HtmlCanvasElement};
use crate::clipboard::ClipboardTransaction;
use crate::clipboard::ClipboardData;
use crate::error::IronError;
use crate::input::InputTransaction;
use crate::{DesktopSize, Extension};
@@ -46,7 +46,7 @@ pub trait SessionBuilder {
pub trait Session {
type SessionTerminationInfo: SessionTerminationInfo;
type InputTransaction: InputTransaction;
type ClipboardTransaction: ClipboardTransaction;
type ClipboardTransaction: ClipboardData;
type Error: IronError;
fn run(&self) -> impl core::future::Future<Output = Result<Self::SessionTerminationInfo, Self::Error>>;
@@ -11,12 +11,9 @@
//! requested: when pasting into notepad, which does not support "text/html", "text/plain"
//! will be requested, and when pasting into WordPad, "text/html" will be requested.
mod transaction;
use std::collections::HashMap;
use futures_channel::mpsc;
use iron_remote_desktop::{ClipboardContent as _, ClipboardTransaction as _};
use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackend};
use ironrdp::cliprdr::pdu::{
ClipboardFormat, ClipboardFormatId, ClipboardFormatName, ClipboardGeneralCapabilityFlags, FileContentsRequest,
@@ -25,14 +22,10 @@ use ironrdp::cliprdr::pdu::{
use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dibv5};
use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html};
use ironrdp_core::{impl_as_any, IntoOwned};
use transaction::ClipboardContentValue;
use wasm_bindgen::prelude::*;
use crate::session::RdpInputEvent;
#[rustfmt::skip]
pub(crate) use transaction::{RdpClipboardTransaction, RdpClipboardContent};
const MIME_TEXT: &str = "text/plain";
const MIME_HTML: &str = "text/html";
const MIME_PNG: &str = "image/png";
@@ -104,7 +97,7 @@ impl WasmClipboardMessageProxy {
/// Messages sent by the JS code or CLIPRDR to the backend implementation.
#[derive(Debug)]
pub(crate) enum WasmClipboardBackendMessage {
LocalClipboardChanged(RdpClipboardTransaction),
LocalClipboardChanged(ClipboardData),
RemoteDataRequest(ClipboardFormatId),
RemoteClipboardChanged(Vec<ClipboardFormat>),
@@ -117,8 +110,8 @@ pub(crate) enum WasmClipboardBackendMessage {
/// Clipboard backend implementation for web. This object should be created once per session and
/// kept alive until session is terminated.
pub(crate) struct WasmClipboard {
local_clipboard: Option<RdpClipboardTransaction>,
remote_clipboard: RdpClipboardTransaction,
local_clipboard: Option<ClipboardData>,
remote_clipboard: ClipboardData,
remote_mapping: HashMap<ClipboardFormatId, String>,
remote_formats_to_read: Vec<ClipboardFormatId>,
@@ -138,7 +131,7 @@ impl WasmClipboard {
pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self {
Self {
local_clipboard: None,
remote_clipboard: RdpClipboardTransaction::init(),
remote_clipboard: ClipboardData::new(),
proxy: message_proxy,
js_callbacks,
@@ -154,13 +147,10 @@ impl WasmClipboard {
}
}
fn handle_local_clipboard_changed(
&mut self,
transaction: RdpClipboardTransaction,
) -> anyhow::Result<Vec<ClipboardFormat>> {
fn handle_local_clipboard_changed(&mut self, transaction: ClipboardData) -> anyhow::Result<Vec<ClipboardFormat>> {
let mut formats = Vec::new();
transaction.contents().iter().for_each(|content| {
match content.mime_type() {
transaction.items().iter().for_each(|content| {
match content.mime_type.as_str() {
MIME_TEXT => formats.push(ClipboardFormat::new(ClipboardFormatId::CF_UNICODETEXT)),
MIME_HTML => {
formats.extend([
@@ -204,15 +194,15 @@ impl WasmClipboard {
let find_content_by_mime = |mime: &str| {
transaction
.contents()
.items()
.iter()
.find(|content| content.mime_type() == mime)
.find(|content| content.mime_type.as_str() == mime)
};
let find_text_content_by_mime = |mime: &str| {
find_content_by_mime(mime)
.and_then(|content| {
if let ClipboardContentValue::Text(text) = content.value() {
if let ClipboardItemValue::Text(text) = &content.value {
Some(text.as_str())
} else {
None
@@ -224,7 +214,7 @@ impl WasmClipboard {
let find_binary_content_by_mime = |mime: &str| {
find_content_by_mime(mime)
.and_then(|content| {
if let ClipboardContentValue::Binary(binary) = content.value() {
if let ClipboardItemValue::Binary(binary) = &content.value {
Some(binary.as_slice())
} else {
None
@@ -372,21 +362,21 @@ impl WasmClipboard {
let content = match pending_format {
ClipboardFormatId::CF_UNICODETEXT => match response.to_unicode_string() {
Ok(text) => Some(RdpClipboardContent::new_text(MIME_TEXT, &text)),
Ok(text) => Some(ClipboardItem::new_text(MIME_TEXT, text)),
Err(err) => {
error!("CF_UNICODETEXT decode error: {}", err);
None
}
},
ClipboardFormatId::CF_DIB => match dib_to_png(response.data()) {
Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)),
Ok(png) => Some(ClipboardItem::new_binary(MIME_PNG, png)),
Err(err) => {
warn!("DIB decode error: {}", err);
None
}
},
ClipboardFormatId::CF_DIBV5 => match dibv5_to_png(response.data()) {
Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)),
Ok(png) => Some(ClipboardItem::new_binary(MIME_PNG, png)),
Err(err) => {
warn!("DIBv5 decode error: {}", err);
None
@@ -394,23 +384,24 @@ impl WasmClipboard {
},
registered => {
let format_name = self.remote_mapping.get(&registered).map(|s| s.as_str());
match format_name {
Some(FORMAT_WIN_HTML_NAME) => match cf_html_to_plain_html(response.data()) {
Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, text)),
Ok(text) => Some(ClipboardItem::new_text(MIME_HTML, text.to_owned())),
Err(err) => {
warn!("CF_HTML decode error: {}", err);
None
}
},
Some(FORMAT_MIME_HTML_NAME) => match response.to_string() {
Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, &text)),
Ok(text) => Some(ClipboardItem::new_text(MIME_HTML, text)),
Err(err) => {
warn!("text/html decode error: {}", err);
None
}
},
Some(FORMAT_MIME_PNG_NAME) | Some(FORMAT_PNG_NAME) => {
Some(RdpClipboardContent::new_binary(MIME_PNG, response.data()))
Some(ClipboardItem::new_binary(MIME_PNG, response.data().to_owned()))
}
_ => {
// Not supported format
@@ -421,7 +412,7 @@ impl WasmClipboard {
};
if let Some(content) = content {
self.remote_clipboard.add_content(content);
self.remote_clipboard.add(content);
}
if let Some(format) = self.remote_formats_to_read.last() {
@@ -429,16 +420,20 @@ impl WasmClipboard {
self.proxy
.send_cliprdr_message(ClipboardMessage::SendInitiatePaste(*format));
} else {
// All formats were read, send clipboard to JS
let transaction = core::mem::take(&mut self.remote_clipboard);
if transaction.is_empty() {
// All formats were read, send clipboard to JS.
let clipboard_data = core::mem::take(&mut self.remote_clipboard);
if clipboard_data.is_empty() {
return Ok(());
}
// Set clipboard when all formats were read
// Set clipboard when all formats were read.
self.js_callbacks
.on_remote_clipboard_changed
.call1(&JsValue::NULL, &JsValue::from(transaction))
.call1(
&JsValue::NULL,
&JsValue::from(crate::__wasm_ffi::ClipboardData::from(clipboard_data)),
)
.expect("failed to call JS callback");
}
@@ -454,18 +449,18 @@ impl WasmClipboard {
self.proxy
.send_cliprdr_message(ClipboardMessage::SendInitiateCopy(formats));
}
Err(err) => {
// Not a critical error, we could skip single clipboard update
error!("Failed to handle local clipboard change: {}", err);
Err(e) => {
// Not a critical error, we could skip single clipboard update.
error!(error = format!("{e:#}"), "Failed to handle local clipboard change");
}
}
}
WasmClipboardBackendMessage::RemoteDataRequest(format) => {
let message = match self.process_remote_data_request(format) {
Ok(message) => message,
Err(err) => {
// Not a critical error, but we should notify remote about error
error!("Failed to process remote data request: {}", err);
Err(e) => {
// Not a critical error, but we should notify remote about it.
error!(error = format!("{e:#}"), "Failed to process remote data request");
FormatDataResponse::new_error()
}
};
@@ -483,32 +478,31 @@ impl WasmClipboard {
Ok(None) => {
// No formats to query
}
Err(err) => {
error!("Failed to process remote clipboard change: {}", err);
Err(e) => {
error!(error = format!("{e:#}"), "Failed to process remote clipboard change");
}
}
}
WasmClipboardBackendMessage::RemoteDataResponse(formats) => {
match self.process_remote_data_response(formats) {
Ok(()) => {}
Err(err) => {
error!("Failed to process remote data response: {}", err);
Err(e) => {
error!(error = format!("{e:#}"), "Failed to process remote data response");
}
}
}
WasmClipboardBackendMessage::FormatListReceived => {
if let Some(callback) = self.js_callbacks.on_remote_received_format_list.as_mut() {
callback.call0(&JsValue::NULL).expect("Failed to call JS callback");
callback.call0(&JsValue::NULL).expect("failed to call JS callback");
}
}
WasmClipboardBackendMessage::ForceClipboardUpdate => {
if let Some(callback) = self.js_callbacks.on_force_clipboard_update.as_mut() {
callback.call0(&JsValue::NULL).expect("Failed to call JS callback");
callback.call0(&JsValue::NULL).expect("failed to call JS callback");
} else {
// If no initial clipboard callback was set, send empty format list instead
return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged(
RdpClipboardTransaction::init(),
));
return self
.process_event(WasmClipboardBackendMessage::LocalClipboardChanged(ClipboardData::new()));
}
}
};
@@ -587,3 +581,108 @@ impl CliprdrBackend for WasmClipboardBackend {
// File transfer not implemented yet
}
}
/// Object which represents complete clipboard transaction with multiple MIME types.
#[derive(Debug, Default, Clone)]
pub(crate) struct ClipboardData {
items: Vec<ClipboardItem>,
}
impl ClipboardData {
pub(crate) fn new() -> Self {
Self { items: Vec::new() }
}
pub(crate) fn add(&mut self, item: ClipboardItem) {
self.items.push(item);
}
pub(crate) fn items(&self) -> &[ClipboardItem] {
&self.items
}
pub(crate) fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub(crate) fn clear(&mut self) {
self.items.clear();
}
}
impl iron_remote_desktop::ClipboardData for ClipboardData {
type Item = ClipboardItem;
fn init() -> Self {
Self::new()
}
fn add_text(&mut self, mime_type: &str, text: &str) {
self.items.push(ClipboardItem {
mime_type: mime_type.to_owned(),
value: ClipboardItemValue::Text(text.to_owned()),
})
}
fn add_binary(&mut self, mime_type: &str, binary: &[u8]) {
self.items.push(ClipboardItem {
mime_type: mime_type.to_owned(),
value: ClipboardItemValue::Binary(binary.to_owned()),
})
}
fn items(&self) -> &[Self::Item] {
&self.items
}
}
impl FromIterator<ClipboardItem> for ClipboardData {
fn from_iter<T: IntoIterator<Item = ClipboardItem>>(iter: T) -> Self {
Self {
items: iter.into_iter().collect(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum ClipboardItemValue {
Text(String),
Binary(Vec<u8>),
}
/// Object which represents single clipboard format represented standard MIME type.
#[derive(Debug, Clone)]
pub(crate) struct ClipboardItem {
pub(crate) mime_type: String,
pub(crate) value: ClipboardItemValue,
}
impl ClipboardItem {
pub(crate) fn new_text(mime_type: impl Into<String>, text: String) -> Self {
Self {
mime_type: mime_type.into(),
value: ClipboardItemValue::Text(text),
}
}
pub(crate) fn new_binary(mime_type: impl Into<String>, payload: Vec<u8>) -> Self {
Self {
mime_type: mime_type.into(),
value: ClipboardItemValue::Binary(payload),
}
}
}
impl iron_remote_desktop::ClipboardItem for ClipboardItem {
fn mime_type(&self) -> &str {
&self.mime_type
}
#[allow(refining_impl_trait)]
fn value(&self) -> JsValue {
match &self.value {
ClipboardItemValue::Text(text) => JsValue::from_str(text),
ClipboardItemValue::Binary(binary) => JsValue::from(js_sys::Uint8Array::from(binary.as_slice())),
}
}
}
@@ -1,108 +0,0 @@
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
/// Object which represents complete clipboard transaction with multiple MIME types.
#[wasm_bindgen]
#[derive(Debug, Default, Clone)]
pub(crate) struct RdpClipboardTransaction {
contents: Vec<RdpClipboardContent>,
}
impl RdpClipboardTransaction {
pub(crate) fn contents(&self) -> &[RdpClipboardContent] {
&self.contents
}
pub(crate) fn clear(&mut self) {
self.contents.clear();
}
}
impl iron_remote_desktop::ClipboardTransaction for RdpClipboardTransaction {
type ClipboardContent = RdpClipboardContent;
fn init() -> Self {
Self { contents: Vec::new() }
}
fn add_content(&mut self, content: Self::ClipboardContent) {
self.contents.push(content);
}
fn is_empty(&self) -> bool {
self.contents.is_empty()
}
fn contents(&self) -> js_sys::Array {
js_sys::Array::from_iter(
self.contents
.iter()
.map(|content: &RdpClipboardContent| JsValue::from(content.clone())),
)
}
}
impl FromIterator<RdpClipboardContent> for RdpClipboardTransaction {
fn from_iter<T: IntoIterator<Item = RdpClipboardContent>>(iter: T) -> Self {
Self {
contents: iter.into_iter().collect(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum ClipboardContentValue {
Text(String),
Binary(Vec<u8>),
}
impl ClipboardContentValue {
pub(crate) fn value(&self) -> JsValue {
match self {
ClipboardContentValue::Text(text) => JsValue::from_str(text),
ClipboardContentValue::Binary(binary) => js_sys::Uint8Array::from(binary.as_slice()).into(),
}
}
}
/// Object which represents single clipboard format represented standard MIME type.
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub(crate) struct RdpClipboardContent {
mime_type: String,
value: ClipboardContentValue,
}
impl RdpClipboardContent {
pub(crate) fn mime_type(&self) -> &str {
&self.mime_type
}
pub(crate) fn value(&self) -> &ClipboardContentValue {
&self.value
}
}
impl iron_remote_desktop::ClipboardContent for RdpClipboardContent {
fn new_text(mime_type: &str, text: &str) -> Self {
Self {
mime_type: mime_type.into(),
value: ClipboardContentValue::Text(text.to_owned()),
}
}
fn new_binary(mime_type: &str, binary: &[u8]) -> Self {
Self {
mime_type: mime_type.into(),
value: ClipboardContentValue::Binary(binary.to_vec()),
}
}
fn mime_type(&self) -> &str {
self.mime_type.as_str()
}
fn value(&self) -> JsValue {
self.value.value()
}
}
+2 -2
View File
@@ -30,8 +30,8 @@ impl RemoteDesktopApi for Api {
type SessionTerminationInfo = session::SessionTerminationInfo;
type DeviceEvent = input::DeviceEvent;
type InputTransaction = input::InputTransaction;
type ClipboardTransaction = clipboard::RdpClipboardTransaction;
type ClipboardContent = clipboard::RdpClipboardContent;
type ClipboardData = clipboard::ClipboardData;
type ClipboardItem = clipboard::ClipboardItem;
type Error = error::IronError;
fn post_setup() {
+2 -2
View File
@@ -35,7 +35,7 @@ use web_sys::HtmlCanvasElement;
use crate::canvas::Canvas;
use crate::clipboard;
use crate::clipboard::{RdpClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::clipboard::{ClipboardData, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::error::IronError;
use crate::image::extract_partial_image;
use crate::input::InputTransaction;
@@ -433,7 +433,7 @@ impl Session {
impl iron_remote_desktop::Session for Session {
type SessionTerminationInfo = SessionTerminationInfo;
type InputTransaction = InputTransaction;
type ClipboardTransaction = RdpClipboardTransaction;
type ClipboardTransaction = ClipboardData;
type Error = IronError;
async fn run(&self) -> Result<Self::SessionTerminationInfo, Self::Error> {
@@ -7,8 +7,8 @@ import init, {
Session,
SessionBuilder,
SessionTerminationInfo,
ClipboardTransaction,
ClipboardContent,
ClipboardData,
ClipboardItem,
Extension,
} from '../../../crates/ironrdp-web/pkg/ironrdp_web';
@@ -20,8 +20,8 @@ export default {
InputTransaction,
IronError,
SessionBuilder,
ClipboardTransaction,
ClipboardContent,
ClipboardData,
ClipboardItem,
Session,
SessionTerminationInfo,
Extension,
@@ -1,6 +0,0 @@
export interface ClipboardContent {
new_text(mime_type: string, text: string): ClipboardContent;
new_binary(mime_type: string, binary: Uint8Array): ClipboardContent;
mime_type(): string;
value(): string;
}
@@ -0,0 +1,9 @@
import type { ClipboardItem } from './ClipboardItem';
export interface ClipboardData {
init(): ClipboardData;
add_text(mime_type: string, text: string): void;
add_binary(mime_type: string, binary: Uint8Array): void;
items(): ClipboardItem[];
is_empty(): boolean;
}
@@ -0,0 +1,4 @@
export interface ClipboardItem {
mime_type(): string;
value(): string | Uint8Array;
}
@@ -1,8 +0,0 @@
import type { ClipboardContent } from './ClipboardContent';
export interface ClipboardTransaction {
init(): ClipboardTransaction;
add_content(content: ClipboardContent): void;
is_empty(): boolean;
content(): Array<ClipboardContent>;
}
@@ -5,8 +5,8 @@ import type { IronError } 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';
import type { ClipboardData } from './ClipboardData';
import type { ClipboardItem } from './ClipboardItem';
export interface RemoteDesktopModule {
init: () => Promise<unknown>;
@@ -18,6 +18,6 @@ export interface RemoteDesktopModule {
Session: Session;
SessionBuilder: SessionBuilder;
SessionTerminationInfo: SessionTerminationInfo;
ClipboardTransaction: ClipboardTransaction;
ClipboardContent: ClipboardContent;
ClipboardData: ClipboardData;
ClipboardItem: ClipboardItem;
}
@@ -1,7 +1,7 @@
import type { InputTransaction } from './InputTransaction';
import type { DesktopSize } from './DesktopSize';
import type { SessionTerminationInfo } from './SessionTerminationInfo';
import type { ClipboardTransaction } from './ClipboardTransaction';
import type { ClipboardData } from './ClipboardData';
export interface Session {
run(): Promise<SessionTerminationInfo>;
@@ -11,7 +11,7 @@ export interface Session {
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>;
on_clipboard_paste(data: ClipboardData): Promise<void>;
resize(
width: number,
height: number,
@@ -1,6 +1,6 @@
import type { Session } from './Session';
import type { DesktopSize } from './DesktopSize';
import type { ClipboardTransaction } from './ClipboardTransaction';
import type { ClipboardData } from './ClipboardData';
export interface SessionBuilder {
init(): SessionBuilder;
@@ -76,7 +76,7 @@ interface SetCursorStyleCallback {
}
interface RemoteClipboardChangedCallback {
(transaction: ClipboardTransaction): void;
(data: ClipboardData): void;
}
interface RemoteReceiveForwardListCallback {
@@ -21,7 +21,7 @@
import type { ResizeEvent } from './interfaces/ResizeEvent';
import { PublicAPI } from './services/PublicAPI';
import { ScreenScale } from './enums/ScreenScale';
import type { ClipboardTransaction } from './interfaces/ClipboardTransaction';
import type { ClipboardData } from './interfaces/ClipboardData';
import type { RemoteDesktopModule } from './interfaces/RemoteDesktopModule';
let {
@@ -68,14 +68,14 @@
let isClipboardApiSupported = false;
let lastClientClipboardItems = new Map<string, string | Uint8Array>();
let lastClientClipboardTransaction: ClipboardTransaction | null = null;
let lastClientClipboardData: ClipboardData | null = null;
let lastClipboardMonitorLoopError: Error | null = null;
/* Firefox-specific BEGIN */
// See `ffRemoteClipboardTransaction` variable docs below
const FF_REMOTE_CLIPBOARD_TRANSACTION_SET_RETRY_INTERVAL = 100; // ms
const FF_REMOTE_CLIPBOARD_TRANSACTION_SET_MAX_RETRIES = 30; // 3 seconds (100ms * 30)
// See `ffRemoteClipboardData` variable docs below
const FF_REMOTE_CLIPBOARD_DATA_SET_RETRY_INTERVAL = 100; // ms
const FF_REMOTE_CLIPBOARD_DATA_SET_MAX_RETRIES = 30; // 3 seconds (100ms * 30)
// On Firefox, this interval is used to stop delaying the keyboard events if the paste event has
// failed and we haven't received any clipboard data from the remote side.
const FF_LOCAL_CLIPBOARD_COPY_TIMEOUT = 1000; // 1s (For text-only data this should be enough)
@@ -84,10 +84,10 @@
// called in scope of user-initiated event processing (e.g. keyboard event), but we receive
// clipboard data from the remote side asynchronously in wasm service callback. therefore we
// set this variable in callback and use its value on the user-initiated copy event.
let ffRemoteClipboardTransaction: ClipboardTransaction | null = null;
let ffRemoteClipboardData: ClipboardData | null = null;
// For Firefox we need this variable to perform wait loop for the remote side to finish sending
// clipboard content to the client.
let ffRemoteClipboardTransactionRetriesLeft = 0;
let ffRemoteClipboardDataRetriesLeft = 0;
let ffPostponeKeyboardEvents = false;
let ffDelayedKeyboardEvents: KeyboardEvent[] = [];
let ffCnavasFocused = false;
@@ -131,12 +131,12 @@
return (evt.ctrlKey && evt.code === 'KeyV') || evt.code == 'Paste';
}
// This function is required to covert `ClipboardTransaction` to a object that can be used
// This function is required to convert `ClipboardData` to a object that can be used
// with `ClipboardItem` API.
function clipboardTransactionToRecord(transaction: ClipboardTransaction): Record<string, Blob> {
function clipboardDataToRecord(data: ClipboardData): Record<string, Blob> {
let result = {} as Record<string, Blob>;
for (const item of transaction.content()) {
for (const item of data.items()) {
let mime = item.mime_type();
let value = new Blob([item.value()], { type: mime });
@@ -149,8 +149,8 @@
// This callback is required to send initial clipboard state if available.
function onForceClipboardUpdate() {
try {
if (lastClientClipboardTransaction) {
remoteDesktopService.onClipboardChanged(lastClientClipboardTransaction);
if (lastClientClipboardData) {
remoteDesktopService.onClipboardChanged(lastClientClipboardData);
} else {
remoteDesktopService.onClipboardChangedEmpty();
}
@@ -160,9 +160,9 @@
}
// This callback is required to update client clipboard state when remote side has changed.
function onRemoteClipboardChanged(transaction: ClipboardTransaction) {
function onRemoteClipboardChanged(data: ClipboardData) {
try {
const mime_formats = clipboardTransactionToRecord(transaction);
const mime_formats = clipboardDataToRecord(data);
const clipboard_item = new ClipboardItem(mime_formats);
navigator.clipboard.write([clipboard_item]);
} catch (err) {
@@ -236,7 +236,7 @@
if (!sameValue) {
lastClientClipboardItems = values;
let transaction = remoteDesktopService.constructClipboardTransaction();
let data = remoteDesktopService.createClipboardData();
// Iterate over `Record` type
values.forEach((value: string | Uint8Array, key: string) => {
@@ -246,15 +246,15 @@
}
if (key.startsWith('text/') && typeof value === 'string') {
transaction.add_content(remoteDesktopService.constructClipboardContentFromText(key, value));
data.add_text(key, value);
} else if (key.startsWith('image/') && value instanceof Uint8Array) {
transaction.add_content(remoteDesktopService.constructClipboardContentFromBinary(key, value));
data.add_binary(key, value);
}
});
if (!transaction.is_empty()) {
lastClientClipboardTransaction = transaction;
remoteDesktopService.onClipboardChanged(transaction);
if (!data.is_empty()) {
lastClientClipboardData = data;
remoteDesktopService.onClipboardChanged(data);
}
}
} catch (err) {
@@ -286,28 +286,35 @@
// Only set variable on callback, the real clipboard update will be performed in keyboard
// callback. (User-initiated event is required for Firefox to allow clipboard write)
function ffOnRemoteClipboardChanged(transaction: ClipboardTransaction) {
ffRemoteClipboardTransaction = transaction;
function ffOnRemoteClipboardChanged(data: ClipboardData) {
ffRemoteClipboardData = data;
}
function ffWaitForRemoteClipboardTransactionSet() {
if (ffRemoteClipboardTransaction) {
function ffWaitForRemoteClipboardDataSet() {
if (ffRemoteClipboardData) {
try {
let transaction = ffRemoteClipboardTransaction;
ffRemoteClipboardTransaction = null;
for (const content of transaction.content()) {
let clipboard_data = ffRemoteClipboardData;
ffRemoteClipboardData = null;
for (const item of clipboard_data.items()) {
// Firefox only supports text/plain mime type for clipboard writes :(
if (content.mime_type() === 'text/plain') {
navigator.clipboard.writeText(content.value());
if (item.mime_type() === 'text/plain') {
const value = item.value();
if (typeof value === 'string') {
navigator.clipboard.writeText(value);
} else {
loggingService.error('Unexpected value for text/plain clipboard item');
}
break;
}
}
} catch (err) {
console.error('Failed to set client clipboard: ' + err);
}
} else if (ffRemoteClipboardTransactionRetriesLeft > 0) {
ffRemoteClipboardTransactionRetriesLeft--;
setTimeout(ffWaitForRemoteClipboardTransactionSet, FF_REMOTE_CLIPBOARD_TRANSACTION_SET_RETRY_INTERVAL);
} else if (ffRemoteClipboardDataRetriesLeft > 0) {
ffRemoteClipboardDataRetriesLeft--;
setTimeout(ffWaitForRemoteClipboardDataSet, FF_REMOTE_CLIPBOARD_DATA_SET_RETRY_INTERVAL);
}
}
@@ -334,7 +341,7 @@
}
try {
let transaction = remoteDesktopService.constructClipboardTransaction();
let clipboard_data = remoteDesktopService.createClipboardData();
if (evt.clipboardData == null) {
return;
@@ -345,11 +352,10 @@
if (mime.startsWith('text/')) {
clipItem.getAsString((str: string) => {
let content = remoteDesktopService.constructClipboardContentFromText(mime, str);
transaction.add_content(content);
clipboard_data.add_text(mime, str);
if (!transaction.is_empty()) {
remoteDesktopService.onClipboardChanged(transaction as ClipboardTransaction);
if (!clipboard_data.is_empty()) {
remoteDesktopService.onClipboardChanged(clipboard_data);
}
});
break;
@@ -363,11 +369,11 @@
file.arrayBuffer().then((buffer: ArrayBuffer) => {
const strict_buffer = new Uint8Array(buffer);
let content = remoteDesktopService.constructClipboardContentFromBinary(mime, strict_buffer);
transaction.add_content(content);
if (!transaction.is_empty()) {
remoteDesktopService.onClipboardChanged(transaction);
clipboard_data.add_binary(mime, strict_buffer);
if (!clipboard_data.is_empty()) {
remoteDesktopService.onClipboardChanged(clipboard_data);
}
});
break;
@@ -634,8 +640,8 @@
// only after some user-initiated event (e.g. keyboard event).
// therefore we need to wait here for the clipboard data to be ready.
ffRemoteClipboardTransactionRetriesLeft = FF_REMOTE_CLIPBOARD_TRANSACTION_SET_MAX_RETRIES;
ffWaitForRemoteClipboardTransactionSet();
ffRemoteClipboardDataRetriesLeft = FF_REMOTE_CLIPBOARD_DATA_SET_MAX_RETRIES;
ffWaitForRemoteClipboardDataSet();
}
remoteDesktopService.sendKeyboardEvent(evt);
+2 -2
View File
@@ -6,8 +6,8 @@ export type { DesktopSize } from './interfaces/DesktopSize';
export type { SessionEvent, IronError, IronErrorKind } 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 { ClipboardData } from './interfaces/ClipboardData';
export type { ClipboardItem } from './interfaces/ClipboardItem';
export type { DeviceEvent } from './interfaces/DeviceEvent';
export type { InputTransaction } from './interfaces/InputTransaction';
export type { Session } from './interfaces/Session';
@@ -13,8 +13,7 @@ import type { ResizeEvent } from '../interfaces/ResizeEvent';
import { ScreenScale } from '../enums/ScreenScale';
import type { MousePosition } from '../interfaces/MousePosition';
import type { SessionEvent, IronErrorKind, IronError } from '../interfaces/session-event';
import type { ClipboardTransaction } from '../interfaces/ClipboardTransaction';
import type { ClipboardContent } from '../interfaces/ClipboardContent';
import type { ClipboardData } from '../interfaces/ClipboardData';
import type { Session } from '../interfaces/Session';
import type { DeviceEvent } from '../interfaces/DeviceEvent';
import type { SessionTerminationInfo } from '../interfaces/SessionTerminationInfo';
@@ -22,7 +21,7 @@ import type { RemoteDesktopModule } from '../interfaces/RemoteDesktopModule';
import { ConfigBuilder } from './ConfigBuilder';
import type { Config } from './Config';
type OnRemoteClipboardChanged = (transaction: ClipboardTransaction) => void;
type OnRemoteClipboardChanged = (data: ClipboardData) => void;
type OnRemoteReceivedFormatsList = () => void;
type OnForceClipboardUpdate = () => void;
@@ -65,16 +64,8 @@ export class RemoteDesktopService {
loggingService.info('Web bridge initialized.');
}
constructClipboardTransaction(): ClipboardTransaction {
return this.module.ClipboardTransaction.init();
}
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);
createClipboardData(): ClipboardData {
return this.module.ClipboardData.init();
}
async init(debug: LogType) {
@@ -273,16 +264,16 @@ export class RemoteDesktopService {
/// Triggered by the browser when local clipboard is updated. Clipboard backend should
/// cache the content and send it to the server when it is requested.
onClipboardChanged(transaction: ClipboardTransaction): Promise<void> {
onClipboardChanged(data: ClipboardData): Promise<void> {
const onClipboardChangedPromise = async () => {
await this.session?.on_clipboard_paste(transaction);
await this.session?.on_clipboard_paste(data);
};
return onClipboardChangedPromise();
}
onClipboardChangedEmpty(): Promise<void> {
const onClipboardChangedPromise = async () => {
await this.session?.on_clipboard_paste(this.module.ClipboardTransaction.init());
await this.session?.on_clipboard_paste(this.module.ClipboardData.init());
};
return onClipboardChangedPromise();
}