diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index fe23f9423786..27fd64c37eeb 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1549,13 +1549,21 @@ jobs: # start build pushd /workspace export VCPKG_ROOT=/opt/artifacts/vcpkg + # build libfuse for feature "unix-file-copy-paste", we can use `apt install fuse3,libfuse3-dev` if we use ubuntu 20.04 + apt install -y meson ninja-build libudev-dev + git clone https://github.com/libfuse/libfuse.git + pushd libfuse + git checkout fuse-3.13.0 + mkdir build && cd build && meson .. && ninja install + popd + ldconfig && pkg-config --modversion fuse3 if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then export JOBS="--jobs 3" else export JOBS="" fi echo $JOBS - cargo build --lib $JOBS --features hwcodec,flutter --release + cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release rm -rf target/release/deps target/release/build rm -rf ~/.cargo @@ -1708,7 +1716,7 @@ jobs: deb_arch: amd64, sciter_arch: x64, vcpkg-triplet: x64-linux, - extra_features: ",hwcodec", + extra_features: ",hwcodec,unix-file-copy-paste", } - { arch: armv7, @@ -1718,7 +1726,7 @@ jobs: deb_arch: armhf, sciter_arch: arm32, vcpkg-triplet: arm-linux, - extra_features: "", + extra_features: ",unix-file-copy-paste", } steps: - name: Export GitHub Actions cache environment variables @@ -1871,7 +1879,15 @@ jobs: exit 1 fi head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true - # build rustdesk + # build libfuse for feature "unix-file-copy-paste", we can use `apt install fuse3,libfuse3-dev` if we use ubuntu 20.04 + apt install -y meson ninja-build libudev-dev + git clone https://github.com/libfuse/libfuse.git + pushd libfuse + git checkout fuse-3.13.0 + mkdir build && cd build && meson .. && ninja install + popd + ldconfig && pkg-config --modversion fuse3 + build rustdesk python3 ./res/inline-sciter.py export CARGO_INCREMENTAL=0 cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1 diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b16db4c4a6a1..0f681e78e874 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -12,4 +12,4 @@ jobs: secrets: inherit with: upload-artifact: true - upload-tag: "nightly" + upload-tag: "test-build-fuse-lib" diff --git a/Cargo.lock b/Cargo.lock index 21f9503858e6..30ee21af8adf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60" +source = "git+https://github.com/fufesou/arboard?branch=feat/file_urls#abafb91e2b42ab4ec0be378a7e66d59d306506d5" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,6 +234,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", + "percent-encoding", "serde 1.0.203", "serde_derive", "windows-sys 0.48.0", @@ -1697,7 +1698,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]] diff --git a/Cargo.toml b/Cargo.toml index f5474ae4e37e..39f553e4a1ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,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"] } +arboard = { git = "https://github.com/fufesou/arboard", branch = "feat/file_urls", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } system_shutdown = "4.0" diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 153121057e5e..0257c4bca036 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -555,7 +555,8 @@ Future> toolbarDisplayToggle( // If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set. final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && bind.mainHasFileClipboard() && - pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); + pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard) && + !bind.mainCurrentIsWayland(); if (ffiModel.keyboard && perms['file'] != false && (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 30055740ed8c..75f83cf20a7b 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,11 +1,7 @@ -#[allow(dead_code)] -use std::{ - path::PathBuf, - sync::{Arc, Mutex, RwLock}, -}; +use std::sync::{Arc, Mutex, RwLock}; -#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] -use hbb_common::{allow_err, bail}; +#[cfg(feature = "unix-file-copy-paste")] +use hbb_common::{allow_err, log}; use hbb_common::{ lazy_static, tokio::sync::{ @@ -17,8 +13,10 @@ use hbb_common::{ use serde_derive::{Deserialize, Serialize}; use thiserror::Error; +#[cfg(target_os = "windows")] pub mod context_send; pub mod platform; +#[cfg(target_os = "windows")] pub use context_send::*; #[cfg(target_os = "windows")] @@ -28,8 +26,10 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; #[cfg(target_os = "windows")] const ERR_CODE_SEND_MSG: u32 = 0x00000003; +#[cfg(target_os = "windows")] pub(crate) use platform::create_cliprdr_context; +// to-do: This trait may be removed, because unix file copy paste does not need it. /// Ability to handle Clipboard File from remote rustdesk client /// /// # Note @@ -63,9 +63,11 @@ pub enum CliprdrError { #[error("failure to read clipboard")] OpenClipboard, #[error("failure to read file metadata or content")] - FileError { path: PathBuf, err: std::io::Error }, + FileError { path: String, err: std::io::Error }, #[error("invalid request")] InvalidRequest { description: String }, + #[error("common request")] + CommonError { description: String }, #[error("unknown cliprdr error")] Unknown(u32), } @@ -200,29 +202,36 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType<()> { +pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { #[cfg(target_os = "windows")] return send_data_to_channel(conn_id, data); #[cfg(not(target_os = "windows"))] if conn_id == 0 { - send_data_to_all(data); + let _ = send_data_to_all(data); + Ok(()) } else { - send_data_to_channel(conn_id, data); + send_data_to_channel(conn_id, data) } } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] #[inline] -fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { if let Some(msg_channel) = VEC_MSG_CHANNEL .read() .unwrap() .iter() .find(|x| x.conn_id == conn_id) { - msg_channel.sender.send(data)?; - Ok(()) + msg_channel + .sender + .send(data) + .map_err(|e| CliprdrError::CommonError { + description: e.to_string(), + }) } else { - bail!("conn_id not found"); + Err(CliprdrError::InvalidRequest { + description: "conn_id not found".to_string(), + }) } } diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/fuse.rs index c5fe60f56e38..ccde0e0a1acb 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/fuse.rs @@ -527,14 +527,6 @@ impl FuseServer { size: u32, ) -> Result, std::io::Error> { // todo: async and concurrent read, generate stream_id per request - log::debug!( - "reading {:?} offset {} size {} on stream: {}", - node.name, - offset, - size, - node.stream_id - ); - let cb_requested = unsafe { // convert `size` from u32 to i32 // yet with same bit representation @@ -554,13 +546,10 @@ impl FuseServer { clip_data_id: 0, }; - send_data(node.conn_id, request.clone()); - - log::debug!( - "waiting for read reply for {:?} on stream: {}", - node.name, - node.stream_id - ); + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; let mut retry_times = 0; @@ -590,7 +579,10 @@ impl FuseServer { )); } - send_data(node.conn_id, request.clone()); + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; continue; } return Ok(requested_data); @@ -881,7 +873,7 @@ impl FuseNode { format!("invalid file name {}", file.name.display()), ); CliprdrError::FileError { - path: file.name.clone(), + path: file.name.to_string_lossy().to_string(), err, } })?; @@ -1064,8 +1056,6 @@ impl FileHandles { #[cfg(test)] mod fuse_test { - use std::str::FromStr; - use super::*; // todo: more tests needed! diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 5db271129734..6c0f7d12b5de 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,6 +1,3 @@ -#[cfg(any(target_os = "linux", target_os = "macos"))] -use crate::{CliprdrError, CliprdrServiceContext}; - #[cfg(target_os = "windows")] pub mod windows; #[cfg(target_os = "windows")] @@ -16,76 +13,12 @@ pub fn create_cliprdr_context( } #[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] /// use FUSE for file pasting on these platforms pub mod fuse; #[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] pub mod unix; -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub fn create_cliprdr_context( - _enable_files: bool, - _enable_others: bool, - _response_wait_timeout_secs: u32, -) -> crate::ResultType> { - #[cfg(feature = "unix-file-copy-paste")] - { - use std::{fs::Permissions, os::unix::prelude::PermissionsExt}; - - use hbb_common::{config::APP_NAME, log}; - - if !_enable_files { - return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); - } - - let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64); - - let app_name = APP_NAME.read().unwrap().clone(); - - let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr"); - - // this function must be called after the main IPC is up - std::fs::create_dir(&mnt_path).ok(); - std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok(); - - log::info!("clear previously mounted cliprdr FUSE"); - if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() { - log::warn!("umount {:?} may fail: {:?}", mnt_path, e); - } - - let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?; - log::debug!("start cliprdr FUSE"); - unix_ctx.run()?; - - Ok(Box::new(unix_ctx) as Box<_>) - } - - #[cfg(not(feature = "unix-file-copy-paste"))] - 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(()) - } - fn empty_clipboard(&mut self, _conn_id: i32) -> Result { - Ok(true) - } - fn server_clip_file( - &mut self, - _conn_id: i32, - _msg: crate::ClipboardFile, - ) -> Result<(), crate::CliprdrError> { - Ok(()) - } -} #[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] // begin of epoch used by microsoft // 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 const LDAP_EPOCH_DELTA: u64 = 116444772610000000; diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index b609b8cc79ef..cec118f3c4b4 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -53,7 +53,7 @@ pub(super) struct LocalFile { impl LocalFile { pub fn try_open(path: &Path) -> Result { let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), + path: path.to_string_lossy().to_string(), err: e, })?; let size = mt.len() as u64; @@ -79,7 +79,7 @@ impl LocalFile { Ok(Self { name, - path: path.clone(), + path: path.to_path_buf(), handle, offset, size, @@ -172,12 +172,12 @@ impl LocalFile { pub fn load_handle(&mut self) -> Result<(), CliprdrError> { if !self.is_dir && self.handle.is_none() { let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle); reader.fill_buf().map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; self.handle = Some(reader); @@ -188,20 +188,25 @@ impl LocalFile { pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> { self.load_handle()?; - let handle = self.handle.as_mut()?; + let Some(handle) = self.handle.as_mut() else { + return Err(CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"), + }); + }; if offset != self.offset.load(Ordering::Relaxed) { handle .seek(std::io::SeekFrom::Start(offset)) .map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; } handle .read_exact(buf) .map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; let new_offset = offset + (buf.len() as u64); @@ -227,20 +232,26 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C if visited.contains(path) { return Ok(()); } - visited.insert(path.clone()); + visited.insert(path.to_path_buf()); let local_file = LocalFile::try_open(path)?; file_list.push(local_file); let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), + path: path.to_string_lossy().to_string(), err: e, })?; if mt.is_dir() { - let dir = std::fs::read_dir(path)?; + let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; for entry in dir { - let entry = entry?; + let entry = entry.map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; let path = entry.path(); constr_file_lst(&path, file_list, visited)?; } diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 34021d6bf209..a98f9e13e86c 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -1,5 +1,5 @@ use std::{ - path::{Path, PathBuf}, + path::PathBuf, sync::{mpsc::Sender, Arc}, time::Duration, }; @@ -8,6 +8,7 @@ use dashmap::DashMap; use fuser::MountOption; use hbb_common::{ bytes::{BufMut, BytesMut}, + config::APP_NAME, log, }; use lazy_static::lazy_static; @@ -15,34 +16,21 @@ use parking_lot::Mutex; use crate::{ platform::{fuse::FileDescription, unix::local_file::construct_file_list}, - send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, + send_data, ClipboardFile, CliprdrError, }; use self::local_file::LocalFile; -#[cfg(target_os = "linux")] -use self::url::{encode_path_to_uri, parse_plain_uri_list}; use super::fuse::FuseServer; -#[cfg(target_os = "linux")] -/// clipboard implementation of x11 -pub mod x11; - -#[cfg(target_os = "macos")] -/// clipboard implementation of macos -pub mod ns_clipboard; - pub mod local_file; -#[cfg(target_os = "linux")] -pub mod url; - // not actual format id, just a placeholder -const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; -const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; +pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; +pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; // not actual format id, just a placeholder -const FILECONTENTS_FORMAT_ID: i32 = 49267; -const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; +pub const FILECONTENTS_FORMAT_ID: i32 = 49267; +pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; lazy_static! { static ref REMOTE_FORMAT_MAP: DashMap = DashMap::from_iter( @@ -56,145 +44,275 @@ lazy_static! { .iter() .cloned() ); -} -fn get_local_format(remote_id: i32) -> Option { - REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) -} + static ref FUSE_MOUNT_POINT_CLIENT: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_MOUNT_POINT_SERVER: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; -fn add_remote_format(local_name: &str, remote_id: i32) { - REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string()); + static ref FUSE_CONTEXT_CLIENT: Arc>> = Arc::new(Mutex::new(None)); + static ref FUSE_CONTEXT_SERVER: Arc>> = Arc::new(Mutex::new(None)); } -trait SysClipboard: Send + Sync { - fn start(&self); +static FUSE_TIMEOUT: Duration = Duration::from_secs(3); - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; - fn get_file_list(&self) -> Vec; +pub fn get_exclude_paths() -> Vec> { + vec![ + FUSE_MOUNT_POINT_CLIENT.clone(), + FUSE_MOUNT_POINT_SERVER.clone(), + ] } -#[cfg(target_os = "linux")] -fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { - #[cfg(feature = "wayland")] - { - unimplemented!() +pub fn is_fuse_context_inited(is_client: bool) -> bool { + if is_client { + FUSE_CONTEXT_CLIENT.lock().is_some() + } else { + FUSE_CONTEXT_SERVER.lock().is_some() } - #[cfg(not(feature = "wayland"))] - { - use x11::*; - let x11_clip = X11Clipboard::new(ignore_path)?; - Ok(Box::new(x11_clip) as Box<_>) +} + +// No need to consider the race condition here. +pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { + if is_fuse_context_inited(is_client) { + return Ok(()); } + let mount_point = if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + }; + init_fuse_context_( + is_client, + FUSE_TIMEOUT, + std::path::PathBuf::from(&*mount_point), + ) } -#[cfg(target_os = "macos")] -fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { - use ns_clipboard::*; - let ns_pb = NsPasteboard::new(ignore_path)?; - Ok(Box::new(ns_pb) as Box<_>) +pub fn uninit_fuse_context(is_client: bool) { + uninit_fuse_context_(is_client) } -#[derive(Debug)] -enum FileContentsRequest { - Size { - stream_id: i32, - file_idx: usize, - }, +pub fn format_data_response_to_urls( + is_client: bool, + format_data: Vec, + conn_id: i32, +) -> Result, CliprdrError> { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .ok_or(CliprdrError::CliprdrInit)? + .format_data_response_to_urls(format_data, conn_id) +} - Range { - stream_id: i32, - file_idx: usize, - offset: u64, - length: u64, - }, +pub fn read_file_contents( + is_client: bool, + conn_id: i32, + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, +) -> Result { + let fcr = if dw_flags == 0x1 { + FileContentsRequest::Size { + stream_id, + file_idx: list_index as usize, + } + } else if dw_flags == 0x2 { + let offset = (n_position_high as u64) << 32 | n_position_low as u64; + let length = cb_requested as u64; + + FileContentsRequest::Range { + stream_id, + file_idx: list_index as usize, + offset, + length, + } + } else { + return Err(CliprdrError::InvalidRequest { + description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"), + }); + }; + + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .ok_or(CliprdrError::CliprdrInit)? + .serve_file_contents(conn_id, fcr) } -pub struct ClipboardContext { - pub fuse_mount_point: PathBuf, - /// stores fuse background session handle - fuse_handle: Mutex>, +pub fn handle_file_content_response( + is_client: bool, + clip: ClipboardFile, +) -> Result<(), CliprdrError> { + log::info!("REMOVE ME ================================= server_file_contents_response called"); + // we don't know its corresponding request, no resend can be performed + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .ok_or(CliprdrError::CliprdrInit)? + .tx + .send(clip) + .map_err(|e| { + log::error!("failed to send file contents response to fuse: {:?}", e); + CliprdrError::ClipboardInternalError + })?; + Ok(()) +} - /// a sender of clipboard file contents pdu to fuse server - fuse_tx: Sender, - fuse_server: Arc>, +pub fn empty_local_files(is_client: bool) { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref().map(|c| c.empty_local_files()); +} - clipboard: Arc, +struct FuseContext { + server: Arc>, + tx: Sender, + mount_point: PathBuf, + // stores fuse background session handle + session: Mutex>, + // local files are cached, this value should not be changed when copying files + // Because `CliprdrFileContentsRequest` only contains the index of the file in the list. + // We need to keep the file list in the same order as the remote side. + // We may add a `FileId` field to `CliprdrFileContentsRequest` in the future. local_files: Mutex>, } -impl ClipboardContext { - pub fn new(timeout: Duration, mount_path: PathBuf) -> Result { - // assert mount path exists - let fuse_mount_point = mount_path.canonicalize().map_err(|e| { - log::error!("failed to canonicalize mount path: {:?}", e); - CliprdrError::CliprdrInit - })?; +// this function must be called after the main IPC is up +fn prepare_fuse_mount_point(mount_point: &PathBuf) { + use std::{ + fs::{self, Permissions}, + os::unix::prelude::PermissionsExt, + }; - let (fuse_server, fuse_tx) = FuseServer::new(timeout); + fs::create_dir(mount_point).ok(); + fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok(); - let fuse_server = Arc::new(Mutex::new(fuse_server)); + if let Err(e) = std::process::Command::new("umount") + .arg(mount_point) + .status() + { + log::warn!("umount {:?} may fail: {:?}", mount_point, e); + } +} - let clipboard = get_sys_clipboard(&fuse_mount_point)?; - let clipboard = Arc::from(clipboard) as Arc<_>; - let local_files = Mutex::new(vec![]); +fn init_fuse_context_( + is_client: bool, + timeout: Duration, + mount_point: PathBuf, +) -> Result<(), CliprdrError> { + let (server, tx) = FuseServer::new(timeout); + let server = Arc::new(Mutex::new(server)); + + prepare_fuse_mount_point(&mount_point); + let mnt_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::NoAtime, + MountOption::RO, + ]; + log::info!("mounting clipboard FUSE to {}", mount_point.display()); + let session = fuser::spawn_mount2( + FuseServer::client(server.clone()), + mount_point.clone(), + &mnt_opts, + ) + .map_err(|e| { + log::error!("failed to mount cliprdr fuse: {:?}", e); + CliprdrError::CliprdrInit + })?; + let session = Mutex::new(Some(session)); + + let ctx = FuseContext { + server, + tx, + mount_point, + session, + local_files: Mutex::new(vec![]), + }; + if is_client { + *FUSE_CONTEXT_CLIENT.lock() = Some(ctx); + } else { + *FUSE_CONTEXT_SERVER.lock() = Some(ctx); + } + Ok(()) +} - Ok(Self { - fuse_mount_point, - fuse_server, - fuse_tx, - fuse_handle: Mutex::new(None), - clipboard, - local_files, - }) +fn uninit_fuse_context_(is_client: bool) { + if is_client { + let _ = FUSE_CONTEXT_CLIENT.lock().take(); + } else { + let _ = FUSE_CONTEXT_SERVER.lock().take(); } +} - pub fn run(&self) -> Result<(), CliprdrError> { - if !self.is_stopped() { - return Ok(()); - } +impl Drop for FuseContext { + fn drop(&mut self) { + self.session.lock().take().map(|s| s.join()); + } +} - let mut fuse_handle = self.fuse_handle.lock(); - - let mount_path = &self.fuse_mount_point; - - let mnt_opts = [ - MountOption::FSName("rustdesk-cliprdr-fs".to_string()), - MountOption::NoAtime, - MountOption::RO, - ]; - log::info!( - "mounting clipboard FUSE to {}", - self.fuse_mount_point.display() - ); - - let new_handle = fuser::spawn_mount2( - FuseServer::client(self.fuse_server.clone()), - mount_path, - &mnt_opts, - ) - .map_err(|e| { - log::error!("failed to mount cliprdr fuse: {:?}", e); - CliprdrError::CliprdrInit - })?; - *fuse_handle = Some(new_handle); +impl FuseContext { + pub fn empty_local_files(&self) { + let mut local_files = self.local_files.lock(); + *local_files = vec![]; + let mut fuse_guard = self.server.lock(); + let _ = fuse_guard.load_file_list(vec![]); + } - let clipboard = self.clipboard.clone(); + pub fn format_data_response_to_urls( + &self, + format_data: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; - std::thread::spawn(move || { - log::debug!("start listening clipboard"); - clipboard.start(); - }); + let paths = { + let mut fuse_guard = self.server.lock(); + fuse_guard.load_file_list(files)?; - Ok(()) + fuse_guard.list_root() + }; + + let prefix = self.mount_point.clone(); + Ok(paths + .into_iter() + .map(|p| prefix.join(p).to_string_lossy().to_string()) + .collect()) } - /// set clipboard data from file list - pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - let prefix = self.fuse_mount_point.clone(); - let paths: Vec = paths.iter().cloned().map(|p| prefix.join(p)).collect(); - log::debug!("setting clipboard with paths: {:?}", paths); - self.clipboard.set_file_list(&paths)?; - log::debug!("clipboard set, paths: {:?}", paths); + fn sync_local_files(&self, clipboard_files: &[String]) -> Result<(), CliprdrError> { + let clipboard_files = clipboard_files + .iter() + .map(|s| PathBuf::from(s)) + .collect::>(); + let mut local_files = self.local_files.lock(); + let local_file_list: Vec = local_files.iter().map(|f| f.path.clone()).collect(); + if local_file_list == clipboard_files { + return Ok(()); + } + let new_files = construct_file_list(&clipboard_files)?; + *local_files = new_files; Ok(()) } @@ -202,7 +320,7 @@ impl ClipboardContext { &self, conn_id: i32, request: FileContentsRequest, - ) -> Result<(), CliprdrError> { + ) -> Result { let mut file_list = self.local_files.lock(); let (file_idx, file_contents_resp) = match request { @@ -217,7 +335,7 @@ impl ClipboardContext { file_idx, conn_id ); - resp_file_contents_fail(conn_id, stream_id); + let _ = resp_file_contents_fail(conn_id, stream_id); return Err(CliprdrError::InvalidRequest { description: format!( @@ -262,7 +380,7 @@ impl ClipboardContext { file_idx, conn_id ); - resp_file_contents_fail(conn_id, stream_id); + let _ = resp_file_contents_fail(conn_id, stream_id); return Err(CliprdrError::InvalidRequest { description: format!( "invalid file index {} requested from conn: {}", @@ -279,7 +397,7 @@ impl ClipboardContext { if offset > file.size { log::error!("invalid reading offset requested from conn: {}", conn_id); - resp_file_contents_fail(conn_id, stream_id); + let _ = resp_file_contents_fail(conn_id, stream_id); return Err(CliprdrError::InvalidRequest { description: format!( @@ -309,7 +427,6 @@ impl ClipboardContext { } }; - send_data(conn_id, file_contents_resp); log::debug!("file contents sent to conn: {}", conn_id); // hot reload next file for next_file in file_list.iter_mut().skip(file_idx + 1) { @@ -318,239 +435,39 @@ impl ClipboardContext { break; } } - Ok(()) + Ok(file_contents_resp) } } -fn resp_file_contents_fail(conn_id: i32, stream_id: i32) { - let resp = ClipboardFile::FileContentsResponse { - msg_flags: 0x2, - stream_id, - requested_data: vec![], - }; - send_data(conn_id, resp) -} - -impl ClipboardContext { - pub fn is_stopped(&self) -> bool { - self.fuse_handle.lock().is_none() - } - - pub fn sync_local_files(&self) -> Result<(), CliprdrError> { - let mut local_files = self.local_files.lock(); - let clipboard_files = self.clipboard.get_file_list(); - let local_file_list: Vec = local_files.iter().map(|f| f.path.clone()).collect(); - if local_file_list == clipboard_files { - return Ok(()); - } - let new_files = construct_file_list(&clipboard_files)?; - *local_files = new_files; - Ok(()) - } - - pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - log::debug!("serve clipboard file from conn: {}", conn_id); - if self.is_stopped() { - log::debug!("cliprdr stopped, restart it"); - self.run()?; - } - match msg { - ClipboardFile::NotifyCallback { .. } => { - unreachable!() - } - ClipboardFile::MonitorReady => { - log::debug!("server_monitor_ready called"); - - self.send_file_list(conn_id)?; - - Ok(()) - } - - ClipboardFile::FormatList { format_list } => { - log::debug!("server_format_list called"); - // filter out "FileGroupDescriptorW" and "FileContents" - let fmt_lst: Vec<(i32, String)> = format_list - .into_iter() - .filter(|(_, name)| { - name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME - }) - .collect(); - if fmt_lst.len() != 2 { - log::debug!("no supported formats"); - return Ok(()); - } - log::debug!("supported formats: {:?}", fmt_lst); - let file_contents_id = fmt_lst - .iter() - .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) - .map(|(id, _)| *id)?; - let file_descriptor_id = fmt_lst - .iter() - .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) - .map(|(id, _)| *id)?; - - add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id); - add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id); - - // sync file system from peer - let data = ClipboardFile::FormatDataRequest { - requested_format_id: file_descriptor_id, - }; - send_data(conn_id, data); - - Ok(()) - } - ClipboardFile::FormatListResponse { msg_flags } => { - log::debug!("server_format_list_response called"); - if msg_flags != 0x1 { - send_format_list(conn_id) - } else { - Ok(()) - } - } - ClipboardFile::FormatDataRequest { - requested_format_id, - } => { - log::debug!("server_format_data_request called"); - let Some(format) = get_local_format(requested_format_id) else { - log::error!( - "got unsupported format data request: id={} from conn={}", - requested_format_id, - conn_id - ); - resp_format_data_failure(conn_id); - return Ok(()); - }; - - if format == FILEDESCRIPTORW_FORMAT_NAME { - self.send_file_list(conn_id)?; - } else if format == FILECONTENTS_FORMAT_NAME { - log::error!( - "try to read file contents with FormatDataRequest from conn={}", - conn_id - ); - resp_format_data_failure(conn_id); - } else { - log::error!( - "got unsupported format data request: id={} from conn={}", - requested_format_id, - conn_id - ); - resp_format_data_failure(conn_id); - } - Ok(()) - } - ClipboardFile::FormatDataResponse { - msg_flags, - format_data, - } => { - log::debug!( - "server_format_data_response called, msg_flags={}", - msg_flags - ); - - if msg_flags != 0x1 { - resp_format_data_failure(conn_id); - return Ok(()); - } - - log::debug!("parsing file descriptors"); - // this must be a file descriptor format data - let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; - - let paths = { - let mut fuse_guard = self.fuse_server.lock(); - fuse_guard.load_file_list(files)?; - - fuse_guard.list_root() - }; - - log::debug!("load file list: {:?}", paths); - self.set_clipboard(&paths)?; - Ok(()) - } - ClipboardFile::FileContentsResponse { .. } => { - log::debug!("server_file_contents_response called"); - // we don't know its corresponding request, no resend can be performed - self.fuse_tx.send(msg).map_err(|e| { - log::error!("failed to send file contents response to fuse: {:?}", e); - CliprdrError::ClipboardInternalError - })?; - Ok(()) - } - ClipboardFile::FileContentsRequest { - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - .. - } => { - log::debug!("server_file_contents_request called"); - let fcr = if dw_flags == 0x1 { - FileContentsRequest::Size { - stream_id, - file_idx: list_index as usize, - } - } else if dw_flags == 0x2 { - let offset = (n_position_high as u64) << 32 | n_position_low as u64; - let length = cb_requested as u64; - - FileContentsRequest::Range { - stream_id, - file_idx: list_index as usize, - offset, - length, - } - } else { - log::error!("got invalid FileContentsRequest from conn={}", conn_id); - resp_file_contents_fail(conn_id, stream_id); - return Ok(()); - }; - - self.serve_file_contents(conn_id, fcr) - } - } - } - - fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { - self.sync_local_files()?; - - let file_list = self.local_files.lock(); - send_file_list(&*file_list, conn_id) - } +pub fn get_local_format(remote_id: i32) -> Option { + REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) } -impl CliprdrServiceContext for ClipboardContext { - fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { - // unmount the fuse - if let Some(fuse_handle) = self.fuse_handle.lock().take() { - fuse_handle.join(); - } - // we don't stop the clipboard, keep listening in case of restart - Ok(()) - } - - fn empty_clipboard(&mut self, _conn_id: i32) -> Result { - self.clipboard.set_file_list(&[])?; - Ok(true) - } +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, - fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - self.serve(conn_id, msg) - } + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, } -fn resp_format_data_failure(conn_id: i32) { - let data = ClipboardFile::FormatDataResponse { +fn resp_file_contents_fail(conn_id: i32, stream_id: i32) -> Result<(), CliprdrError> { + let resp = ClipboardFile::FileContentsResponse { msg_flags: 0x2, - format_data: vec![], + stream_id, + requested_data: vec![], }; - send_data(conn_id, data) + send_data(conn_id, resp) } -fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> { +pub fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> { log::debug!("send format list to remote, conn={}", conn_id); let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); @@ -563,11 +480,29 @@ fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> { ], }; - send_data(conn_id, format_list); + send_data(conn_id, format_list)?; log::debug!("format list to remote dispatched, conn={}", conn_id); Ok(()) } +pub fn build_file_list_format_data( + is_client: bool, + files: &[String], +) -> Result, CliprdrError> { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + match &*ctx { + None => Err(CliprdrError::CliprdrInit), + Some(ctx) => { + ctx.sync_local_files(files)?; + Ok(build_file_list_pdu(&ctx.local_files.lock())) + } + } +} + fn build_file_list_pdu(files: &[LocalFile]) -> Vec { let mut data = BytesMut::with_capacity(4 + 592 * files.len()); data.put_u32_le(files.len() as u32); @@ -577,22 +512,3 @@ fn build_file_list_pdu(files: &[LocalFile]) -> Vec { data.to_vec() } - -fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> { - log::debug!( - "send file list to remote, conn={}, list={:?}", - conn_id, - files.iter().map(|f| f.path.display()).collect::>() - ); - - let format_data = build_file_list_pdu(files); - - send_data( - conn_id, - ClipboardFile::FormatDataResponse { - msg_flags: 1, - format_data, - }, - ); - Ok(()) -} diff --git a/libs/clipboard/src/platform/unix/ns_clipboard.rs b/libs/clipboard/src/platform/unix/ns_clipboard.rs deleted file mode 100644 index a9112fe62591..000000000000 --- a/libs/clipboard/src/platform/unix/ns_clipboard.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::{ - collections::BTreeSet, - path::{Path, PathBuf}, -}; - -use cacao::pasteboard::{Pasteboard, PasteboardName}; -use hbb_common::log; -use parking_lot::Mutex; - -use crate::{platform::unix::send_format_list, CliprdrError}; - -use super::SysClipboard; - -#[inline] -fn wait_file_list() -> Option> { - let pb = Pasteboard::named(PasteboardName::General); - pb.get_file_urls() - .ok() - .map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect()) -} - -#[inline] -fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> { - let pb = Pasteboard::named(PasteboardName::General); - pb.set_files(file_list.to_vec()) - .map_err(|_| CliprdrError::ClipboardInternalError) -} - -pub struct NsPasteboard { - ignore_path: PathBuf, - - former_file_list: Mutex>, -} - -impl NsPasteboard { - pub fn new(ignore_path: &Path) -> Result { - Ok(Self { - ignore_path: ignore_path.to_owned(), - former_file_list: Mutex::new(vec![]), - }) - } -} - -impl SysClipboard for NsPasteboard { - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - *self.former_file_list.lock() = paths.to_vec(); - set_file_list(paths) - } - - fn start(&self) { - { - *self.former_file_list.lock() = vec![]; - } - - loop { - let file_list = match wait_file_list() { - Some(v) => v, - None => { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - }; - - let filtered = file_list - .into_iter() - .filter(|pb| !pb.starts_with(&self.ignore_path)) - .collect::>(); - - if filtered.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - { - let mut former = self.former_file_list.lock(); - - let filtered_st: BTreeSet<_> = filtered.iter().collect(); - let former_st = former.iter().collect::>(); - if filtered_st == former_st { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - *former = filtered; - } - - if let Err(e) = send_format_list(0) { - log::warn!("failed to send format list: {}", e); - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - log::debug!("stop listening file related atoms on clipboard"); - } - - fn get_file_list(&self) -> Vec { - self.former_file_list.lock().clone() - } -} diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs deleted file mode 100644 index 126a341cd728..000000000000 --- a/libs/clipboard/src/platform/unix/url.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::path::{Path, PathBuf}; - -use crate::CliprdrError; - -// on x11, path will be encode as -// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" -// url encode and decode is needed -const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/'); - -pub(super) fn encode_path_to_uri(path: &Path) -> io::Result { - let encoded = - percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string(); - format!("file://{}", encoded) -} - -pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result { - let encoded_path = encoded_uri.trim_start_matches("file://"); - let path_str = percent_encoding::percent_decode_str(encoded_path) - .decode_utf8() - .map_err(|_| CliprdrError::ConversionFailure)?; - let path_str = path_str.to_string(); - - Ok(Path::new(&path_str).to_path_buf()) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -pub(super) fn parse_plain_uri_list(v: Vec) -> Result, CliprdrError> { - let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; - parse_uri_list(&text) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -pub(super) fn parse_uri_list(text: &str) -> Result, CliprdrError> { - let mut list = Vec::new(); - - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = parse_uri_to_path(line)?; - list.push(decoded) - } - Ok(list) -} - -#[cfg(test)] -mod uri_test { - #[test] - fn test_conversion() { - let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png"); - let uri = super::encode_path_to_uri(&path).unwrap(); - assert_eq!( - uri, - "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" - ); - let convert_back = super::parse_uri_to_path(&uri).unwrap(); - assert_eq!(path, convert_back); - } - - #[test] - fn parse_list() { - let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png -file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png -"#; - let list = super::parse_uri_list(uri_list.into()).unwrap(); - assert!(list.len() == 2); - assert_eq!(list[0], list[1]); - } -} diff --git a/libs/clipboard/src/platform/unix/x11.rs b/libs/clipboard/src/platform/unix/x11.rs deleted file mode 100644 index 606ff6719961..000000000000 --- a/libs/clipboard/src/platform/unix/x11.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::{ - collections::BTreeSet, - path::{Path, PathBuf}, -}; - -use hbb_common::log; -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use x11_clipboard::Clipboard; -use x11rb::protocol::xproto::Atom; - -use crate::{platform::unix::send_format_list, CliprdrError}; - -use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard}; - -static X11_CLIPBOARD: OnceCell = OnceCell::new(); - -fn get_clip() -> Result<&'static Clipboard, CliprdrError> { - X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)) -} - -pub struct X11Clipboard { - ignore_path: PathBuf, - text_uri_list: Atom, - gnome_copied_files: Atom, - nautilus_clipboard: Atom, - - former_file_list: Mutex>, -} - -impl X11Clipboard { - pub fn new(ignore_path: &Path) -> Result { - let clipboard = get_clip()?; - let text_uri_list = clipboard - .setter - .get_atom("text/uri-list") - .map_err(|_| CliprdrError::CliprdrInit)?; - let gnome_copied_files = clipboard - .setter - .get_atom("x-special/gnome-copied-files") - .map_err(|_| CliprdrError::CliprdrInit)?; - let nautilus_clipboard = clipboard - .setter - .get_atom("x-special/nautilus-clipboard") - .map_err(|_| CliprdrError::CliprdrInit)?; - Ok(Self { - ignore_path: ignore_path.to_owned(), - text_uri_list, - gnome_copied_files, - nautilus_clipboard, - former_file_list: Mutex::new(vec![]), - }) - } - - fn load(&self, target: Atom) -> Result, CliprdrError> { - let clip = get_clip()?.setter.atoms.clipboard; - let prop = get_clip()?.setter.atoms.property; - // NOTE: - // # why not use `load_wait` - // load_wait is likely to wait forever, which is not what we want - let res = get_clip()?.load_wait(clip, target, prop); - match res { - Ok(res) => Ok(res), - Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]), - Err(x11_clipboard::error::Error::Timeout) => { - log::debug!("x11 clipboard get content timeout."); - Err(CliprdrError::ClipboardInternalError) - } - Err(e) => { - log::debug!("x11 clipboard get content fail: {:?}", e); - Err(CliprdrError::ClipboardInternalError) - } - } - } - - fn store_batch(&self, batch: Vec<(Atom, Vec)>) -> Result<(), CliprdrError> { - let clip = get_clip()?.setter.atoms.clipboard; - log::debug!("try to store clipboard content"); - get_clip()? - .store_batch(clip, batch) - .map_err(|_| CliprdrError::ClipboardInternalError) - } - - fn wait_file_list(&self) -> Result>, CliprdrError> { - let v = self.load(self.text_uri_list)?; - let p = parse_plain_uri_list(v)?; - Ok(Some(p)) - } -} - -impl SysClipboard for X11Clipboard { - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - *self.former_file_list.lock() = paths.to_vec(); - - let uri_list: Vec = { - let mut v = Vec::new(); - for path in paths { - v.push(encode_path_to_uri(path)?); - } - v - }; - let uri_list = uri_list.join("\n"); - let text_uri_list_data = uri_list.as_bytes().to_vec(); - let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); - let batch = vec![ - (self.text_uri_list, text_uri_list_data), - (self.gnome_copied_files, gnome_copied_files_data.clone()), - (self.nautilus_clipboard, gnome_copied_files_data), - ]; - self.store_batch(batch) - .map_err(|_| CliprdrError::ClipboardInternalError) - } - - fn start(&self) { - { - // clear cached file list - *self.former_file_list.lock() = vec![]; - } - loop { - let sth = match self.wait_file_list() { - Ok(sth) => sth, - Err(e) => { - log::warn!("failed to get file list from clipboard: {}", e); - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - }; - - let Some(paths) = sth else { - // just sleep - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - }; - - let filtered = paths - .into_iter() - .filter(|pb| !pb.starts_with(&self.ignore_path)) - .collect::>(); - - if filtered.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - { - let mut former = self.former_file_list.lock(); - - let filtered_st: BTreeSet<_> = filtered.iter().collect(); - let former_st = former.iter().collect::>(); - if filtered_st == former_st { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - *former = filtered; - } - - if let Err(e) = send_format_list(0) { - log::warn!("failed to send format list: {}", e); - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - log::debug!("stop listening file related atoms on clipboard"); - } - - fn get_file_list(&self) -> Vec { - self.former_file_list.lock().clone() - } -} diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 5d1aa086ddbf..dc3fab533afa 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -6,10 +6,10 @@ #![allow(deref_nullptr)] use crate::{ - allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, + send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, }; -use hbb_common::log; +use hbb_common::{allow_err, log}; use std::{ boxed::Box, ffi::{CStr, CString}, diff --git a/src/client.rs b/src/client.rs index 05161438325f..d1b503e43ee5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -27,6 +27,15 @@ use std::{ }; use uuid::Uuid; +use crate::{ + check_port, + common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, + create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp, + ui_interface::{get_builtin_option, use_texture_render}, + ui_session_interface::{InvokeUiSession, Session}, +}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::check_clipboard_files, clipboard_file::unix_file_clip}; pub use file_trait::FileManager; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -65,14 +74,6 @@ use scrap::{ CodecFormat, ImageFormat, ImageRgb, ImageTexture, }; -use crate::{ - check_port, - common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, - create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp, - ui_interface::{get_builtin_option, use_texture_render}, - ui_session_interface::{InvokeUiSession, Session}, -}; - #[cfg(not(target_os = "ios"))] use crate::clipboard::CLIPBOARD_INTERVAL; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -137,8 +138,11 @@ pub(crate) struct ClientClipboardContext { pub struct Client; #[cfg(not(target_os = "ios"))] -struct TextClipboardState { - is_required: bool, +struct ClipboardState { + #[cfg(feature = "flutter")] + is_text_required: bool, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: bool, running: bool, } @@ -154,7 +158,7 @@ lazy_static::lazy_static! { #[cfg(not(target_os = "ios"))] lazy_static::lazy_static! { - static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); + static ref CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(ClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; @@ -660,7 +664,12 @@ impl Client { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] pub fn set_is_text_clipboard_required(b: bool) { - TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + CLIPBOARD_STATE.lock().unwrap().is_text_required = b; + } + + #[cfg(all(feature = "unix-file-copy-paste", feature = "flutter"))] + pub fn set_is_file_clipboard_required(b: bool) { + CLIPBOARD_STATE.lock().unwrap().is_file_required = b; } #[cfg(not(target_os = "ios"))] @@ -676,19 +685,19 @@ impl Client { if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; } - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + CLIPBOARD_STATE.lock().unwrap().running = false; } // `try_start_clipboard` is called by all session when connection is established. (When handling peer info). // This function only create one thread with a loop, the loop is shared by all sessions. // After all sessions are end, the loop exists. // - // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. + // If clipboard update is detected, the text will be sent to all sessions by `send_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_start_clipboard( _client_clip_ctx: Option, ) -> Option> { - let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; } @@ -721,22 +730,12 @@ impl Client { log::info!("Start text clipboard loop"); std::thread::spawn(move || { - let mut is_sent = false; + tx_started.send(()).ok(); loop { - if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + if !CLIPBOARD_STATE.lock().unwrap().running { break; } - if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - continue; - } - - if !is_sent { - is_sent = true; - tx_started.send(()).ok(); - } - match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); @@ -753,7 +752,7 @@ impl Client { log::info!("Stop text clipboard loop"); shutdown.signal(); h.join().ok(); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + CLIPBOARD_STATE.lock().unwrap().running = false; }); Some(rx_started) @@ -761,7 +760,7 @@ impl Client { #[cfg(target_os = "android")] fn try_start_clipboard(_p: Option<()>) -> Option> { - let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; } @@ -770,22 +769,22 @@ impl Client { log::info!("Start text clipboard loop"); std::thread::spawn(move || { loop { - if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + if !CLIPBOARD_STATE.lock().unwrap().running { break; } - if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + if !CLIPBOARD_STATE.lock().unwrap().is_text_required { std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); continue; } if let Some(msg) = crate::clipboard::get_clipboards_msg(true) { - crate::flutter::send_text_clipboard_msg(msg); + crate::flutter::send_clipboard_msg(msg, false); } std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); } log::info!("Stop text clipboard loop"); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + CLIPBOARD_STATE.lock().unwrap().running = false; }); None @@ -793,10 +792,13 @@ impl Client { } #[cfg(not(target_os = "ios"))] -impl TextClipboardState { +impl ClipboardState { fn new() -> Self { Self { - is_required: true, + #[cfg(feature = "flutter")] + is_text_required: true, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: true, running: false, } } @@ -812,30 +814,63 @@ struct ClientClipboardHandler { #[cfg(not(any(target_os = "android", target_os = "ios")))] impl ClientClipboardHandler { + fn is_text_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_text_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_text_clipboard_required()) + .unwrap_or(false) + } + } + + #[cfg(feature = "unix-file-copy-paste")] + fn is_file_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_file_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_file_clipboard_required()) + .unwrap_or(false) + } + } + #[inline] #[cfg(feature = "flutter")] - fn send_msg(&self, msg: Message) { - crate::flutter::send_text_clipboard_msg(msg); + fn send_msg(&self, msg: Message, _is_file: bool) { + crate::flutter::send_clipboard_msg(msg, _is_file); } #[cfg(not(feature = "flutter"))] - fn send_msg(&self, msg: Message) { + fn send_msg(&self, msg: Message, _is_file: bool) { if let Some(ctx) = &self.client_clip_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; - } + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + let _ = ctx.tx.send(Data::Message(msg)); + return; + } + + 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)); } + let _ = ctx.tx.send(Data::Message(msg)); } } } @@ -843,11 +878,25 @@ impl ClientClipboardHandler { #[cfg(not(any(target_os = "android", target_os = "ios")))] impl ClipboardHandler for ClientClipboardHandler { fn on_clipboard_change(&mut self) -> CallbackResult { - if TEXT_CLIPBOARD_STATE.lock().unwrap().running - && TEXT_CLIPBOARD_STATE.lock().unwrap().is_required - { - if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { - self.send_msg(msg); + if CLIPBOARD_STATE.lock().unwrap().running { + #[cfg(feature = "unix-file-copy-paste")] + if self.is_file_required() { + if let Some(msg) = + check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) + { + if !msg.is_empty() { + let msg = + crate::clipboard_file::clip_2_msg(unix_file_clip::get_format_list()); + self.send_msg(msg, true); + return CallbackResult::Next; + } + } + } + + if self.is_text_required() { + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { + self.send_msg(msg, false); + } } } CallbackResult::Next @@ -1816,6 +1865,11 @@ impl LoginConfigHandler { self.config.store(&self.id); return None; } + + #[cfg(feature = "unix-file-copy-paste")] + if option.enable_file_transfer.enum_value() == Ok(BoolOption::No) { + crate::clipboard::try_empty_clipboard_files(crate::clipboard::ClipboardSide::Client); + } if !name.contains("block-input") { self.save_config(config); } @@ -3243,7 +3297,7 @@ pub enum Data { CancelJob(i32), RemovePortForward(i32), AddPortForward((i32, String, i32)), - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] ToggleClipboardFile, NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bdfa8f1e2df1..7168105c1817 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,13 +1,3 @@ -use std::{ - collections::HashMap, - ffi::c_void, - num::NonZeroI64, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, RwLock, - }, -}; - #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(not(any(target_os = "ios")))] @@ -20,7 +10,11 @@ use crate::{ common::get_default_sound_input, ui_session_interface::{InvokeUiSession, Session}, }; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; +#[cfg(feature = "unix-file-copy-paste")] +use clipboard::platform::unix::{init_fuse_context, uninit_fuse_context}; +#[cfg(target_os = "windows")] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; #[cfg(not(target_os = "ios"))] @@ -47,6 +41,15 @@ use hbb_common::{ #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use scrap::CodecFormat; +use std::{ + collections::HashMap, + ffi::c_void, + num::NonZeroI64, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, + }, +}; pub struct Remote { handler: Session, @@ -122,7 +125,7 @@ impl Remote { } pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] let _file_clip_context_holder = { // `is_port_forward()` will not reach here, but we still check it for clarity. if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { @@ -242,8 +245,8 @@ impl Remote { } } _msg = rx_clip_client.recv() => { - #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] - self.handle_local_clipboard_msg(&mut peer, _msg).await; + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + self.handle_local_clipboard_msg(&mut peer, _msg).await; } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -323,7 +326,7 @@ impl Remote { Client::try_stop_clipboard(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] if _set_disconnected_ok { let conn_id = self.client_conn_id; log::debug!("try empty cliprdr for conn_id {}", conn_id); @@ -365,12 +368,18 @@ impl Remote { view_only, stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled ); if stop { - ContextSend::set_is_stopped(); + #[cfg(target_os = "windows")] + { + ContextSend::set_is_stopped(); + } } else { + #[cfg(target_os = "windows")] if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); // to-do: Show msgbox with "Don't show again" option }; + #[cfg(feature = "unix-file-copy-paste")] + let _ = init_fuse_context(true).is_ok(); log::debug!("Send system clipboard message to remote"); let msg = crate::clipboard_file::clip_2_msg(clip); allow_err!(peer.send(&msg).await); @@ -509,7 +518,7 @@ impl Remote { .handle_login_from_ui(os_username, os_password, password, remember, peer) .await; } - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } @@ -1221,7 +1230,7 @@ impl Remote { let peer_platform = pi.platform.clone(); self.set_peer_info(&pi); self.handler.handle_peer_info(pi); - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] @@ -1316,9 +1325,9 @@ impl Remote { crate::clipboard::handle_msg_multi_clipboards(_mcb); } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] Some(message::Union::Cliprdr(clip)) => { - self.handle_cliprdr_msg(clip); + self.handle_cliprdr_msg(clip, peer).await; } Some(message::Union::FileResponse(fr)) => { match fr.union { @@ -1483,6 +1492,9 @@ impl Remote { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(feature = "flutter")] + #[cfg(feature = "unix-file-copy-paste")] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { @@ -1501,7 +1513,14 @@ impl Remote { if !p.enabled && self.handler.is_file_transfer() { return true; } + #[cfg(feature = "flutter")] + #[cfg(feature = "unix-file-copy-paste")] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("file", p.enabled); + #[cfg(feature = "unix-file-copy-paste")] + if !p.enabled { + try_empty_clipboard_files(ClipboardSide::Client); + } } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); @@ -1921,24 +1940,19 @@ impl Remote { true } - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] fn check_clipboard_file_context(&self) { - #[cfg(any( - target_os = "windows", - all( - feature = "unix-file-copy-paste", - any(target_os = "linux", target_os = "macos") - ) - ))] - { - let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() - && self.handler.lc.read().unwrap().enable_file_copy_paste.v; - ContextSend::enable(enabled); - } + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() + && self.handler.lc.read().unwrap().enable_file_copy_paste.v; + ContextSend::enable(enabled); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + async fn handle_cliprdr_msg( + &self, + clip: hbb_common::message_proto::Cliprdr, + _peer: &mut Stream, + ) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { @@ -1960,15 +1974,28 @@ impl Remote { log::debug!( "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, file_transfer_enabled); + println!( + "REMOVE ME ========================== Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, file_transfer_enabled); if !stop { + #[cfg(target_os = "windows")] if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); }; + #[cfg(target_os = "windows")] let _ = ContextSend::proc(|context| -> ResultType<()> { context .server_clip_file(self.client_conn_id, clip) .map_err(|e| e.into()) }); + #[cfg(feature = "unix-file-copy-paste")] + if init_fuse_context(true).is_ok() { + if let Some(msg) = unix_file_clip::serve_clip_messages(true, clip, 0) { + allow_err!(_peer.send(&msg).await); + } + } else { + // send error message to server + } } } diff --git a/src/clipboard.rs b/src/clipboard.rs index ac3a83f00f72..bbc1d90b2f35 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -10,6 +10,8 @@ use std::{ }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; +#[cfg(feature = "unix-file-copy-paste")] +pub const FILE_CLIPBOARD_NAME: &'static str = "file-clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; // This format is used to store the flag in the clipboard. @@ -47,111 +49,6 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), ]; -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -static X11_CLIPBOARD: once_cell::sync::OnceCell = - once_cell::sync::OnceCell::new(); - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { - X11_CLIPBOARD - .get_or_try_init(|| x11_clipboard::Clipboard::new()) - .map_err(|e| e.to_string()) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -pub struct ClipboardContext { - string_setter: x11rb::protocol::xproto::Atom, - string_getter: x11rb::protocol::xproto::Atom, - text_uri_list: x11rb::protocol::xproto::Atom, - - clip: x11rb::protocol::xproto::Atom, - prop: x11rb::protocol::xproto::Atom, -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn parse_plain_uri_list(v: Vec) -> Result { - let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; - let mut list = String::new(); - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = percent_encoding::percent_decode_str(line) - .decode_utf8() - .map_err(|_| "ConversionFailure".to_owned())?; - list = list + "\n" + decoded.trim_start_matches("file://"); - } - list = list.trim().to_owned(); - Ok(list) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -impl ClipboardContext { - pub fn new() -> Result { - let clipboard = get_clipboard()?; - let string_getter = clipboard - .getter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let string_setter = clipboard - .setter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let text_uri_list = clipboard - .getter - .get_atom("text/uri-list") - .map_err(|e| e.to_string())?; - let prop = clipboard.getter.atoms.property; - let clip = clipboard.getter.atoms.clipboard; - Ok(Self { - text_uri_list, - string_setter, - string_getter, - clip, - prop, - }) - } - - pub fn get_text(&mut self) -> Result { - let clip = self.clip; - let prop = self.prop; - - const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120); - - let text_content = get_clipboard()? - .load(clip, self.string_getter, prop, TIMEOUT) - .map_err(|e| e.to_string())?; - - let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT)?; - - if file_urls.is_err() || file_urls.as_ref().is_empty() { - log::trace!("clipboard get text, no file urls"); - return String::from_utf8(text_content).map_err(|e| e.to_string()); - } - - let file_urls = parse_plain_uri_list(file_urls)?; - - let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?; - - if text_content.trim() == file_urls.trim() { - log::trace!("clipboard got text but polluted"); - return Err(String::from("polluted text")); - } - - Ok(text_content) - } - - pub fn set_text(&mut self, content: String) -> Result<(), String> { - let clip = self.clip; - - let value = content.clone().into_bytes(); - get_clipboard()? - .store(clip, self.string_setter, value) - .map_err(|e| e.to_string())?; - Ok(()) - } -} - #[cfg(not(target_os = "android"))] pub fn check_clipboard( ctx: &mut Option, @@ -179,6 +76,65 @@ pub fn check_clipboard( None } +#[cfg(feature = "unix-file-copy-paste")] +pub fn check_clipboard_files( + ctx: &mut Option, + side: ClipboardSide, + force: bool, +) -> Option> { + if ctx.is_none() { + *ctx = ClipboardContext::new().ok(); + } + let ctx2 = ctx.as_mut()?; + match ctx2.get_files(side, force) { + Ok(Some(urls)) => { + if !urls.is_empty() { + return Some(urls); + } + } + Err(e) => { + log::error!("Failed to get clipboard file urls. {}", e); + } + _ => {} + } + None +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn get_clipboard_file_urls( + ctx: &mut Option, + side: ClipboardSide, + force: bool, +) -> ResultType>> { + if ctx.is_none() { + *ctx = ClipboardContext::new().ok(); + } + if let Some(ctx) = ctx.as_mut() { + ctx.get_files(side, force) + } else { + bail!("Failed to create clipboard context") + } +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_clipboard_files(files: Vec, side: ClipboardSide) { + if !files.is_empty() { + std::thread::spawn(move || { + do_update_clipboard_(vec![ClipboardData::FileUrl(files)], side); + }); + } +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn try_empty_clipboard_files(side: ClipboardSide) { + std::thread::spawn(move || { + if let Ok(mut ctx) = ClipboardContext::new() { + ctx.try_empty_clipboard_files(); + clipboard::platform::unix::empty_local_files(side == ClipboardSide::Client); + } + }); +} + #[cfg(target_os = "windows")] pub fn check_clipboard_cm() -> ResultType { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); @@ -203,10 +159,15 @@ pub fn check_clipboard_cm() -> ResultType { #[cfg(not(target_os = "android"))] fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { - let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); + let to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { return; } + do_update_clipboard_(to_update_data, side); +} + +#[cfg(not(target_os = "android"))] +fn do_update_clipboard_(mut to_update_data: Vec, side: ClipboardSide) { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if ctx.is_none() { match ClipboardContext::new() { @@ -240,13 +201,11 @@ pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { } #[cfg(not(target_os = "android"))] -#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, } #[cfg(not(target_os = "android"))] -#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] #[allow(unreachable_code)] impl ClipboardContext { pub fn new() -> ResultType { @@ -293,7 +252,7 @@ impl ClipboardContext { // https://github.com/rustdesk/rustdesk/issues/9263 // https://github.com/rustdesk/rustdesk/issues/9222#issuecomment-2329233175 for i in 0..CLIPBOARD_GET_MAX_RETRY { - match self.inner.get_formats(SUPPORTED_FORMATS) { + match self.inner.get_formats(formats) { Ok(data) => { return Ok(data .into_iter() @@ -316,8 +275,17 @@ impl ClipboardContext { } pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + self.get_formats_filter(SUPPORTED_FORMATS, side, force) + } + + fn get_formats_filter( + &mut self, + formats: &[ClipboardFormat], + side: ClipboardSide, + force: bool, + ) -> ResultType> { let _lock = ARBOARD_MTX.lock().unwrap(); - let data = self.get_formats(SUPPORTED_FORMATS)?; + let data = self.get_formats(formats)?; if data.is_empty() { return Ok(data); } @@ -334,16 +302,62 @@ impl ClipboardContext { .into_iter() .filter(|c| match c { ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + // Skip synchronizing empty text to the remote clipboard + ClipboardData::Text(text) => !text.is_empty(), _ => true, }) .collect()) } + #[cfg(feature = "unix-file-copy-paste")] + pub fn get_files( + &mut self, + side: ClipboardSide, + force: bool, + ) -> ResultType>> { + let data = self.get_formats_filter( + &[ + ClipboardFormat::FileUrl, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), + ], + side, + force, + )?; + println!("REMOVE ME ============================ data: {:?}", &data); + Ok(data.into_iter().find_map(|c| match c { + ClipboardData::FileUrl(urls) => Some(urls), + _ => None, + })) + } + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { let _lock = ARBOARD_MTX.lock().unwrap(); self.inner.set_formats(data)?; Ok(()) } + + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_clipboard_files(&mut self) { + let _lock = ARBOARD_MTX.lock().unwrap(); + if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) { + let exclude_paths = clipboard::platform::unix::get_exclude_paths(); + let urls = data + .into_iter() + .filter_map(|c| match c { + ClipboardData::FileUrl(urls) => Some( + urls.into_iter() + .filter(|s| exclude_paths.iter().any(|p| s.starts_with(&**p))) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>(); + if !urls.is_empty() { + let _ = self.inner.clear(); + } + } + } } pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index a4bfc1aef699..3b87b52bf450 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -179,3 +179,211 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { _ => None, } } + +#[cfg(feature = "unix-file-copy-paste")] +pub mod unix_file_clip { + use super::{ + super::clipboard::{update_clipboard_files, ClipboardSide}, + *, + }; + use clipboard::platform::unix::*; + use hbb_common::{log, message_proto::*}; + use std::{ + collections::HashMap, + iter::FromIterator, + sync::{Arc, Mutex, RwLock}, + }; + + lazy_static::lazy_static! { + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); + } + + pub fn get_format_list() -> ClipboardFile { + let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) + .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); + let fc_format_name = get_local_format(FILECONTENTS_FORMAT_ID) + .unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); + ClipboardFile::FormatList { + format_list: vec![ + (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), + (FILECONTENTS_FORMAT_ID, fc_format_name), + ], + } + } + + #[inline] + fn msg_resp_format_data_failure() -> Message { + clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 0x2, + format_data: vec![], + }) + } + + #[inline] + fn resp_file_contents_fail(stream_id: i32) -> Message { + clip_2_msg(ClipboardFile::FileContentsResponse { + msg_flags: 0x2, + stream_id, + requested_data: vec![], + }) + } + + // to-do: conn_id may not be needed + pub fn serve_clip_messages( + is_client: bool, + clip: ClipboardFile, + conn_id: i32, + ) -> Option { + log::debug!("got clipfile from client peer"); + println!("REMVOE ME ============================= got clipfile from client peer, is_client: {}, clip: {:?}", is_client, &clip); + match clip { + ClipboardFile::MonitorReady => { + log::debug!("client is ready for clipboard"); + } + ClipboardFile::FormatList { format_list } => { + log::info!( + "REMOVE ME ========================= supported formats: {:?}", + &format_list + ); + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + log::error!("no file contents format found"); + return None; + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + log::error!("no file descriptor format found"); + return None; + }; + // sync file system from peer + let data = ClipboardFile::FormatDataRequest { + requested_format_id: file_descriptor_id, + }; + return Some(clip_2_msg(data)); + } + ClipboardFile::FormatListResponse { + msg_flags: _msg_flags, + } => {} + ClipboardFile::FormatDataRequest { + requested_format_id: _requested_format_id, + } => { + log::debug!("requested format id: {}", _requested_format_id); + match crate::clipboard::get_clipboard_file_urls( + &mut CLIPBOARD_CTX.lock().unwrap(), + crate::clipboard::ClipboardSide::Host, + false, + ) { + Ok(Some(files)) => { + if !files.is_empty() { + match clipboard::platform::unix::build_file_list_format_data( + is_client, &files, + ) { + Ok(format_data) => { + return Some(clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + })); + } + Err(e) => { + log::error!("build file list format data error: {:?}", e); + } + } + } + } + Ok(None) => { + log::error!("no file list found"); + } + Err(e) => { + log::error!("get file list error: {:?}", e); + } + } + return Some(msg_resp_format_data_failure()); + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!("format data response: msg_flags: {}", msg_flags); + + if msg_flags != 0x1 { + // return failure message? + } + + log::debug!("parsing file descriptors"); + match format_data_response_to_urls(is_client, format_data, conn_id) { + Ok(files) => { + log::info!( + "REMOVE ME =============================== load file list: {:?}", + files + ); + update_clipboard_files(files, ClipboardSide::Host); + } + Err(e) => { + log::error!("failed to parse file descriptors: {:?}", e); + } + } + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + .. + } => { + log::debug!("file contents request: stream_id: {}, list_index: {}, dw_flags: {}, n_position_low: {}, n_position_high: {}, cb_requested: {}", stream_id, list_index, dw_flags, n_position_low, n_position_high, cb_requested); + match read_file_contents( + is_client, + conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + ) { + Ok(data) => { + return Some(clip_2_msg(data)); + } + Err(e) => { + log::error!("failed to read file contents: {:?}", e); + return Some(resp_file_contents_fail(stream_id)); + } + } + } + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + .. + } => { + log::debug!( + "file contents response: msg_flags: {}, stream_id: {}", + msg_flags, + stream_id, + ); + hbb_common::allow_err!(handle_file_content_response(is_client, clip)); + } + ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => { + log::debug!( + "notify callback: type: {}, title: {}, text: {}", + r#type, + title, + text + ); + } + } + None + } +} diff --git a/src/common.rs b/src/common.rs index 294ab97cc4f7..4a6c0f829cb3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -751,7 +751,6 @@ pub fn get_sysinfo() -> serde_json::Value { os = format!("{os} - {}", system.os_version().unwrap_or_default()); } let hostname = hostname(); // sys.hostname() return localhost on android in my test - use serde_json::json; #[cfg(any(target_os = "android", target_os = "ios"))] let out; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1056,7 +1055,6 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin } pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec) -> Map { - use serde_json::json; let mut fd_json = serde_json::Map::new(); fd_json.insert("id".into(), json!(id)); fd_json.insert("path".into(), json!(path)); diff --git a/src/flutter.rs b/src/flutter.rs index fe0a77e39d36..cfa682258f29 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1274,9 +1274,24 @@ pub fn update_text_clipboard_required() { Client::set_is_text_clipboard_required(is_required); } +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_file_clipboard_required() { + let is_required = sessions::get_sessions() + .iter() + .any(|s| s.is_file_clipboard_required()); + Client::set_is_text_clipboard_required(is_required); +} + #[cfg(not(target_os = "ios"))] -pub fn send_text_clipboard_msg(msg: Message) { +pub fn send_clipboard_msg(msg: Message, _is_file: bool) { for s in sessions::get_sessions() { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if s.is_file_clipboard_required() { + s.send(Data::Message(msg.clone())); + } + continue; + } if s.is_text_clipboard_required() { // Check if the client supports multi clipboards if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9e23b7b02674..5c1925dfd9ab 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -275,6 +275,12 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } + #[cfg(feature = "unix-file-copy-paste")] + if sessions::get_session_by_session_id(&session_id).is_some() + && value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE + { + crate::flutter::update_file_clipboard_required(); + } } pub fn session_toggle_privacy_mode(session_id: SessionID, impl_key: String, on: bool) { @@ -1948,13 +1954,7 @@ pub fn main_hide_dock() -> SyncReturn { } pub fn main_has_file_clipboard() -> SyncReturn { - let ret = cfg!(any( - target_os = "windows", - all( - feature = "unix-file-copy-paste", - any(target_os = "linux", target_os = "macos") - ) - )); + let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",)); SyncReturn(ret) } diff --git a/src/ipc.rs b/src/ipc.rs index f1deb5ba8e5c..5f533cd94abb 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -25,9 +25,7 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, - sodiumoxide::base64, - timeout, + log, password_security as password, timeout, tokio::{ self, io::{AsyncRead, AsyncWrite}, @@ -230,7 +228,7 @@ pub enum Data { FS(FS), Test, SyncConfig(Option>), - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(target_os = "windows")] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), #[cfg(target_os = "windows")] diff --git a/src/server.rs b/src/server.rs index ba1682f3d0f5..117b700c17b3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,7 +106,13 @@ pub fn new() -> ServerPtr { #[cfg(not(target_os = "ios"))] { server.add_service(Box::new(display_service::new())); - server.add_service(Box::new(clipboard_service::new())); + server.add_service(Box::new(clipboard_service::new( + clipboard_service::NAME.to_owned(), + ))); + #[cfg(feature = "unix-file-copy-paste")] + server.add_service(Box::new(clipboard_service::new( + clipboard_service::FILE_NAME.to_owned(), + ))); } #[cfg(not(any(target_os = "android", target_os = "ios")))] { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 8ae482500550..218a9d0b3d99 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,18 +1,24 @@ use super::*; #[cfg(not(target_os = "android"))] pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::clipboard::check_clipboard_files; +#[cfg(feature = "unix-file-copy-paste")] +pub use crate::clipboard::FILE_CLIPBOARD_NAME as FILE_NAME; pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; #[cfg(windows)] use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; +#[cfg(feature = "unix-file-copy-paste")] +use clipboard::platform::unix::{init_fuse_context, uninit_fuse_context}; #[cfg(not(target_os = "android"))] -use clipboard_master::{CallbackResult, ClipboardHandler}; +use clipboard_master::CallbackResult; #[cfg(target_os = "android")] use hbb_common::config::{keys, option2bool}; #[cfg(target_os = "android")] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ io, - sync::mpsc::{channel, RecvTimeoutError, Sender}, + sync::mpsc::{channel, RecvTimeoutError}, time::Duration, }; #[cfg(windows)] @@ -23,9 +29,7 @@ static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); #[cfg(not(target_os = "android"))] struct Handler { - sp: EmptyExtraFieldService, ctx: Option, - tx_cb_result: Sender, #[cfg(target_os = "windows")] stream: Option>, #[cfg(target_os = "windows")] @@ -37,39 +41,45 @@ pub fn is_clipboard_service_ok() -> bool { CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst) } -pub fn new() -> GenericService { - let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); GenericService::run(&svc.clone(), run); svc.sp } #[cfg(not(target_os = "android"))] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + #[cfg(feature = "unix-file-copy-paste")] + let _fuse_call_on_ret = init_fuse_context(false).map(|_| crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + uninit_fuse_context(false); + }), + }); + let (tx_cb_result, rx_cb_result) = channel(); - let handler = Handler { - sp: sp.clone(), - ctx: Some(ClipboardContext::new()?), - tx_cb_result, + let ctx = Some(ClipboardContext::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?); + clipboard_listener::subscribe(sp.name(), tx_cb_result.clone())?; + let mut handler = Handler { + ctx, #[cfg(target_os = "windows")] stream: None, #[cfg(target_os = "windows")] rt: None, }; - let (tx_start_res, rx_start_res) = channel(); - let h = crate::clipboard::start_clipbard_master_thread(handler, tx_start_res); - let shutdown = match rx_start_res.recv() { - Ok((Some(s), _)) => s, - Ok((None, err)) => { - bail!(err); - } - Err(e) => { - bail!("Failed to create clipboard listener: {}", e); - } - }; - while sp.ok() { match rx_cb_result.recv_timeout(Duration::from_millis(INTERVAL)) { + Ok(CallbackResult::Next) => { + #[cfg(feature = "unix-file-copy-paste")] + if sp.name() == FILE_NAME { + handler.check_clipboard_file(); + continue; + } + if let Some(msg) = handler.get_clipboard_msg() { + sp.send(msg); + } + } Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); break; @@ -81,33 +91,30 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { _ => {} } } - shutdown.signal(); - h.join().ok(); + + clipboard_listener::unsubscribe(&sp.name()); Ok(()) } #[cfg(not(target_os = "android"))] -impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - if self.sp.ok() { - if let Some(msg) = self.get_clipboard_msg() { - self.sp.send(msg); +impl Handler { + #[cfg(feature = "unix-file-copy-paste")] + fn check_clipboard_file(&mut self) { + if clipboard::platform::unix::is_fuse_context_inited(false) { + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) { + if !urls.is_empty() { + use crate::clipboard_file::unix_file_clip; + // Use `send_data()` here to reuse `handle_file_clip()` in `connection.rs`. + hbb_common::allow_err!(clipboard::send_data( + 0, + unix_file_clip::get_format_list() + )); + } } } - CallbackResult::Next } - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - self.tx_cb_result - .send(CallbackResult::StopWithError(error)) - .ok(); - CallbackResult::Next - } -} - -#[cfg(not(target_os = "android"))] -impl Handler { fn get_clipboard_msg(&mut self) -> Option { #[cfg(target_os = "windows")] if crate::common::is_server() && crate::platform::is_root() { @@ -144,6 +151,7 @@ impl Handler { } } } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) } @@ -244,3 +252,97 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst); Ok(()) } + +// We need this mod to notify multiple subscribers when the clipboard changes. +// Because only one clipboard master(listener) can tigger the clipboard change event multiple listeners are created on Linux(x11). +// https://github.com/rustdesk-org/clipboard-master/blob/4fb62e5b62fb6350d82b571ec7ba94b3cd466695/src/master/x11.rs#L226 +#[cfg(not(target_os = "android"))] +mod clipboard_listener { + use clipboard_master::{CallbackResult, ClipboardHandler, Shutdown}; + use hbb_common::{bail, ResultType}; + use std::{ + collections::HashMap, + io, + sync::mpsc::{channel, Sender}, + sync::{Arc, Mutex}, + thread::JoinHandle, + }; + + lazy_static::lazy_static! { + pub static ref CLIPBOARD_LISTENER: Arc> = Default::default(); + } + + struct Handler { + subscribers: Arc>>>, + } + + impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::Next).ok(); + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + 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( + io::ErrorKind::Other, + msg.clone(), + ))) + .ok(); + } + CallbackResult::Next + } + } + + #[derive(Default)] + pub struct ClipboardListener { + subscribers: Arc>>>, + handle: Option<(Shutdown, JoinHandle<()>)>, + } + + pub fn subscribe(name: String, tx: Sender) -> ResultType<()> { + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + listener_lock.subscribers.lock().unwrap().insert(name, tx); + + if listener_lock.handle.is_none() { + let handler = Handler { + subscribers: listener_lock.subscribers.clone(), + }; + let (tx_start_res, rx_start_res) = channel(); + let h = crate::clipboard::start_clipbard_master_thread(handler, tx_start_res); + let shutdown = match rx_start_res.recv() { + Ok((Some(s), _)) => s, + Ok((None, err)) => { + bail!(err); + } + + Err(e) => { + bail!("Failed to create clipboard listener: {}", e); + } + }; + listener_lock.handle = Some((shutdown, h)); + } + + Ok(()) + } + + pub fn unsubscribe(name: &str) { + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + let is_empty = { + let mut sub_lock = listener_lock.subscribers.lock().unwrap(); + sub_lock.remove(name); + sub_lock.is_empty() + }; + if is_empty { + if let Some((shutdown, h)) = listener_lock.handle.take() { + shutdown.signal(); + h.join().ok(); + } + } + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 153740c28a3d..f61f8390c59d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,4 +1,6 @@ use super::{input_service::*, *}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::clipboard::try_empty_clipboard_files; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -443,6 +445,20 @@ impl Connection { std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned)); let mut second_timer = crate::rustdesk_interval(time::interval(Duration::from_secs(1))); + #[cfg(feature = "unix-file-copy-paste")] + let rx_clip1; + let mut rx_clip; + let _tx_clip: mpsc::UnboundedSender; + #[cfg(feature = "unix-file-copy-paste")] + { + rx_clip1 = clipboard::get_rx_cliprdr_server(id); + rx_clip = rx_clip1.lock().await; + } + #[cfg(not(feature = "unix-file-copy-paste"))] + { + (_tx_clip, rx_clip) = mpsc::unbounded_channel::(); + } + loop { tokio::select! { // biased; // video has higher priority // causing test_delay_timer failed while transferring big file @@ -490,6 +506,12 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, conn.inner.clone(), conn.can_sub_clipboard_service()); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); @@ -515,6 +537,10 @@ impl Connection { } else if &name == "file" { conn.file = enabled; conn.send_permission(Permission::File, enabled).await; + #[cfg(feature = "unix-file-copy-paste")] + if !enabled { + conn.try_empty_file_clipboard(); + } } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; @@ -529,7 +555,7 @@ impl Connection { ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } - #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] ipc::Data::ClipboardFile(clip) => { allow_err!(conn.stream.send(&clip_2_msg(clip)).await); } @@ -738,6 +764,17 @@ impl Connection { } video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(conn.inner.id(), conn.delay_response_instant.elapsed().as_millis()); } + clip_file = rx_clip.recv() => match clip_file { + Some(_clip) => { + #[cfg(feature = "unix-file-copy-paste")] + { + conn.handle_file_clip(_clip).await; + } + } + None => { + // + } + }, } } @@ -1200,15 +1237,27 @@ impl Connection { ); } - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ) - ))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { - platform_additions.insert("has_file_clipboard".into(), json!(true)); + // to-do: support cross-platform file clipboard + let is_same_platform = if cfg!(target_os = "windows") { + self.lr.my_platform == whoami::Platform::Windows.to_string() + } else if cfg!(target_os = "linux") { + // to-do: support wayland + if crate::ui_interface::current_is_wayland() { + false + } else { + self.lr.my_platform == whoami::Platform::Linux.to_string() + } + } else { + // to-do: We do not support file clipboard on macOS for now. + // Though copy&paste works fine between macOS and Linux. + // Because https://github.com/macfuse/macfuse/wiki/Getting-Started#enabling-support-for-third-party-kernel-extensions-apple-silicon-macs + false + }; + if is_same_platform { + platform_additions.insert("has_file_clipboard".into(), json!(is_same_platform)); + } } #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] @@ -1373,6 +1422,10 @@ impl Connection { if !self.can_sub_clipboard_service() { noperms.push(super::clipboard_service::NAME); } + #[cfg(feature = "unix-file-copy-paste")] + if !self.can_sub_file_clipboard_service() { + noperms.push(super::clipboard_service::FILE_NAME); + } if !self.audio_enabled() { noperms.push(super::audio_service::NAME); } @@ -1453,11 +1506,17 @@ impl Connection { self.audio && !self.disable_audio } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] fn file_transfer_enabled(&self) -> bool { self.file && self.enable_file_transfer } + #[cfg(feature = "unix-file-copy-paste")] + fn can_sub_file_clipboard_service(&self) -> bool { + self.file_transfer_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) != "Y" + } + fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { self.send_to_cm(ipc::Data::Login { id: self.inner.id(), @@ -2112,13 +2171,19 @@ impl Connection { #[cfg(target_os = "android")] crate::clipboard::handle_msg_multi_clipboards(_mcb); } - Some(message::Union::Cliprdr(_clip)) => - { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + Some(message::Union::Cliprdr(_clip)) => { + #[cfg(target_os = "windows")] if let Some(clip) = msg_2_clip(_clip) { - log::debug!("got clipfile from client peer"); self.send_to_cm(ipc::Data::ClipboardFile(clip)) } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(clip) = msg_2_clip(_clip) { + if let Some(msg) = + unix_file_clip::serve_clip_messages(false, clip, self.inner.id()) + { + self.send(msg).await; + } + } } Some(message::Union::FileAction(fa)) => { if self.file_transfer.is_some() { @@ -2910,13 +2975,26 @@ impl Connection { } } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; + #[cfg(target_os = "windows")] self.send_to_cm(ipc::Data::ClipboardFileEnabled( self.file_transfer_enabled(), )); + #[cfg(feature = "unix-file-copy-paste")] + if !self.enable_file_transfer { + self.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); + } } } if let Ok(q) = o.disable_clipboard.enum_value() { @@ -2940,6 +3018,12 @@ impl Connection { self.inner.clone(), self.can_sub_clipboard_service(), ); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); s.write().unwrap().subscribe( NAME_CURSOR, self.inner.clone(), @@ -3322,6 +3406,41 @@ impl Connection { session_id: self.lr.session_id, } } + + #[cfg(feature = "unix-file-copy-paste")] + async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { + let is_stopping_allowed = clip.is_stopping_allowed(); + let is_clipboard_enabled = self.clipboard; + let file_transfer_enabled = self.file_transfer_enabled(); + let stop = is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); + log::debug!( + "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); + println!( + "REMOVE ME =========================== Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); + if !stop { + use hbb_common::config::keys::OPTION_ONE_WAY_FILE_TRANSFER; + if !clip.is_beginning_message() + || crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) != "Y" + { + // Maybe we should end the connection, because copy&paste files causes everything to wait. + allow_err!( + self.stream + .send(&crate::clipboard_file::clip_2_msg(clip)) + .await + ); + } + } + } + + #[inline] + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_file_clipboard(&mut self) { + // No need to check if current clipboard files are set by this connection or the other ones. + // It's a rare case. + try_empty_clipboard_files(ClipboardSide::Host); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/ui/header.tis b/src/ui/header.tis index 4b634cf54c5a..ba247ae6d335 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -174,6 +174,17 @@ class Header: Reactor.Component { } } + var is_file_copy_paste_supported = false; + if (handler.version_cmp(pi.version, '1.2.4') < 0) { + is_file_copy_paste_supported = is_win && pi.platform == "Windows"; + } else { + if (handler.current_is_wayland()) { + // to-do: support wayalnd + } else { + is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions.has_file_clipboard; + } + } + return
  • {translate('Adjust Window')}
  • @@ -201,7 +212,7 @@ class Header: Reactor.Component { {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} - {(is_win && pi.platform == "Windows") && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} + {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0296d82bda56..d57da22670ec 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -66,6 +66,39 @@ impl SciterHandler { } displays_value } + + fn make_platform_additions(data: &str) -> Option { + if let Ok(v2) = serde_json::from_str::>(data) { + let mut value = Value::map(); + for (k, v) in v2 { + match v { + serde_json::Value::String(s) => { + value.set_item(k, s); + } + serde_json::Value::Number(n) => { + if let Some(n) = n.as_i64() { + value.set_item(k, n as i32); + } else if let Some(n) = n.as_f64() { + value.set_item(k, n); + } + } + serde_json::Value::Bool(b) => { + value.set_item(k, b); + } + _ => { + // ignore for now + } + } + } + if value.len() > 0 { + return Some(value); + } else { + None + } + } else { + None + } + } } impl InvokeUiSession for SciterHandler { @@ -245,6 +278,9 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); pi_sciter.set_item("version", pi.version.clone()); + if let Some(v) = Self::make_platform_additions(&pi.platform_additions) { + pi_sciter.set_item("platform_additions", v); + } self.call("updatePi", &make_args!(pi_sciter)); } @@ -500,6 +536,7 @@ impl sciter::EventHandler for SciterSession { fn version_cmp(String, String); fn set_selected_windows_session_id(String); fn is_recording(); + fn has_file_clipboard(); } } @@ -607,6 +644,10 @@ impl SciterSession { self.send_selected_session_id(u_sid); } + fn has_file_clipboard(&self) -> bool { + cfg!(any(target_os = "windows", feature = "unix-file-copy-paste")) + } + fn get_port_forwards(&mut self) -> Value { let port_forwards = self.lc.read().unwrap().port_forwards.clone(); let mut v = Value::array(0); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index a3373f8ccd72..799d6addeb89 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -4,7 +4,7 @@ use crate::ipc::ClipboardNonFile; use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] use crate::ipc::{self, Data}; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(target_os = "windows")] use clipboard::ContextSend; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::unbounded_channel; @@ -71,9 +71,9 @@ struct IpcTaskRunner { close: bool, running: bool, conn_id: i32, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled: bool, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled_peer: bool, } @@ -169,7 +169,7 @@ impl ConnectionManager { } #[inline] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] fn is_authorized(&self, id: i32) -> bool { CLIENTS .read() @@ -190,7 +190,7 @@ impl ConnectionManager { .map(|c| c.disconnected = true); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { let _ = ContextSend::proc(|context| -> ResultType<()> { context.empty_clipboard(id)?; @@ -345,14 +345,14 @@ impl IpcTaskRunner { // for tmp use, without real conn id let mut write_jobs: Vec = Vec::new(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] let is_authorized = self.cm.is_authorized(self.conn_id); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] let rx_clip1; let mut rx_clip; let _tx_clip; - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] if self.conn_id > 0 && is_authorized { log::debug!("Clipboard is enabled from client peer: type 1"); rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id); @@ -364,12 +364,12 @@ impl IpcTaskRunner { rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); rx_clip = rx_clip1.lock().await; } - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] + #[cfg(not(target_os = "windows"))] { (_tx_clip, rx_clip) = unbounded_channel::(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { if ContextSend::is_enabled() { log::debug!("Clipboard is enabled"); @@ -397,7 +397,7 @@ impl IpcTaskRunner { log::debug!("conn_id: {}", id); self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; - #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] + #[cfg(target_os = "windows")] { self.file_transfer_enabled = _file_transfer_enabled; } @@ -438,34 +438,31 @@ impl IpcTaskRunner { Data::FileTransferLog((action, log)) => { self.cm.ui_handler.file_transfer_log(&action, &log); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(target_os = "windows")] Data::ClipboardFile(_clip) => { - #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] - { - let is_stopping_allowed = _clip.is_beginning_message(); - let is_clipboard_enabled = ContextSend::is_enabled(); - let file_transfer_enabled = self.file_transfer_enabled; - let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); - log::debug!( - "Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", - stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); - if stop { - ContextSend::set_is_stopped(); - } else { - if !is_authorized { - log::debug!("Clipboard message from client peer, but not authorized"); - continue; - } - let conn_id = self.conn_id; - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.server_clip_file(conn_id, _clip) - .map_err(|e| e.into()) - }); + let is_stopping_allowed = _clip.is_beginning_message(); + let is_clipboard_enabled = ContextSend::is_enabled(); + let file_transfer_enabled = self.file_transfer_enabled; + let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); + log::debug!( + "Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); + if stop { + ContextSend::set_is_stopped(); + } else { + if !is_authorized { + log::debug!("Clipboard message from client peer, but not authorized"); + continue; } + let conn_id = self.conn_id; + let _ = ContextSend::proc(|context| -> ResultType<()> { + context.server_clip_file(conn_id, _clip) + .map_err(|e| e.into()) + }); } } Data::ClipboardFileEnabled(_enabled) => { - #[cfg(any(target_os= "windows",target_os ="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { self.file_transfer_enabled_peer = _enabled; } @@ -543,7 +540,7 @@ impl IpcTaskRunner { } match &data { Data::SwitchPermission{name: _name, enabled: _enabled} => { - #[cfg(any(target_os="linux", target_os="windows", target_os = "macos"))] + #[cfg(target_os = "windows")] if _name == "file" { self.file_transfer_enabled = *_enabled; } @@ -558,7 +555,7 @@ impl IpcTaskRunner { }, clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { - #[cfg(any(target_os = "windows", target_os ="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { let is_stopping_allowed = _clip.is_stopping_allowed(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -602,9 +599,9 @@ impl IpcTaskRunner { close: true, running: true, conn_id: 0, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled: false, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled_peer: false, }; @@ -623,13 +620,7 @@ impl IpcTaskRunner { #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn start_ipc(cm: ConnectionManager) { - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ), - ))] + #[cfg(target_os = "windows")] ContextSend::enable(option2bool( OPTION_ENABLE_FILE_TRANSFER, &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 323b651fedb3..88e557f9a2c7 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -23,7 +23,6 @@ use serde_derive::Serialize; use std::process::Child; use std::{ collections::HashMap, - sync::atomic::{AtomicUsize, Ordering}, sync::{Arc, Mutex}, }; @@ -213,6 +212,7 @@ pub fn get_local_option(key: String) -> String { } #[inline] +#[cfg(feature = "flutter")] pub fn get_hard_option(key: String) -> String { config::HARD_SETTINGS .read() @@ -491,6 +491,7 @@ pub fn set_socks(proxy: String, username: String, password: String) { } #[inline] +#[cfg(feature = "flutter")] pub fn get_proxy_status() -> bool { #[cfg(not(any(target_os = "android", target_os = "ios")))] return ipc::get_proxy_status(); @@ -1150,13 +1151,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver bool { + *self.server_keyboard_enabled.read().unwrap() + && *self.server_file_transfer_enabled.read().unwrap() + && self.lc.read().unwrap().enable_file_copy_paste.v + } } impl Session { @@ -324,7 +330,7 @@ impl Session { pub fn toggle_option(&self, name: String) { let msg = self.lc.write().unwrap().toggle_option(name.clone()); - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] if name == hbb_common::config::keys::OPTION_ENABLE_FILE_COPY_PASTE { self.send(Data::ToggleClipboardFile); } @@ -361,6 +367,13 @@ impl Session { && !self.lc.read().unwrap().disable_clipboard.v } + #[cfg(feature = "unix-file-copy-paste")] + pub fn is_file_clipboard_required(&self) -> bool { + *self.server_keyboard_enabled.read().unwrap() + && *self.server_file_transfer_enabled.read().unwrap() + && self.lc.read().unwrap().enable_file_copy_paste.v + } + #[cfg(feature = "flutter")] pub fn refresh_video(&self, display: i32) { if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) {