fix: support ext_data_control_v1 Wayland protocol for clipboard sync

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 <noreply@anthropic.com>
This commit is contained in:
Matvey 2026-03-18 23:56:10 +03:00
parent c0da4a6645
commit 74a3502702
4 changed files with 62 additions and 27 deletions

34
Cargo.lock generated
View file

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

View file

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

View file

@ -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<crate::clipboard::ClipboardContext>,
last_sent_clipboard_sig: Option<Vec<u8>>,
#[cfg(not(feature = "flutter"))]
client_clip_ctx: Option<ClientClipboardContext>,
}
#[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);
}
}

View file

@ -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<ClipboardContext>,
last_clipboard_sig: Option<Vec<u8>>,
#[cfg(target_os = "windows")]
stream: Option<ipc::ConnectionTmpl<parity_tokio_ipc::ConnectionClient>>,
#[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) {