diff --git a/Cargo.lock b/Cargo.lock index fcac8e5ad..c469eb81d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "clipboard-master" version = "4.0.0-beta.6" -source = "git+https://github.com/rustdesk-org/clipboard-master#7762d74e38db37cfeb6ded88c964b9cdbddfb6db" +source = "git+https://github.com/rustdesk-org/clipboard-master#522fafb65edfe7939cf63fa797f916569ecab595" dependencies = [ "objc", "objc-foundation", diff --git a/src/client.rs b/src/client.rs index ef468b60c..d3e66e73c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,7 @@ #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::clipboard_listener; +use crate::clipboard::clipboard_listener::{self, ClipboardEvent}; use async_trait::async_trait; use bytes::Bytes; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use clipboard_master::CallbackResult; #[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -1942,14 +1940,19 @@ impl Client { break; } match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { - Ok(CallbackResult::Next) => { - handler.check_clipboard(); + Ok(ClipboardEvent::Changed) => { + handler.check_clipboard(true); } - Ok(CallbackResult::Stop) => { + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] + Ok(ClipboardEvent::InitialSelection) => { + // Preserve file startup sync without bypassing text initial-sync settings. + handler.check_clipboard(false); + } + Ok(ClipboardEvent::Stop) => { log::debug!("Clipboard listener stopped"); break; } - Ok(CallbackResult::StopWithError(err)) => { + Ok(ClipboardEvent::StopWithError(err)) => { log::error!("Clipboard listener stopped with error: {}", err); break; } @@ -2051,16 +2054,16 @@ impl ClientClipboardHandler { } } - fn check_clipboard(&mut self) { + fn check_clipboard(&mut self, check_text: bool) { if CLIPBOARD_STATE.lock().unwrap().running { #[cfg(feature = "unix-file-copy-paste")] - if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { - if !urls.is_empty() { - #[cfg(target_os = "macos")] - if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { - return; - } - if self.is_file_required() { + if self.is_file_required() { + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { + if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } match clipboard::platform::unix::serv_files::sync_files(&urls) { Ok(()) => { let msg = crate::clipboard_file::clip_2_msg( @@ -2077,8 +2080,8 @@ impl ClientClipboardHandler { } } - if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { - if self.is_text_required() { + if check_text && self.is_text_required() { + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { self.send_msg(msg, false); } } @@ -4973,6 +4976,8 @@ pub enum Data { RejectInsecureConnection, Login((String, String, String, bool)), Message(Message), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + InitialClipboard(usize, Option), SendFiles((i32, JobType, String, String, i32, bool, bool)), RemoveDirAll((i32, String, bool, bool)), ConfirmDeleteFiles((i32, i32)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b4d0a6c64..4ee6fa1bb 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,5 +1,5 @@ #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, ClipboardSide}; +use crate::clipboard::{clipboard_listener, update_clipboard, ClipboardSide}; #[cfg(not(any(target_os = "ios")))] use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER}; use crate::{ @@ -84,7 +84,12 @@ pub struct Remote { remove_jobs: HashMap, timer: crate::RustDeskInterval, last_update_jobs_status: (Instant, HashMap), + // Set after PeerInfo for this round, not when the transport connects. is_connected: bool, + // Whether the scheduled initial snapshot may still be sent. + // The connection loop clears this when handling its result or a live clipboard update. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + initial_clipboard_pending: bool, first_frame: bool, #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: i32, // used for file clipboard @@ -132,6 +137,8 @@ impl Remote { timer: crate::rustdesk_interval(time::interval(SEC30)), last_update_jobs_status: (Instant::now(), Default::default()), is_connected: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + initial_clipboard_pending: false, first_frame: false, #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: 0, @@ -432,6 +439,11 @@ impl Remote { #[cfg(not(target_os = "ios"))] if self.handler.is_default() && _set_disconnected_ok { + // Other sessions may keep the listener running after this one disconnects. + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); Client::try_stop_clipboard(); } @@ -445,7 +457,7 @@ impl Remote { #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_local_clipboard_msg( - &self, + &mut self, peer: &mut Stream, msg: Option, ) { @@ -458,6 +470,10 @@ impl Remote { } => { self.handler.msgbox(&r#type, &title, &text, ""); } + // File-data responses bypass is_stopping_allowed, but still require login. + _ if !self.is_connected => { + log::debug!("Discarding local file clipboard message before login"); + } _ => { let is_stopping_allowed = clip.is_stopping_allowed(); let server_file_transfer_enabled = @@ -466,9 +482,7 @@ impl Remote { self.handler.lc.read().unwrap().enable_file_copy_paste.v; let view_only = self.handler.lc.read().unwrap().view_only.v; let stop = is_stopping_allowed - && (view_only - || !self.is_connected - || !(server_file_transfer_enabled && file_transfer_enabled)); + && (view_only || !(server_file_transfer_enabled && file_transfer_enabled)); log::debug!( "Process clipboard message from system, view_only: {}, stop: {}, is_stopping_allowed: {}, server_file_transfer_enabled: {}, file_transfer_enabled: {}", view_only, stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled @@ -485,6 +499,10 @@ impl Remote { // to-do: Show msgbox with "Don't show again" option }; log::debug!("Send system clipboard message to remote"); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if matches!(&clip, clipboard::ClipboardFile::FormatList { .. }) { + self.initial_clipboard_pending = false; + } let msg = crate::clipboard_file::clip_2_msg(clip); allow_err!(peer.send(&msg).await); } @@ -625,6 +643,82 @@ impl Remote { self.sent_close_reason = true; } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn start_initial_clipboard_sync(&mut self) { + let peer_info = { + let lc = self.handler.lc.read().unwrap(); + lc.peer_info + .as_ref() + .map(|pi| (pi.version.clone(), pi.platform.clone())) + }; + let Some((peer_version, peer_platform)) = peer_info else { + log::error!("Cannot start initial clipboard sync without peer information"); + return; + }; + + self.initial_clipboard_pending = true; + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + // Clipboard access and encoding must not block the connection loop. + let read_clipboard = move || { + // Capture after readiness, before reading; later changes invalidate this snapshot. + let generation = clipboard_listener::current_generation(); + let msg_out = if permission_config.is_text_clipboard_required() { + crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) + } else { + None + }; + let msg_out = msg_out.filter(|_| permission_config.is_text_clipboard_required()); + // Empty or failed reads must also finish the pending initial-sync attempt. + if let Err(err) = sender.send(Data::InitialClipboard(generation, msg_out)) { + log::debug!("Failed to send initial clipboard result: {}", err); + } + }; + #[cfg(target_os = "linux")] + self.spawn_initial_clipboard_read_after_ready(read_clipboard); + #[cfg(not(target_os = "linux"))] + tokio::task::spawn_blocking(read_clipboard); + } + + // Linux listener creation returns before X11/Wayland has subscribed to changes. + // Capture after readiness so changes during the read can invalidate the snapshot. + // A separate task keeps a stalled backend from blocking the connection loop. + #[cfg(target_os = "linux")] + fn spawn_initial_clipboard_read_after_ready(&self, read_clipboard: impl FnOnce() + Send + 'static) { + // Initial-sync wait budget, not a protocol-defined startup deadline. + const CLIPBOARD_READY_TIMEOUT: Duration = Duration::from_secs(5); + + let sender = self.sender.clone(); + tokio::spawn(async move { + let ready = tokio::select! { + result = time::timeout(CLIPBOARD_READY_TIMEOUT, clipboard_listener::wait_for_ready()) => { + match result { + Ok(ready) => ready, + Err(err) => Err(err.into()), + } + } + _ = sender.closed() => return, + }; + if let Err(err) = ready { + log::error!( + "Failed to wait for clipboard listener readiness (limit {:?}): {}", + CLIPBOARD_READY_TIMEOUT, + err, + ); + let generation = clipboard_listener::current_generation(); + if let Err(err) = sender.send(Data::InitialClipboard(generation, None)) { + log::debug!("Failed to send initial clipboard result: {}", err); + } + return; + } + tokio::task::spawn_blocking(read_clipboard); + }); + } + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { @@ -640,6 +734,27 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Data::InitialClipboard(generation, msg) => { + // A live update supersedes any initial snapshot still being prepared. + if !self.initial_clipboard_pending { + return true; + } + self.initial_clipboard_pending = false; + if !self.handler.is_text_clipboard_required() { + return true; + } + if generation != clipboard_listener::current_generation() { + // A clipboard change or listener restart can invalidate the snapshot + // without sending a live update that supersedes it. + drop(msg); + self.start_initial_clipboard_sync(); + return true; + } + if let Some(msg) = msg { + allow_err!(peer.send(&msg).await); + } + } Data::Message(msg) => { // The Flutter clipboard broadcast is process-wide, so a clipboard can reach this // round's queue before the round has logged in; it is dropped here, on the round @@ -655,6 +770,20 @@ impl Remote { return true; } match &msg.union { + Some(message::Union::Cliprdr(_)) if !self.is_connected => { + log::debug!("Discarding outgoing file clipboard message before login"); + return true; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(message::Union::Clipboard(_)) | Some(message::Union::MultiClipboards(_)) => { + self.initial_clipboard_pending = false; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(message::Union::Cliprdr(clip)) + if matches!(clip.union.as_ref(), Some(cliprdr::Union::FormatList(_))) => + { + self.initial_clipboard_pending = false; + } Some(message::Union::Misc(misc)) => match misc.union { Some(misc::Union::RefreshVideo(_)) => { self.video_threads.iter().for_each(|(_, v)| { @@ -1467,6 +1596,14 @@ impl Remote { #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); if self.handler.is_default() { + // Startup notifications must see the current sessions' clipboard requirements. + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + crate::flutter::update_text_clipboard_required(); + + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); @@ -1488,31 +1625,18 @@ impl Remote { timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok(); } + // `is_connected`` becomes true after the first PeerInfo. + // Reconnects create a new Remote. Refreshes after a generation change + // use a separate entry point and are not blocked by this guard. #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.handler.lc.read().unwrap().sync_init_clipboard.v { - if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( - &peer_version, - &peer_platform, - crate::clipboard::ClipboardSide::Client, - ) { - let sender = self.sender.clone(); - let permission_config = self.handler.get_permission_config(); - tokio::spawn(async move { - if permission_config.is_text_clipboard_required() { - sender.send(Data::Message(msg_out)).ok(); - } - }); - } + if !self.is_connected + && self.handler.is_text_clipboard_required() + && self.handler.lc.read().unwrap().sync_init_clipboard.v + { + self.start_initial_clipboard_sync(); } // to-do: Android, is `sync_init_clipboard` really needed? // https://github.com/rustdesk/rustdesk/discussions/9010 - - #[cfg(feature = "flutter")] - #[cfg(not(target_os = "ios"))] - crate::flutter::update_text_clipboard_required(); - - #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] - crate::flutter::update_file_clipboard_required(); } if self.handler.is_file_transfer() { @@ -1555,7 +1679,10 @@ impl Remote { crate::flutter::send_clipboard_msg_to_other_sessions(msg, session_id); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(vec![cb], ClipboardSide::Client); + { + self.initial_clipboard_pending = false; + update_clipboard(vec![cb], ClipboardSide::Client); + } #[cfg(target_os = "ios")] { let content = if cb.compress { @@ -1590,7 +1717,10 @@ impl Remote { crate::flutter::send_clipboard_msg_to_other_sessions(msg, session_id); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(_mcb.clipboards, ClipboardSide::Client); + { + self.initial_clipboard_pending = false; + update_clipboard(_mcb.clipboards, ClipboardSide::Client); + } #[cfg(target_os = "ios")] { if let Some(cb) = _mcb @@ -2417,6 +2547,10 @@ impl Remote { #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_cliprdr_msg(&mut self, clip: base::message_proto::Cliprdr, _peer: &mut Stream) { + if !self.is_connected { + log::debug!("Discarding incoming file clipboard message before login"); + return; + } log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] if let Some(base::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { @@ -2448,6 +2582,9 @@ impl Remote { }; #[cfg(target_os = "windows")] { + if matches!(&clip, clipboard::ClipboardFile::FormatList { .. }) { + self.initial_clipboard_pending = false; + } let _ = ContextSend::proc(|context| -> ResultType<()> { context .server_clip_file(self.client_conn_id, clip) @@ -2456,6 +2593,10 @@ impl Remote { } #[cfg(feature = "unix-file-copy-paste")] if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if matches!(&clip, clipboard::ClipboardFile::FormatList { .. }) { + self.initial_clipboard_pending = false; + } let mut out_msgs = vec![]; #[cfg(target_os = "macos")] diff --git a/src/clipboard.rs b/src/clipboard.rs index d6d4c4994..9a1bd6058 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -34,8 +34,6 @@ const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; #[cfg(not(target_os = "android"))] lazy_static::lazy_static! { 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())); // For updating in server and getting content in cm. // Clipboard on Linux is "server--clients" mode. // The clipboard content is owned by the server and passed to the clients when requested. @@ -79,8 +77,7 @@ pub fn check_clipboard( side: ClipboardSide, force: bool, ) -> Option { - let (msg, clipboards) = read_clipboard_message(ctx, side, force)?; - *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + let (msg, _) = read_clipboard_message(ctx, side, force)?; Some(msg) } @@ -564,18 +561,25 @@ pub fn get_current_clipboard_msg( peer_platform: &str, side: ClipboardSide, ) -> Option { - let mut multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap(); - if multi_clipboards.clipboards.is_empty() { - let mut ctx = ClipboardContext::new().ok()?; - *multi_clipboards = proto::create_multi_clipboards(ctx.get(side, true).ok()?); + // Clipboard changes can be skipped while synchronization is disabled. + #[cfg(target_os = "linux")] + if !clipboard_listener::is_ready() { + log::error!("Cannot read initial clipboard before the listener is ready"); + return None; } + let ctx = match ClipboardContext::new() { + Ok(ctx) => ctx, + Err(err) => { + log::error!("Failed to create clipboard context for initial sync: {}", err); + return None; + } + }; + let (msg, multi_clipboards) = read_clipboard_message(&mut Some(ctx), side, true)?; if multi_clipboards.clipboards.is_empty() { return None; } if is_support_multi_clipboard(peer_version, peer_platform) { - let mut msg = Message::new(); - msg.set_multi_clipboards(multi_clipboards.clone()); Some(msg) } else { // Find the first text clipboard and send it. @@ -888,10 +892,13 @@ pub fn get_clipboards_msg(client: bool) -> Option { #[cfg(not(target_os = "android"))] pub mod clipboard_listener { use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; + #[cfg(target_os = "linux")] + use hbb_common::tokio::sync::watch; use hbb_common::{bail, log, ResultType}; use std::{ collections::HashMap, io, + sync::atomic::{AtomicUsize, Ordering}, sync::mpsc::{channel, Sender}, sync::{Arc, Mutex}, thread::JoinHandle, @@ -901,15 +908,83 @@ pub mod clipboard_listener { pub static ref CLIPBOARD_LISTENER: Arc> = Default::default(); } + static CLIPBOARD_GENERATION: AtomicUsize = AtomicUsize::new(0); + + pub enum ClipboardEvent { + Changed, + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] + InitialSelection, + Stop, + StopWithError(io::Error), + } + + pub fn current_generation() -> usize { + CLIPBOARD_GENERATION.load(Ordering::SeqCst) + } + + #[cfg(target_os = "linux")] + pub fn is_ready() -> bool { + let listener = CLIPBOARD_LISTENER.lock().unwrap(); + let ready = listener + .ready + .as_ref() + .map(|ready| *ready.borrow()) + .unwrap_or(false); + ready + && listener + .handle + .as_ref() + .map(|(_, h)| !h.is_finished()) + .unwrap_or(false) + } + + #[cfg(target_os = "linux")] + pub async fn wait_for_ready() -> ResultType<()> { + let Some(mut ready) = CLIPBOARD_LISTENER.lock().unwrap().ready.clone() else { + bail!("Clipboard listener has not started"); + }; + let is_ready = *ready.borrow_and_update(); + if !is_ready { + ready.changed().await?; + } + Ok(()) + } + struct Handler { - subscribers: Arc>>>, + subscribers: Arc>>>, + #[cfg(target_os = "linux")] + ready: Option>, } impl ClipboardHandler for Handler { + #[cfg(target_os = "linux")] + fn on_clipboard_ready(&mut self) { + // A restart must invalidate older snapshots, including on X11 or an empty selection. + CLIPBOARD_GENERATION.fetch_add(1, Ordering::SeqCst); + if let Some(tx) = self.ready.take() { + if let Err(err) = tx.send(true) { + log::debug!("Failed to report clipboard listener readiness: {}", err); + } + } + } + fn on_clipboard_change(&mut self) -> CallbackResult { + // Empty or unsupported contents still invalidate an initial snapshot. + CLIPBOARD_GENERATION.fetch_add(1, Ordering::SeqCst); let sub_lock = self.subscribers.lock().unwrap(); for tx in sub_lock.values() { - tx.send(CallbackResult::Next).ok(); + tx.send(ClipboardEvent::Changed).ok(); + } + CallbackResult::Next + } + + fn on_clipboard_initial_selection(&mut self) -> CallbackResult { + // Only client file sync consumes this event; text initial sync is per session. + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] + for tx in self.subscribers.lock().unwrap().values() { + if let Err(err) = tx.send(ClipboardEvent::InitialSelection) { + log::debug!("Failed to notify initial clipboard selection: {}", err); + } } CallbackResult::Next } @@ -918,23 +993,30 @@ pub mod clipboard_listener { let msg = format!("Clipboard listener error: {}", error); let sub_lock = self.subscribers.lock().unwrap(); for tx in sub_lock.values() { - tx.send(CallbackResult::StopWithError(io::Error::new( + tx.send(ClipboardEvent::StopWithError(io::Error::new( io::ErrorKind::Other, msg.clone(), ))) .ok(); } + #[cfg(target_os = "linux")] + if self.ready.is_some() { + // Subscribers must receive the error before a failed startup stops. + return CallbackResult::StopWithError(error); + } CallbackResult::Next } } #[derive(Default)] pub struct ClipboardListener { - subscribers: Arc>>>, + subscribers: Arc>>>, handle: Option<(Shutdown, JoinHandle<()>)>, + #[cfg(target_os = "linux")] + ready: Option>, } - pub fn subscribe(name: String, tx: Sender) -> ResultType<()> { + pub fn subscribe(name: String, tx: Sender) -> ResultType<()> { log::info!("Subscribe clipboard listener: {}", &name); let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); listener_lock @@ -946,8 +1028,12 @@ pub mod clipboard_listener { cleanup_stale_listener(&mut listener_lock); if listener_lock.handle.is_none() { log::info!("Start clipboard listener thread"); + #[cfg(target_os = "linux")] + let (tx_ready, rx_ready) = watch::channel(false); let handler = Handler { subscribers: listener_lock.subscribers.clone(), + #[cfg(target_os = "linux")] + ready: Some(tx_ready), }; let (tx_start_res, rx_start_res) = channel(); let h = start_clipboard_master_thread(handler, tx_start_res); @@ -962,6 +1048,11 @@ pub mod clipboard_listener { } }; listener_lock.handle = Some((shutdown, h)); + #[cfg(target_os = "linux")] + { + // Initial-sync tasks await backend readiness without blocking the connection loop. + listener_lock.ready = Some(rx_ready); + } log::info!("Clipboard listener thread started"); } @@ -993,7 +1084,7 @@ pub mod clipboard_listener { let is_empty = { let mut sub_lock = listener_lock.subscribers.lock().unwrap(); if let Some(tx) = sub_lock.remove(name) { - tx.send(CallbackResult::Stop).ok(); + tx.send(ClipboardEvent::Stop).ok(); } sub_lock.is_empty() }; diff --git a/src/flutter.rs b/src/flutter.rs index cd1f1567d..345ae3929 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1417,6 +1417,7 @@ fn try_send_close_event(event_stream: &Option>) { pub fn update_text_clipboard_required() { let is_required = sessions::get_sessions() .iter() + .filter(|s| s.connection_round_state.lock().unwrap().is_connected()) .any(|s| s.is_default() && s.is_text_clipboard_required()); #[cfg(target_os = "android")] let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required); @@ -1427,6 +1428,7 @@ pub fn update_text_clipboard_required() { pub fn update_file_clipboard_required() { let is_required = sessions::get_sessions() .iter() + .filter(|s| s.connection_round_state.lock().unwrap().is_connected()) .any(|s| s.is_default() && s.is_file_clipboard_required()); Client::set_is_file_clipboard_required(is_required); } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 8fd757c22..54ae9e640 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,6 +1,6 @@ use super::*; #[cfg(not(target_os = "android"))] -use crate::clipboard::clipboard_listener; +use crate::clipboard::clipboard_listener::{self, ClipboardEvent}; #[cfg(not(target_os = "android"))] pub use crate::clipboard::{ClipboardContext, ClipboardSide}; pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; @@ -15,8 +15,6 @@ pub use crate::{ use base::config::keys; #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] use clipboard::platform::unix::fuse::{init_fuse_context, uninit_fuse_context}; -#[cfg(not(target_os = "android"))] -use clipboard_master::CallbackResult; #[cfg(target_os = "android")] use hbb_common::config::option2bool; #[cfg(target_os = "android")] @@ -81,7 +79,7 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { while sp.ok() { match rx_cb_result.recv_timeout(Duration::from_millis(INTERVAL)) { - Ok(CallbackResult::Next) => { + Ok(ClipboardEvent::Changed) => { #[cfg(feature = "unix-file-copy-paste")] if sp.name() == FILE_NAME { handler.check_clipboard_file(); @@ -91,11 +89,14 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { sp.send(msg); } } - Ok(CallbackResult::Stop) => { + // Host startup state must not supersede a client's explicit initial snapshot. + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] + Ok(ClipboardEvent::InitialSelection) => {} + Ok(ClipboardEvent::Stop) => { log::debug!("Clipboard listener stopped"); break; } - Ok(CallbackResult::StopWithError(err)) => { + Ok(ClipboardEvent::StopWithError(err)) => { bail!("Clipboard listener stopped with error: {}", err); } Err(RecvTimeoutError::Timeout) => {} diff --git a/src/server/connection.rs b/src/server/connection.rs index ed4a1112c..03a7665a3 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -914,6 +914,11 @@ impl Connection { if !conn.is_remote() { continue; } + // The CM can send MonitorReady before this connection is authorized. + if !conn.authorized { + log::debug!("Discarding file clipboard message before authorization"); + continue; + } match clip { clipboard::ClipboardFile::Files { files } => { let files = files.into_iter().map(|(f, s)| { @@ -6114,6 +6119,10 @@ impl Connection { #[cfg(feature = "unix-file-copy-paste")] async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { + if !self.authorized { + log::debug!("Discarding file clipboard message before authorization"); + return; + } let is_stopping_allowed = clip.is_stopping_allowed(); let file_transfer_enabled = self.file_transfer_enabled(); let stop = is_stopping_allowed && !file_transfer_enabled;