diff --git a/Cargo.lock b/Cargo.lock index cfc0f8560..bb905da1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6" +source = "git+https://github.com/rustdesk-org/arboard#58d2b6a5c3af3d1aa15481ddff1bf70551f35a48" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,24 +234,11 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", - "resvg", "windows-sys 0.48.0", "wl-clipboard-rs", "x11rb 0.13.1", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "async-broadcast" version = "0.5.1" @@ -1000,9 +987,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -1526,12 +1513,6 @@ dependencies = [ "dasp_sample", ] -[[package]] -name = "data-url" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" - [[package]] name = "dbus" version = "0.9.7" @@ -1691,7 +1672,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.4", ] [[package]] @@ -2081,12 +2062,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "flume" version = "0.11.0" @@ -2143,29 +2118,6 @@ dependencies = [ "libm", ] -[[package]] -name = "fontconfig-parser" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" -dependencies = [ - "roxmltree 0.19.0", -] - -[[package]] -name = "fontdb" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - [[package]] name = "foreign-types" version = "0.3.2" @@ -3213,12 +3165,6 @@ dependencies = [ "tiff", ] -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - [[package]] name = "impersonate_system" version = "0.1.0" @@ -3462,16 +3408,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kurbo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" -dependencies = [ - "arrayvec", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3777,15 +3713,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.6.5" @@ -4732,15 +4659,9 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher 0.2.3", + "siphasher", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.5" @@ -5414,31 +5335,6 @@ dependencies = [ "winreg 0.50.0", ] -[[package]] -name = "resvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" -dependencies = [ - "gif", - "jpeg-decoder", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - -[[package]] -name = "rgb" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" -dependencies = [ - "bytemuck", -] - [[package]] name = "ring" version = "0.17.8" @@ -5463,18 +5359,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "roxmltree" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rpassword" version = "2.1.0" @@ -5853,22 +5737,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rustybuzz" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" -dependencies = [ - "bitflags 2.6.0", - "bytemuck", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "ryu" version = "1.0.18" @@ -6159,27 +6027,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - [[package]] name = "siphasher" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -6189,15 +6042,6 @@ dependencies = [ "autocfg 1.3.0", ] -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -6268,15 +6112,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - [[package]] name = "strsim" version = "0.8.0" @@ -6338,16 +6173,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "svgtypes" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" -dependencies = [ - "kurbo", - "siphasher 1.0.1", -] - [[package]] name = "syn" version = "0.15.44" @@ -6687,32 +6512,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if 1.0.0", - "log", - "png", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinyvec" version = "1.6.1" @@ -7004,12 +6803,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "typenum" version = "1.17.0" @@ -7082,18 +6875,6 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" - -[[package]] -name = "unicode-ccc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -7109,30 +6890,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode-script" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" - [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.1.13" @@ -7195,33 +6958,6 @@ dependencies = [ "log", ] -[[package]] -name = "usvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.20.0", - "rustybuzz", - "simplecss", - "siphasher 1.0.1", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - [[package]] name = "utf16string" version = "0.2.0" @@ -8220,12 +7956,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "zbus" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index 0c54897e1..ef1da853a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,7 @@ enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } ctrlc = "3.2" # arboard = { version = "3.4.0", features = ["wayland-data-control"] } -arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] } +arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } system_shutdown = "4.0" diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 2be4ce809..5db271129 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::{CliprdrError, CliprdrServiceContext}; #[cfg(target_os = "windows")] @@ -63,8 +64,10 @@ pub fn create_cliprdr_context( return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); } +#[cfg(any(target_os = "linux", target_os = "macos"))] struct DummyCliprdrContext {} +#[cfg(any(target_os = "linux", target_os = "macos"))] impl CliprdrServiceContext for DummyCliprdrContext { fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { Ok(()) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index f346a7228..d7a8cf0a7 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -81,6 +81,7 @@ message LoginRequest { uint64 session_id = 10; string version = 11; OSLogin os_login = 12; + string my_platform = 13; } message Auth2FA { @@ -315,13 +316,25 @@ message Hash { string challenge = 2; } +enum ClipboardFormat { + Text = 0; + Rtf = 1; + Html = 2; + ImageRgba = 21; + ImagePng = 22; + ImageSvg = 23; +} + message Clipboard { bool compress = 1; bytes content = 2; int32 width = 3; int32 height = 4; + ClipboardFormat format = 5; } +message MultiClipboards { repeated Clipboard clipboards = 1; } + enum FileType { Dir = 0; DirLink = 2; @@ -816,5 +829,6 @@ message Message { PeerInfo peer_info = 25; PointerDeviceEvent pointer_device_event = 26; Auth2FA auth_2fa = 27; + MultiClipboards multi_clipboards = 28; } } diff --git a/src/client.rs b/src/client.rs index 4c3f53b03..4357c0074 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,7 +65,7 @@ use crate::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL}; +use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; @@ -136,18 +136,11 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); - static ref OLD_CLIPBOARD_DATA: Arc> = Default::default(); static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_old_clipboard_text() -> Arc> { - OLD_CLIPBOARD_DATA.clone() -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_key_state(key: enigo::Key) -> bool { use enigo::KeyboardControllable; @@ -719,7 +712,9 @@ impl Client { // // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn try_start_clipboard(_ctx: Option) -> Option> { + fn try_start_clipboard( + client_clip_ctx: Option, + ) -> Option> { let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; @@ -740,15 +735,8 @@ impl Client { continue; } - if let Some(msg) = check_clipboard(&mut ctx, Some(OLD_CLIPBOARD_DATA.clone())) { - #[cfg(feature = "flutter")] - crate::flutter::send_text_clipboard_msg(msg); - #[cfg(not(feature = "flutter"))] - if let Some(ctx) = &_ctx { - if ctx.cfg.is_text_clipboard_required() { - let _ = ctx.tx.send(Data::Message(msg)); - } - } + if let Some(msg) = check_clipboard(&mut ctx, ClipboardSide::Client) { + Self::send_msg(&client_clip_ctx, msg); } if !is_sent { @@ -765,15 +753,39 @@ impl Client { } #[inline] + #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn get_current_clipboard_msg() -> Option { - let data = &*OLD_CLIPBOARD_DATA.lock().unwrap(); - if data.is_empty() { - None - } else { - Some(data.create_msg()) + fn send_msg(_ctx: &Option, msg: Message) { + crate::flutter::send_text_clipboard_msg(msg); + } + + #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn send_msg(ctx: &Option, msg: Message) { + if let Some(ctx) = &ctx { + if ctx.cfg.is_text_clipboard_required() { + if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() { + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &pi.version, + &pi.platform, + multi_clipboards, + ) { + let _ = ctx.tx.send(Data::Message(msg_out)); + return; + } + } + } + let _ = ctx.tx.send(Data::Message(msg)); + } } } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn get_current_clipboard_msg(peer_version: &str, peer_platform: &str) -> Option { + crate::clipboard::get_cache_msg(peer_version, peer_platform) + } } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -2023,11 +2035,16 @@ impl LoginConfigHandler { if display_name.is_empty() { display_name = crate::username(); } + #[cfg(not(target_os = "android"))] + let my_platform = whoami::platform().to_string(); + #[cfg(target_os = "android")] + let my_platform = "Android".into(); let mut lr = LoginRequest { username: pure_id, password: password.into(), my_id, my_name: display_name, + my_platform, option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 19074bbd1..ac630cda9 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -41,7 +41,7 @@ use crate::client::{ new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL}; +use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; #[cfg(not(any(target_os = "ios")))] @@ -1118,6 +1118,8 @@ impl Remote { } } Some(login_response::Union::PeerInfo(pi)) => { + let peer_version = pi.version.clone(); + let peer_platform = pi.platform.clone(); self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { @@ -1139,7 +1141,9 @@ impl Remote { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg_out) = Client::get_current_clipboard_msg() { + if let Some(msg_out) = + Client::get_current_clipboard_msg(&peer_version, &peer_platform) + { let sender = self.sender.clone(); let permission_config = self.handler.get_permission_config(); tokio::spawn(async move { @@ -1180,7 +1184,7 @@ impl Remote { Some(message::Union::Clipboard(cb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, Some(crate::client::get_old_clipboard_text())); + update_clipboard(vec![cb], ClipboardSide::Client); #[cfg(any(target_os = "android", target_os = "ios"))] { let content = if cb.compress { @@ -1194,6 +1198,12 @@ impl Remote { } } } + Some(message::Union::MultiClipboards(_mcb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard.v { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(_mcb.clipboards, ClipboardSide::Client); + } + } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] Some(message::Union::Cliprdr(clip)) => { self.handle_cliprdr_msg(clip); diff --git a/src/clipboard.rs b/src/clipboard.rs index 0db9f59c1..e0cf85afa 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -3,24 +3,32 @@ use std::sync::{ Arc, Mutex, }; +use arboard::{ClipboardData, ClipboardFormat}; use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; -use hbb_common::{ - allow_err, - compress::{compress as compress_func, decompress}, - log, - message_proto::*, - ResultType, -}; +use hbb_common::{log, message_proto::*, ResultType}; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; -const FAKE_SVG_WIDTH: usize = 999999; + +// This format is used to store the flag in the clipboard. +const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; lazy_static::lazy_static! { - pub static ref CONTENT: Arc> = Default::default(); static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); + // cache the clipboard msg + static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); } +const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ + ClipboardFormat::Text, + ClipboardFormat::Html, + ClipboardFormat::Rtf, + ClipboardFormat::ImageRgba, + ClipboardFormat::ImagePng, + ClipboardFormat::ImageSvg, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), +]; + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] static X11_CLIPBOARD: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -126,58 +134,40 @@ impl ClipboardContext { } } -pub fn check_clipboard( - ctx: &mut Option, - old: Option>>, -) -> Option { +pub fn check_clipboard(ctx: &mut Option, side: ClipboardSide) -> Option { if ctx.is_none() { *ctx = ClipboardContext::new(true).ok(); } let ctx2 = ctx.as_mut()?; - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - let content = ctx2.get(); + let content = ctx2.get(side); if let Ok(content) = content { if !content.is_empty() { - if matches!(content, ClipboardData::Text(_)) { - // Skip the text if the last content is image-svg/html - if ctx2.is_last_plain { - return None; - } - } - - let changed = content != *old.lock().unwrap(); - if changed { - log::info!("{} update found on {}", CLIPBOARD_NAME, side); - let msg = content.create_msg(); - *old.lock().unwrap() = content; - return Some(msg); - } + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + return Some(msg); } } None } -fn update_clipboard_(clipboard: Clipboard, old: Option>>) { - let content = ClipboardData::from_msg(clipboard); - if content.is_empty() { +fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { + let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); + if to_update_data.is_empty() { return; } match ClipboardContext::new(false) { Ok(mut ctx) => { - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old + to_update_data.push(ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + ))); + if let Err(e) = ctx.set(&to_update_data) { + log::debug!("Failed to set clipboard: {}", e); } else { - CONTENT.clone() - }; - allow_err!(ctx.set(&content)); - *old.lock().unwrap() = content; - log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } } Err(err) => { log::error!("Failed to create clipboard context: {}", err); @@ -185,137 +175,17 @@ fn update_clipboard_(clipboard: Clipboard, old: Option> } } -pub fn update_clipboard(clipboard: Clipboard, old: Option>>) { +pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { std::thread::spawn(move || { - update_clipboard_(clipboard, old); + update_clipboard_(multi_clipboards, side); }); } -#[derive(Clone)] -pub enum ClipboardData { - Text(String), - Image(arboard::ImageData<'static>, u64), - Empty, -} - -impl Default for ClipboardData { - fn default() -> Self { - ClipboardData::Empty - } -} - -impl ClipboardData { - fn image(image: arboard::ImageData<'static>) -> ClipboardData { - let hash = 0; - /* - use std::hash::{DefaultHasher, Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - image.bytes.hash(&mut hasher); - let hash = hasher.finish(); - */ - ClipboardData::Image(image, hash) - } - - pub fn is_empty(&self) -> bool { - match self { - ClipboardData::Empty => true, - ClipboardData::Text(s) => s.is_empty(), - ClipboardData::Image(a, _) => a.bytes().is_empty(), - } - } - - fn from_msg(clipboard: Clipboard) -> Self { - let is_image = clipboard.width > 0; - let data = if clipboard.compress { - decompress(&clipboard.content) - } else { - clipboard.content.into() - }; - if is_image { - // We cannot use data.start_with(b" Message { - let mut msg = Message::new(); - - match self { - ClipboardData::Text(s) => { - let compressed = compress_func(s.as_bytes()); - let compress = compressed.len() < s.as_bytes().len(); - let content = if compress { - compressed - } else { - s.clone().into_bytes() - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - ..Default::default() - }); - } - ClipboardData::Image(a, _) => { - let compressed = compress_func(&a.bytes()); - let compress = compressed.len() < a.bytes().len(); - let content = if compress { - compressed - } else { - a.bytes().to_vec() - }; - let (w, h) = match a { - arboard::ImageData::Rgba(a) => (a.width, a.height), - arboard::ImageData::Svg(_) => (FAKE_SVG_WIDTH as _, 0 as _), - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - width: w as _, - height: h as _, - ..Default::default() - }); - } - _ => {} - } - msg - } -} - -impl PartialEq for ClipboardData { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (ClipboardData::Text(a), ClipboardData::Text(b)) => a == b, - (ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => match (a, b) { - (arboard::ImageData::Rgba(a), arboard::ImageData::Rgba(b)) => { - a.width == b.width && a.height == b.height && a.bytes == b.bytes - } - (arboard::ImageData::Svg(a), arboard::ImageData::Svg(b)) => a == b, - _ => false, - }, - (ClipboardData::Empty, ClipboardData::Empty) => true, - _ => false, - } - } -} - #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, counter: (Arc, u64), shutdown: Option, - is_last_plain: bool, } #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] @@ -392,7 +262,6 @@ impl ClipboardContext { inner: board, counter: (change_count, 0), shutdown, - is_last_plain: false, }) } @@ -402,35 +271,29 @@ impl ClipboardContext { self.counter.0.load(Ordering::SeqCst) } - pub fn get(&mut self) -> ResultType { + pub fn get(&mut self, side: ClipboardSide) -> ResultType> { let cn = self.change_count(); let _lock = ARBOARD_MTX.lock().unwrap(); - // only for image for the time being, - // because I do not want to change behavior of text clipboard for the time being if cn != self.counter.1 { - self.is_last_plain = false; self.counter.1 = cn; - if let Ok(image) = self.inner.get_image() { - // Both text and image svg may be set by some applications - // But we only want to send the svg content. - // - // We can't call `get_text()` and store current text in `old` in outer scope, - // because it may be updated later than svg. - // Then the text will still be sent and replace the image svg content. - self.is_last_plain = matches!(image, arboard::ImageData::Svg(_)); - return Ok(ClipboardData::image(image)); + let data = self.inner.get_formats(SUPPORTED_FORMATS)?; + if !data.is_empty() { + for c in data.iter() { + if let ClipboardData::Special((_, d)) = c { + if side.is_owner(d) { + return Ok(vec![]); + } + } + } } + return Ok(data); } - Ok(ClipboardData::Text(self.inner.get_text()?)) + Ok(vec![]) } - fn set(&mut self, data: &ClipboardData) -> ResultType<()> { + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { let _lock = ARBOARD_MTX.lock().unwrap(); - match data { - ClipboardData::Text(s) => self.inner.set_text(s)?, - ClipboardData::Image(a, _) => self.inner.set_image(a.clone())?, - _ => {} - } + self.inner.set_formats(data)?; Ok(()) } } @@ -442,3 +305,210 @@ impl Drop for ClipboardContext { } } } + +pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { + use hbb_common::get_version_number; + get_version_number(peer_version) >= get_version_number("1.2.7") + && !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform) +} + +pub fn get_cache_msg(peer_version: &str, peer_platform: &str) -> Option { + let multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap().clone(); + if multi_clipboards.clipboards.is_empty() { + return None; + } + + let mut msg = Message::new(); + if is_support_multi_clipboard(peer_version, peer_platform) { + msg.set_multi_clipboards(multi_clipboards); + } else { + for clipboard in multi_clipboards.clipboards.iter() { + if clipboard.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text) + { + msg.set_clipboard(clipboard.clone()); + break; + } + } + } + Some(msg) +} + +pub fn reset_cache() { + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = MultiClipboards::new(); +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ClipboardSide { + Host, + Client, +} + +impl ClipboardSide { + // 01: the clipboard is owned by the host + // 10: the clipboard is owned by the client + fn get_owner_data(&self) -> Vec { + match self { + ClipboardSide::Host => vec![0b01], + ClipboardSide::Client => vec![0b10], + } + } + + fn is_owner(&self, data: &[u8]) -> bool { + if data.len() == 0 { + return false; + } + match self { + ClipboardSide::Host => data[0] & 0b01 == 0b01, + ClipboardSide::Client => data[0] & 0b10 == 0b10, + } + } +} + +impl std::fmt::Display for ClipboardSide { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClipboardSide::Host => write!(f, "host"), + ClipboardSide::Client => write!(f, "client"), + } + } +} + +pub use proto::get_msg_if_not_support_multi_clip; +mod proto { + use arboard::ClipboardData; + use hbb_common::{ + compress::{compress as compress_func, decompress}, + message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}, + }; + + fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard { + let compressed = compress_func(s.as_bytes()); + let compress = compressed.len() < s.as_bytes().len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: format.into(), + ..Default::default() + } + } + + fn image_to_proto(a: arboard::ImageData) -> Clipboard { + match &a { + arboard::ImageData::Rgba(rgba) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + width: rgba.width as _, + height: rgba.height as _, + format: ClipboardFormat::ImageRgba.into(), + ..Default::default() + } + } + arboard::ImageData::Png(png) => Clipboard { + compress: false, + content: png.to_owned().to_vec().into(), + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }, + arboard::ImageData::Svg(_) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::ImageSvg.into(), + ..Default::default() + } + } + } + } + + fn clipboard_data_to_proto(data: ClipboardData) -> Option { + let d = match data { + ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), + ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), + ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), + ClipboardData::Image(a) => image_to_proto(a), + _ => return None, + }; + Some(d) + } + + pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { + MultiClipboards { + clipboards: vec_data + .into_iter() + .filter_map(clipboard_data_to_proto) + .collect(), + ..Default::default() + } + } + + fn from_clipboard(clipboard: Clipboard) -> Option { + let data = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content.into() + }; + match clipboard.format.enum_value() { + Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text), + Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf), + Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html), + Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba( + clipboard.width as _, + clipboard.height as _, + data.into(), + ))), + Ok(ClipboardFormat::ImagePng) => { + Some(ClipboardData::Image(arboard::ImageData::png(data.into()))) + } + Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( + std::str::from_utf8(&data).unwrap_or_default(), + ))), + _ => None, + } + } + + pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { + multi_clipboards + .into_iter() + .filter_map(from_clipboard) + .collect() + } + + pub fn get_msg_if_not_support_multi_clip( + version: &str, + platform: &str, + multi_clipboards: &MultiClipboards, + ) -> Option { + if crate::clipboard::is_support_multi_clipboard(version, platform) { + return None; + } + let mut msg = Message::new(); + // Find the first text clipboard and send it. + for clipboard in multi_clipboards.clipboards.iter() { + if clipboard.format.enum_value() == Ok(ClipboardFormat::Text) { + msg.set_clipboard(clipboard.clone()); + break; + } + } + Some(msg) + } +} diff --git a/src/flutter.rs b/src/flutter.rs index d1e44008b..773abb3f2 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1256,6 +1256,19 @@ pub fn update_text_clipboard_required() { pub fn send_text_clipboard_msg(msg: Message) { for s in sessions::get_sessions() { if s.is_text_clipboard_required() { + // Check if the client supports multi clipboards + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + let version = s.ui_handler.peer_info.read().unwrap().version.clone(); + let platform = s.ui_handler.peer_info.read().unwrap().platform.clone(); + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &version, + &platform, + multi_clipboards, + ) { + s.send(Data::Message(msg_out)); + continue; + } + } s.send(Data::Message(msg.clone())); } } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index eeeea4999..7faa2de3d 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,7 +1,7 @@ use super::*; pub use crate::clipboard::{ - check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, - CONTENT, + check_clipboard, get_cache_msg, ClipboardContext, ClipboardSide, + CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, }; #[derive(Default)] @@ -11,7 +11,7 @@ struct State { impl super::service::Reset for State { fn reset(&mut self) { - *CONTENT.lock().unwrap() = Default::default(); + crate::clipboard::reset_cache(); self.ctx = None; } @@ -34,14 +34,14 @@ pub fn new() -> GenericService { } fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { - if let Some(msg) = check_clipboard(&mut state.ctx, None) { + if let Some(msg) = check_clipboard(&mut state.ctx, ClipboardSide::Host) { sp.send(msg); } sp.snapshot(|sps| { - let data = CONTENT.lock().unwrap().clone(); - if !data.is_empty() { - let msg_out = data.create_msg(); - sps.send_shared(Arc::new(msg_out)); + // Just create a message with multi clipboards here + // The actual peer version and peer platform will be checked again before sending. + if let Some(msg) = get_cache_msg("1.2.7", "Windows") { + sps.send_shared(Arc::new(msg)); } Ok(()) })?; diff --git a/src/server/connection.rs b/src/server/connection.rs index cc43650e0..26533fcf4 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,6 +1,6 @@ use super::{input_service::*, *}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::update_clipboard; +use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(target_os = "android")] @@ -685,8 +685,19 @@ impl Connection { msg = Arc::new(new_msg); } } + Some(message::Union::MultiClipboards(_multi_clipboards)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { + if let Err(err) = conn.stream.send(&msg_out).await { + conn.on_close(&err.to_string(), false).await; + break; + } + continue; + } + } _ => {} } + let msg: &Message = &msg; if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; @@ -2053,7 +2064,14 @@ impl Connection { { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard { - update_clipboard(_cb, None); + update_clipboard(vec![_cb], ClipboardSide::Host); + } + } + Some(message::Union::MultiClipboards(_mcb)) => + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(_mcb.clipboards, ClipboardSide::Host); } } Some(message::Union::Cliprdr(_clip)) =>