feat: clipboard, multi format (#8672)

* feat: clipboard, multi format

Signed-off-by: fufesou <linlong1266@gmail.com>

* inline

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2024-07-11 00:05:25 +08:00 committed by GitHub
parent e2d217a138
commit 011647511c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 376 additions and 501 deletions

280
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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(())

View File

@ -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;
}
}

View File

@ -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<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
static ref OLD_CLIPBOARD_DATA: Arc<Mutex<crate::clipboard::ClipboardData>> = Default::default();
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = 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<Mutex<crate::clipboard::ClipboardData>> {
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<ClientClipboardContext>) -> Option<UnboundedReceiver<()>> {
fn try_start_clipboard(
client_clip_ctx: Option<ClientClipboardContext>,
) -> Option<UnboundedReceiver<()>> {
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<Message> {
let data = &*OLD_CLIPBOARD_DATA.lock().unwrap();
if data.is_empty() {
None
} else {
Some(data.create_msg())
fn send_msg(_ctx: &Option<ClientClipboardContext>, 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<ClientClipboardContext>, 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<Message> {
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(),

View File

@ -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<T: InvokeUiSession> Remote<T> {
}
}
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<T: InvokeUiSession> Remote<T> {
}
#[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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
}
}
}
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);

View File

@ -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<Mutex<ClipboardData>> = Default::default();
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
// cache the clipboard msg
static ref LAST_MULTI_CLIPBOARDS: Arc<Mutex<MultiClipboards>> = 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<x11_clipboard::Clipboard> =
once_cell::sync::OnceCell::new();
@ -126,58 +134,40 @@ impl ClipboardContext {
}
}
pub fn check_clipboard(
ctx: &mut Option<ClipboardContext>,
old: Option<Arc<Mutex<ClipboardData>>>,
) -> Option<Message> {
pub fn check_clipboard(ctx: &mut Option<ClipboardContext>, side: ClipboardSide) -> Option<Message> {
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<Arc<Mutex<ClipboardData>>>) {
let content = ClipboardData::from_msg(clipboard);
if content.is_empty() {
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, 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<Arc<Mutex<ClipboardData>>
}
}
pub fn update_clipboard(clipboard: Clipboard, old: Option<Arc<Mutex<ClipboardData>>>) {
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, 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"<svg") to check if it is svg image
// because svg image may starts with other bytes
let img = if clipboard.height == 0 && clipboard.width as usize == FAKE_SVG_WIDTH {
arboard::ImageData::svg(std::str::from_utf8(&data).unwrap_or_default())
} else {
arboard::ImageData::rgba(clipboard.width as _, clipboard.height as _, data.into())
};
ClipboardData::Image(img, 0)
} else {
if let Ok(content) = String::from_utf8(data) {
ClipboardData::Text(content)
} else {
ClipboardData::Empty
}
}
}
pub fn create_msg(&self) -> 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<AtomicU64>, u64),
shutdown: Option<Shutdown>,
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<ClipboardData> {
pub fn get(&mut self, side: ClipboardSide) -> ResultType<Vec<ClipboardData>> {
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<Message> {
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<u8> {
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::<Vec<u8>>()
};
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<Clipboard> {
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<ClipboardData>) -> MultiClipboards {
MultiClipboards {
clipboards: vec_data
.into_iter()
.filter_map(clipboard_data_to_proto)
.collect(),
..Default::default()
}
}
fn from_clipboard(clipboard: Clipboard) -> Option<ClipboardData> {
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<Clipboard>) -> Vec<ClipboardData> {
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<Message> {
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)
}
}

View File

@ -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()));
}
}

View File

@ -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(())
})?;

View File

@ -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)) =>