From 74a3502702e3bc08fcf50a260a033ddb5923b77c Mon Sep 17 00:00:00 2001 From: Matvey Date: Wed, 18 Mar 2026 23:56:10 +0300 Subject: [PATCH] fix: support ext_data_control_v1 Wayland protocol for clipboard sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KDE Plasma 6.5+ dropped the zwlr_data_control_v1 protocol in favor of the standardized ext_data_control_v1 protocol. This broke clipboard sync between Linux Wayland hosts and remote peers — the first copy/paste works but subsequent clipboard changes are never forwarded. Root cause: Both clipboard-master (change detection) and wl-clipboard-rs (clipboard read/write) only supported zwlr_data_control_v1. On compositors that only expose ext_data_control_v1 (Plasma 6.5+), clipboard-master fell back to X11 mode and wl-clipboard-rs failed silently. Changes: - Update wl-clipboard-rs 0.9.0 -> 0.9.3 (adds ext_data_control_v1 support with automatic fallback to zwlr) - Update wayland-protocols-wlr 0.3.3 -> 0.3.9 - Switch clipboard-master to a fork that supports both ext_data_control_v1 (preferred) and zwlr_data_control_v1 (fallback) for change detection - Add polling fallback on Linux Wayland: on recv timeout, re-read clipboard to catch changes that the event-driven listener may have missed - Add protobuf-level deduplication to avoid sending identical clipboard content on each polling cycle Fixes #13338 Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 34 ++++++++++++--------------------- Cargo.toml | 2 +- src/client.rs | 23 ++++++++++++++++++++-- src/server/clipboard_service.rs | 30 +++++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index febfd6b17..e64631168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1324,7 +1324,7 @@ dependencies = [ [[package]] name = "clipboard-master" version = "4.0.0-beta.6" -source = "git+https://github.com/rustdesk-org/clipboard-master#ddc39f00a6211959489ae683aa6ae6eedf03a809" +source = "git+https://github.com/night-hood/clipboard-master?branch=fix/ext-data-control-v1#c2e9f129cfd9179ace1a340d193aaa9556c2ed71" dependencies = [ "objc", "objc-foundation", @@ -2282,7 +2282,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2694,7 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -6673,7 +6673,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -7457,7 +7457,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -7514,7 +7514,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -9733,9 +9733,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ "bitflags 2.9.1", "wayland-backend", @@ -10408,15 +10408,6 @@ dependencies = [ "windows-targets 0.53.5", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" @@ -10838,16 +10829,15 @@ dependencies = [ [[package]] name = "wl-clipboard-rs" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" dependencies = [ "libc", "log", "os_pipe", - "rustix 0.38.34", - "tempfile", - "thiserror 1.0.61", + "rustix 1.1.2", + "thiserror 2.0.17", "tree_magic_mini", "wayland-backend", "wayland-client", diff --git a/Cargo.toml b/Cargo.toml index 3961e9d0b..ed90144fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ clipboard = { path = "libs/clipboard" } ctrlc = "3.2" # arboard = { version = "3.4", features = ["wayland-data-control"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } -clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } +clipboard-master = { git = "https://github.com/night-hood/clipboard-master", branch = "fix/ext-data-control-v1" } portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" } system_shutdown = "4.0" diff --git a/src/client.rs b/src/client.rs index 527f65a12..16cf6f328 100644 --- a/src/client.rs +++ b/src/client.rs @@ -986,6 +986,7 @@ impl Client { std::thread::spawn(move || { let mut handler = ClientClipboardHandler { ctx: None, + last_sent_clipboard_sig: None, #[cfg(not(feature = "flutter"))] client_clip_ctx: _client_clip_ctx, }; @@ -1007,7 +1008,12 @@ impl Client { log::error!("Clipboard listener stopped with error: {}", err); break; } - Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Timeout) => { + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + handler.check_clipboard(); + } + } Err(RecvTimeoutError::Disconnected) => { log::error!("Clipboard listener disconnected"); break; @@ -1070,12 +1076,25 @@ impl ClipboardState { #[cfg(not(any(target_os = "android", target_os = "ios")))] struct ClientClipboardHandler { ctx: Option, + last_sent_clipboard_sig: Option>, #[cfg(not(feature = "flutter"))] client_clip_ctx: Option, } #[cfg(not(any(target_os = "android", target_os = "ios")))] impl ClientClipboardHandler { + fn should_send_msg(&mut self, msg: &Message) -> bool { + use hbb_common::protobuf::Message as _; + let Ok(sig) = msg.write_to_bytes() else { + return true; + }; + if self.last_sent_clipboard_sig.as_ref() == Some(&sig) { + return false; + } + self.last_sent_clipboard_sig = Some(sig); + true + } + fn is_text_required(&self) -> bool { #[cfg(feature = "flutter")] { @@ -1132,7 +1151,7 @@ impl ClientClipboardHandler { } if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { - if self.is_text_required() { + if self.is_text_required() && self.should_send_msg(&msg) { self.send_msg(msg, false); } } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 1d2f0a3fb..e9168d106 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -26,6 +26,8 @@ use std::{ }; #[cfg(windows)] use tokio::runtime::Runtime; +#[cfg(not(target_os = "android"))] +use hbb_common::protobuf::Message as _; #[cfg(target_os = "android")] static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); @@ -33,6 +35,7 @@ static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); #[cfg(not(target_os = "android"))] struct Handler { ctx: Option, + last_clipboard_sig: Option>, #[cfg(target_os = "windows")] stream: Option>, #[cfg(target_os = "windows")] @@ -71,6 +74,7 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { clipboard_listener::subscribe(sp.name(), tx_cb_result)?; let mut handler = Handler { ctx, + last_clipboard_sig: None, #[cfg(target_os = "windows")] stream: None, #[cfg(target_os = "windows")] @@ -86,7 +90,9 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { continue; } if let Some(msg) = handler.get_clipboard_msg() { - sp.send(msg); + if handler.should_send_clipboard_msg(&msg) { + sp.send(msg); + } } } Ok(CallbackResult::Stop) => { @@ -96,7 +102,16 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { Ok(CallbackResult::StopWithError(err)) => { bail!("Clipboard listener stopped with error: {}", err); } - Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Timeout) => { + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && sp.name() == NAME { + if let Some(msg) = handler.get_clipboard_msg() { + if handler.should_send_clipboard_msg(&msg) { + sp.send(msg); + } + } + } + } Err(RecvTimeoutError::Disconnected) => { log::error!("Clipboard listener disconnected"); break; @@ -111,6 +126,17 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { #[cfg(not(target_os = "android"))] impl Handler { + fn should_send_clipboard_msg(&mut self, msg: &Message) -> bool { + let Ok(sig) = msg.write_to_bytes() else { + return true; + }; + if self.last_clipboard_sig.as_ref() == Some(&sig) { + return false; + } + self.last_clipboard_sig = Some(sig); + true + } + #[cfg(feature = "unix-file-copy-paste")] fn check_clipboard_file(&mut self) { if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) {