From 001848bf2fcfcc8eef2268eade1f7186ccd8aa58 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 26 Jun 2026 16:17:44 +0800 Subject: [PATCH] fix(fuse): umount (#15426) * fix(clipboard): clean up stale Linux FUSE mounts Recover Linux file clipboard FUSE mount points before remounting and stop treating a cached context as valid when the mount has already gone away. This fixes the desktop file manager copy failure that shows dialogs such as "Error while copying a" and "There was an error copying the file into xxx". Signed-off-by: fufesou * fix(clipboard): fuse, reduce dups Signed-off-by: fufesou * fix: clear Linux file clipboard before unmounting FUSE Ensure Linux client teardown clears RustDesk file clipboard URLs while the FUSE context is still available. Also prefer fusermount before umount to avoid noisy unprivileged teardown attempts. Signed-off-by: fufesou * fix(clipboard): return and log errors Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/clipboard/src/platform/unix/fuse/cs.rs | 13 +- libs/clipboard/src/platform/unix/fuse/mod.rs | 329 ++++++++++++++++-- .../clipboard/src/platform/unix/local_file.rs | 58 ++- src/client.rs | 18 +- src/client/io_loop.rs | 2 + src/clipboard.rs | 71 ++-- src/clipboard_file.rs | 32 +- src/flutter.rs | 10 + src/ui_session_interface.rs | 4 + 9 files changed, 444 insertions(+), 93 deletions(-) diff --git a/libs/clipboard/src/platform/unix/fuse/cs.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs index fa1dea71d..307c2fb16 100644 --- a/libs/clipboard/src/platform/unix/fuse/cs.rs +++ b/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -533,7 +533,7 @@ impl FuseServer { offset: i64, size: u32, ) -> Result, std::io::Error> { - // todo: async and concurrent read, generate stream_id per request + let request_stream_id = rand::random(); let cb_requested = unsafe { // convert `size` from u32 to i32 // yet with same bit representation @@ -543,7 +543,7 @@ impl FuseServer { let (n_position_high, n_position_low) = ((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32); let request = ClipboardFile::FileContentsRequest { - stream_id: node.stream_id, + stream_id: request_stream_id, list_index: node.index as i32, dw_flags: 2, n_position_low, @@ -573,7 +573,7 @@ impl FuseServer { stream_id, requested_data, } => { - if stream_id != node.stream_id { + if stream_id != request_stream_id { log::debug!("stream id mismatch, ignore"); continue; } @@ -611,11 +611,6 @@ struct FuseNode { /// connection id pub conn_id: i32, - // todo: use stream_id to identify a FileContents request-reply - // instead of a whole file - /// stream id - pub stream_id: i32, - /// file index in peer's file list /// NOTE: /// it is NOT the same as inode, this is the index in the file list @@ -639,7 +634,6 @@ impl FuseNode { pub fn from_description(inode: Inode, desc: FileDescription) -> Self { Self { conn_id: desc.conn_id, - stream_id: rand::random(), index: inode as usize - 2, name: desc .name @@ -656,7 +650,6 @@ impl FuseNode { pub fn new_root() -> Self { Self { conn_id: 0, - stream_id: rand::random(), index: 0, name: String::from("/"), parent: None, diff --git a/libs/clipboard/src/platform/unix/fuse/mod.rs b/libs/clipboard/src/platform/unix/fuse/mod.rs index f0acef6da..a1ad2a4a5 100644 --- a/libs/clipboard/src/platform/unix/fuse/mod.rs +++ b/libs/clipboard/src/platform/unix/fuse/mod.rs @@ -7,7 +7,8 @@ use fuser::MountOption; use hbb_common::{config::Config, log}; use parking_lot::Mutex; use std::{ - path::PathBuf, + io, + path::{Path, PathBuf}, sync::{mpsc::Sender, Arc}, time::Duration, }; @@ -31,6 +32,14 @@ lazy_static::lazy_static! { static FUSE_TIMEOUT: Duration = Duration::from_secs(3); +#[derive(Debug, PartialEq, Eq)] +enum MountPointState { + HealthyMount, + NotMounted, + StaleMount, + Unknown, +} + fn fuse_mount_point(name: &str) -> String { let mut path = PathBuf::from(Config::ipc_path("")); path.pop(); @@ -60,8 +69,27 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { } else { FUSE_CONTEXT_SERVER.lock() }; - if fuse_context_lock.is_some() { - return Ok(()); + if let Some(ctx) = fuse_context_lock.as_ref() { + match inspect_mount_point_state(&ctx.mount_point) { + MountPointState::HealthyMount => return Ok(()), + MountPointState::StaleMount | MountPointState::NotMounted => { + log::warn!( + "clipboard FUSE mount {} is disconnected, remounting", + ctx.mount_point.display() + ); + let stale_context = fuse_context_lock.take(); + drop(fuse_context_lock); + drop(stale_context); + return init_fuse_context(is_client); + } + MountPointState::Unknown => { + log::warn!( + "failed to verify clipboard FUSE mount {}", + ctx.mount_point.display() + ); + return Err(CliprdrError::CliprdrInit); + } + } } let mount_point = if is_client { FUSE_MOUNT_POINT_CLIENT.clone() @@ -70,6 +98,28 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { }; let mount_point = std::path::PathBuf::from(&*mount_point); + match inspect_mount_point_state(&mount_point) { + MountPointState::HealthyMount => { + log::warn!( + "clipboard FUSE mount {} is already active in another context", + mount_point.display() + ); + return Err(CliprdrError::ClipboardOccupied); + } + MountPointState::StaleMount => { + log::warn!( + "clipboard FUSE mount {} is stale, cleaning up before remount", + mount_point.display() + ); + unmount_fuse_mount_point(&mount_point); + validate_mount_state_after_stale_cleanup( + &mount_point, + inspect_mount_point_state(&mount_point), + )?; + } + MountPointState::Unknown => return Err(CliprdrError::CliprdrInit), + MountPointState::NotMounted => {} + } let (server, tx) = FuseServer::new(FUSE_TIMEOUT); let server = Arc::new(Mutex::new(server)); @@ -172,18 +222,23 @@ fn prepare_fuse_mount_point(mount_point: &PathBuf) -> Result<(), CliprdrError> { os::unix::prelude::PermissionsExt, }; + if let Some(parent) = mount_point.parent() { + reject_symlink_path(parent)?; + if let Err(e) = fs::create_dir_all(parent) { + log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e); + return Err(CliprdrError::CliprdrInit); + } + } + + reject_symlink_path(mount_point)?; + let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) { log::warn!( "failed to create clipboard FUSE mount point {}, trying stale mount cleanup: {:?}", mount_point.display(), e ); - if let Err(e) = std::process::Command::new("umount") - .arg(mount_point) - .status() - { - log::warn!("umount {:?} may fail: {:?}", mount_point, e); - } + unmount_fuse_mount_point(mount_point); fs::create_dir_all(mount_point).map_err(|e| { log::error!( "failed to create clipboard FUSE mount point {} after cleanup: {:?}", @@ -205,22 +260,193 @@ fn prepare_fuse_mount_point(mount_point: &PathBuf) -> Result<(), CliprdrError> { } if !recovered_stale_mount { - if let Err(e) = std::process::Command::new("umount") - .arg(mount_point) - .status() - { - log::warn!("umount {:?} may fail: {:?}", mount_point, e); - } + unmount_fuse_mount_point(mount_point); } Ok(()) } -fn uninit_fuse_context_(is_client: bool) { - if is_client { - let _ = FUSE_CONTEXT_CLIENT.lock().take(); - } else { - let _ = FUSE_CONTEXT_SERVER.lock().take(); +fn inspect_mount_point_state(mount_point: &Path) -> MountPointState { + if ensure_mount_point_path_is_safe(mount_point).is_err() { + return MountPointState::Unknown; } + inspect_mount_point_state_with( + mount_point, + std::fs::metadata(mount_point), + std::fs::read_to_string("/proc/self/mountinfo"), + ) +} + +fn validate_mount_state_after_stale_cleanup( + mount_point: &Path, + mount_state: MountPointState, +) -> Result<(), CliprdrError> { + match mount_state { + MountPointState::NotMounted => Ok(()), + MountPointState::HealthyMount => { + log::warn!( + "clipboard FUSE mount {} is still active after stale cleanup", + mount_point.display() + ); + Err(CliprdrError::ClipboardOccupied) + } + MountPointState::StaleMount => { + log::warn!( + "clipboard FUSE mount {} is still stale after cleanup", + mount_point.display() + ); + Err(CliprdrError::CliprdrInit) + } + MountPointState::Unknown => { + log::warn!( + "failed to verify clipboard FUSE mount {} after cleanup", + mount_point.display() + ); + Err(CliprdrError::CliprdrInit) + } + } +} + +fn inspect_mount_point_state_with( + mount_point: &Path, + metadata_result: io::Result, + mountinfo_result: io::Result, +) -> MountPointState { + match metadata_result { + Ok(_) => match mountinfo_result { + Ok(mountinfo) => { + if is_mount_point_listed_in_mountinfo(mount_point, &mountinfo) { + MountPointState::HealthyMount + } else { + MountPointState::NotMounted + } + } + Err(e) => { + log::warn!("failed to read mountinfo for {:?}: {:?}", mount_point, e); + MountPointState::Unknown + } + }, + Err(e) if e.raw_os_error() == Some(libc::ENOTCONN) => MountPointState::StaleMount, + Err(e) if e.kind() == io::ErrorKind::NotFound => MountPointState::NotMounted, + Err(e) => { + log::warn!("failed to inspect FUSE mount {:?}: {:?}", mount_point, e); + MountPointState::Unknown + } + } +} + +fn is_mount_point_listed_in_mountinfo(mount_point: &Path, mountinfo: &str) -> bool { + let mount_point = mount_point.to_string_lossy(); + mountinfo.lines().any(|line| { + let mut fields = line.split_whitespace(); + let _mount_id = fields.next(); + let _parent_id = fields.next(); + let _major_minor = fields.next(); + let _root = fields.next(); + let mount_path = fields.next(); + mount_path == Some(mount_point.as_ref()) + }) +} + +fn reject_symlink_metadata_result( + path: &Path, + metadata_result: io::Result, + allow_disconnected_mount: bool, +) -> Result<(), CliprdrError> { + match metadata_result { + Ok(metadata) if metadata.file_type().is_symlink() => { + log::warn!("refusing to use symlinked FUSE path {:?}", path); + Err(CliprdrError::CliprdrInit) + } + Ok(_) => Ok(()), + Err(e) if allow_disconnected_mount && e.raw_os_error() == Some(libc::ENOTCONN) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Err(e) => { + log::warn!("failed to inspect FUSE path {:?}: {:?}", path, e); + Err(CliprdrError::CliprdrInit) + } + } +} + +fn reject_symlink_path(path: &Path) -> Result<(), CliprdrError> { + reject_symlink_metadata_result(path, std::fs::symlink_metadata(path), false) +} + +fn ensure_mount_point_path_is_safe(mount_point: &Path) -> Result<(), CliprdrError> { + if let Some(parent) = mount_point.parent() { + reject_symlink_path(parent)?; + } + reject_symlink_metadata_result(mount_point, std::fs::symlink_metadata(mount_point), true) +} + +fn unmount_fuse_mount_point(mount_point: &Path) { + if ensure_mount_point_path_is_safe(mount_point).is_err() { + log::warn!( + "refusing to unmount unsafe clipboard FUSE mount point {:?}", + mount_point + ); + return; + } + if inspect_mount_point_state_with( + mount_point, + std::fs::metadata(mount_point), + std::fs::read_to_string("/proc/self/mountinfo"), + ) == MountPointState::NotMounted + { + return; + } + for (program, args) in unmount_command_candidates() { + if run_unmount_command(program, args, mount_point) { + return; + } + } + log::warn!( + "failed to unmount clipboard FUSE mount point {:?}", + mount_point + ); +} + +fn unmount_command_candidates() -> [(&'static str, &'static [&'static str]); 3] { + [ + ("fusermount3", &["-uz"]), + ("fusermount", &["-uz"]), + ("umount", &["-l"]), + ] +} + +fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool { + match std::process::Command::new(program) + .args(args) + .arg(mount_point) + .status() + { + Ok(status) if status.success() => {} + Ok(status) => { + log::debug!( + "{} {:?} exited with status {:?}", + program, + mount_point, + status.code() + ); + return false; + } + Err(e) => { + log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e); + return false; + } + } + true +} + +fn uninit_fuse_context_(is_client: bool) { + let ctx = { + let mut fuse_context_lock = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + fuse_context_lock.take() + }; + drop(ctx); } impl Drop for FuseContext { @@ -265,3 +491,66 @@ impl FuseContext { .collect()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs, io}; + + #[cfg(target_family = "unix")] + use std::os::unix::fs::symlink; + + #[test] + fn classifies_mount_point_state_from_metadata_and_mountinfo() { + let mount_point = std::env::temp_dir().join(format!( + "rustdesk-fuse-mount-state-test-{}-{}", + std::process::id(), + line!() + )); + let mountinfo = format!( + "123 1 0:45 / {} rw,nosuid,nodev - fuse.rustdesk rustdesk rw\n", + mount_point.display() + ); + + assert_eq!( + inspect_mount_point_state_with(&mount_point, Ok(()), Ok(mountinfo)), + MountPointState::HealthyMount + ); + assert_eq!( + inspect_mount_point_state_with(&mount_point, Ok(()), Ok(String::new())), + MountPointState::NotMounted + ); + + let disconnected_metadata: io::Result<()> = + Err(io::Error::from_raw_os_error(libc::ENOTCONN)); + assert_eq!( + inspect_mount_point_state_with(&mount_point, disconnected_metadata, Ok(String::new())), + MountPointState::StaleMount + ); + } + + #[test] + #[cfg(target_family = "unix")] + fn rejects_symlink_mount_point() { + let base = std::env::temp_dir().join(format!( + "rustdesk-fuse-symlink-test-{}-{}", + std::process::id(), + line!() + )); + let mount_parent = base.join("parent"); + let mount_point = mount_parent.join("cliprdr-client"); + let symlink_target = base.join("symlink-target"); + let _ = fs::remove_dir_all(&base); + fs::create_dir_all(&base).unwrap(); + fs::create_dir_all(&mount_parent).unwrap(); + fs::create_dir_all(&symlink_target).unwrap(); + symlink(&symlink_target, &mount_point).unwrap(); + + assert!(matches!( + prepare_fuse_mount_point(&mount_point), + Err(CliprdrError::CliprdrInit) + )); + + let _ = fs::remove_dir_all(&base); + } +} diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index 11d62cad8..50c67b68f 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -192,20 +192,16 @@ impl LocalFile { }); }; - if offset != self.offset.load(Ordering::Relaxed) { + let read_result = if offset != self.offset.load(Ordering::Relaxed) { handle .seek(std::io::SeekFrom::Start(offset)) - .map_err(|e| CliprdrError::FileError { - path: self.path.to_string_lossy().to_string(), - err: e, - })?; + .and_then(|_| handle.read_exact(buf)) + } else { + handle.read_exact(buf) + }; + if let Err(e) = read_result { + return Err(self.invalidate_handle(e)); } - handle - .read_exact(buf) - .map_err(|e| CliprdrError::FileError { - path: self.path.to_string_lossy().to_string(), - err: e, - })?; let new_offset = offset + (buf.len() as u64); self.offset.store(new_offset, Ordering::Relaxed); @@ -217,6 +213,15 @@ impl LocalFile { Ok(()) } + + fn invalidate_handle(&mut self, err: std::io::Error) -> CliprdrError { + self.offset.store(0, Ordering::Relaxed); + self.handle = None; + CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err, + } + } } pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { @@ -278,7 +283,10 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C #[cfg(test)] mod file_list_test { - use std::{path::PathBuf, sync::atomic::AtomicU64}; + use std::{ + path::PathBuf, + sync::atomic::{AtomicU64, Ordering}, + }; use hbb_common::bytes::{BufMut, BytesMut}; @@ -384,4 +392,30 @@ mod file_list_test { as_bin_parse_test("/test")?; Ok(()) } + + #[test] + fn read_exact_at_reopens_after_read_failure() -> Result<(), Box> { + let file_path = std::env::temp_dir().join(format!( + "rustdesk-clipboard-local-file-{}", + std::process::id() + )); + std::fs::write(&file_path, b"")?; + + let mut file = LocalFile::try_open(&std::env::temp_dir(), &file_path)?; + file.size = 1; + + let mut buf = [0u8; 1]; + assert!(file.read_exact_at(&mut buf, 0).is_err()); + assert!(file.handle.is_none()); + assert_eq!(file.offset.load(Ordering::Relaxed), 0); + + std::fs::write(&file_path, [42u8])?; + + file.read_exact_at(&mut buf, 0)?; + assert_eq!(buf, [42u8]); + assert!(file.handle.is_none()); + + std::fs::remove_file(file_path)?; + Ok(()) + } } diff --git a/src/client.rs b/src/client.rs index 87792d408..680ed1bec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -941,21 +941,23 @@ impl Client { #[cfg(not(target_os = "ios"))] fn try_stop_clipboard() { - // There's a bug here. - // If session is closed by the peer, `has_sessions_running()` will always return true. - // It's better to check if the active session number. - // But it's not a problem, because the clipboard thread does not consume CPU. - // - // If we want to fix it, we can add a flag to indicate if session is active. - // But I think it's not necessary to introduce complexity at this point. + // Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions + // should block clipboard cleanup. #[cfg(feature = "flutter")] - if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { + if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) { return; } #[cfg(not(target_os = "android"))] clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME); CLIPBOARD_STATE.lock().unwrap().running = false; #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync( + crate::clipboard::ClipboardSide::Client, + 0, + ) { + log::error!("Failed to empty client clipboard files: {}", e); + } + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] clipboard::platform::unix::fuse::uninit_fuse_context(true); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 012107f57..68cd69700 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -360,6 +360,8 @@ impl Remote { #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] if self.handler.is_default() && _set_disconnected_ok { + // Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is + // unmounted. Keep this async path for other file-clipboard platforms. crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } diff --git a/src/clipboard.rs b/src/clipboard.rs index d2a8a6653..01dc0c9ed 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -151,41 +151,50 @@ pub fn update_clipboard_files(files: Vec, side: ClipboardSide) { #[cfg(feature = "unix-file-copy-paste")] pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { std::thread::spawn(move || { - let mut ctx = CLIPBOARD_CTX.lock().unwrap(); - if ctx.is_none() { - match ClipboardContext::new() { - Ok(x) => { - *ctx = Some(x); - } - Err(e) => { - log::error!("Failed to create clipboard context: {}", e); - return; - } - } - } - #[allow(unused_mut)] - if let Some(mut ctx) = ctx.as_mut() { - #[cfg(target_os = "linux")] - { - use clipboard::platform::unix; - if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { - ctx.try_empty_clipboard_files(_side); - } - } - #[cfg(target_os = "macos")] - { - ctx.try_empty_clipboard_files(_side); - // No need to make sure the context is enabled. - clipboard::ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(_conn_id).ok(); - Ok(()) - }) - .ok(); - } + if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) { + log::error!("Failed to empty clipboard files: {}", e); } }); } +#[cfg(feature = "unix-file-copy-paste")] +pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + bail!("Failed to create clipboard context: {}", e); + } + } + } + #[allow(unused_mut)] + if let Some(mut ctx) = ctx.as_mut() { + #[cfg(target_os = "linux")] + { + use clipboard::platform::unix; + if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + ctx.try_empty_clipboard_files(_side); + } + } + #[cfg(target_os = "macos")] + { + ctx.try_empty_clipboard_files(_side); + // No need to make sure the context is enabled. + clipboard::ContextSend::proc(|context| -> ResultType<()> { + if !context.empty_clipboard(_conn_id)? { + bail!("Failed to empty clipboard files for conn_id {}", _conn_id); + } + Ok(()) + })?; + } + } + Ok(()) +} + #[cfg(target_os = "windows")] pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) { log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id); diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index 724d8aea9..4fa64e26f 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -332,12 +332,16 @@ pub mod unix_file_clip { log::debug!("format data response: msg_flags: {}", msg_flags); if msg_flags != 0x1 { - // return failure message? + log::error!( + "peer reported clipboard format data failure: {}", + msg_flags + ); + return vec![]; } log::debug!("parsing file descriptors"); - if fuse::init_fuse_context(true).is_ok() { - match fuse::format_data_response_to_urls( + match fuse::init_fuse_context(side == ClipboardSide::Client) { + Ok(()) => match fuse::format_data_response_to_urls( side == ClipboardSide::Client, format_data, conn_id, @@ -348,9 +352,10 @@ pub mod unix_file_clip { Err(e) => { log::error!("failed to parse file descriptors: {:?}", e); } + }, + Err(e) => { + log::error!("failed to initialize clipboard FUSE context: {:?}", e); } - } else { - // send error message to server } } ClipboardFile::FileContentsRequest { @@ -386,6 +391,7 @@ pub mod unix_file_clip { ClipboardFile::FileContentsResponse { msg_flags, stream_id, + requested_data, .. } => { log::debug!( @@ -393,13 +399,15 @@ pub mod unix_file_clip { msg_flags, stream_id, ); - if fuse::init_fuse_context(true).is_ok() { - hbb_common::allow_err!(fuse::handle_file_content_response( - side == ClipboardSide::Client, - clip - )); - } else { - // send error message to server + let response = ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + }; + if let Err(e) = + fuse::handle_file_content_response(side == ClipboardSide::Client, response) + { + log::error!("failed to handle file contents response: {:?}", e); } } ClipboardFile::NotifyCallback { diff --git a/src/flutter.rs b/src/flutter.rs index f8b04bf6c..73f2dbde3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -2297,6 +2297,16 @@ pub mod sessions { *r#type == conn_type && s.session_handlers.read().unwrap().len() != 0 }) } + + #[inline] + #[cfg(not(target_os = "ios"))] + pub fn has_connected_sessions_running(conn_type: ConnType) -> bool { + SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| { + *r#type == conn_type + && s.session_handlers.read().unwrap().len() != 0 + && s.connection_round_state.lock().unwrap().is_connected() + }) + } } pub(super) mod async_tasks { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 33d7611c1..1e35672ec 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -128,6 +128,10 @@ impl ConnectionRoundState { true } } + + pub fn is_connected(&self) -> bool { + matches!(self.state, ConnectionState::Connected) + } } impl Default for ConnectionRoundState {