diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index a795bfaed..1bdcd1401 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2025.01.13 VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.3.7" + VERSION: "1.3.8" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -415,7 +415,7 @@ jobs: - name: Build rustdesk run: | - ./build.py --flutter --hwcodec + ./build.py --flutter --hwcodec --unix-file-copy-paste - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -796,7 +796,7 @@ jobs: sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj fi - ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} + ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -1554,7 +1554,7 @@ jobs: 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 @@ -1706,7 +1706,7 @@ jobs: deb_arch: amd64, sciter_arch: x64, vcpkg-triplet: x64-linux, - extra_features: ",hwcodec", + extra_features: ",hwcodec,unix-file-copy-paste", } - { arch: armv7, @@ -1716,7 +1716,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 diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 7cb1f801c..b98cc9618 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.11.16 VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c" - VERSION: "1.3.7" + VERSION: "1.3.8" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 4b7622595..12057fc8f 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/rustdesk-org/arboard#4e16bad260ea05dd7dcdb68cc7549dad3920b940" 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", @@ -1707,7 +1708,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]] @@ -2231,17 +2232,17 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "fuser" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" dependencies = [ "libc", "log", "memchr", + "nix 0.29.0", "page_size", - "pkg-config", "smallvec", - "zerocopy 0.6.6", + "zerocopy 0.8.14", ] [[package]] @@ -4175,7 +4176,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.2", "proc-macro2 1.0.86", "quote 1.0.36", "syn 2.0.68", @@ -4508,9 +4509,9 @@ dependencies = [ [[package]] name = "page_size" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", "winapi 0.3.9", @@ -5506,7 +5507,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.7" +version = "1.3.8" dependencies = [ "android-wakelock", "android_logger", @@ -5606,7 +5607,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.7" +version = "1.3.8" dependencies = [ "brotli", "dirs 5.0.1", @@ -8074,16 +8075,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" -dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -8094,10 +8085,19 @@ dependencies = [ ] [[package]] -name = "zerocopy-derive" -version = "0.6.6" +name = "zerocopy" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -8106,9 +8106,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", diff --git a/Cargo.toml b/Cargo.toml index 637f515d2..034c790fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.7" +version = "1.3.8" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index a5ae4ef78..b49bd7ed9 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.7 + version: 1.3.8 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 25889d5b7..51fe2584e 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.7 + version: 1.3.8 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 387144999..c8dbfec85 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.7+56 +version: 1.3.8+57 environment: sdk: '^3.1.0' diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index c3673a9bd..9db2e5a99 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -34,7 +34,6 @@ parking_lot = {version = "0.12"} [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] rand = {version = "0.8", optional = true} -fuser = {version = "0.13", optional = true} libc = {version = "0.2", optional = true} dashmap = {version ="5.5", optional = true} utf16string = {version = "0.2", optional = true} @@ -44,6 +43,7 @@ once_cell = {version = "1.18", optional = true} percent-encoding = {version ="2.3", optional = true} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +fuser = {version = "0.15", default-features = false, optional = true} [target.'cfg(target_os = "macos")'.dependencies] cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true} diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 6bdd2293a..57e6ce617 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,24 +1,23 @@ -#[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(target_os = "windows")] +use hbb_common::ResultType; +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +use hbb_common::{allow_err, log}; use hbb_common::{ lazy_static, tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, }, - ResultType, }; 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 +27,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 @@ -41,7 +42,6 @@ pub trait CliprdrServiceContext: Send + Sync { fn set_is_stopped(&mut self) -> Result<(), CliprdrError>; /// clear the content on clipboard fn empty_clipboard(&mut self, conn_id: i32) -> Result; - /// run as a server for clipboard RPC fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>; } @@ -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), } @@ -199,37 +201,53 @@ 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<()> { +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +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(), + }) } } +#[inline] #[cfg(target_os = "windows")] pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { - use hbb_common::log; + // Need more tests to see if it's necessary to handle the error. for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { if msg_channel.conn_id != conn_id { allow_err!(msg_channel.sender.send(data.clone())); @@ -237,14 +255,13 @@ pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { } } -#[cfg(feature = "unix-file-copy-paste")] #[inline] -fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { +#[cfg(feature = "unix-file-copy-paste")] +fn send_data_to_all(data: ClipboardFile) { // Need more tests to see if it's necessary to handle the error. for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { allow_err!(msg_channel.sender.send(data.clone())); } - Ok(()) } #[cfg(test)] diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 5db271129..5bf1279cb 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,4 @@ 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/filetype.rs b/libs/clipboard/src/platform/unix/filetype.rs new file mode 100644 index 000000000..6387a3ece --- /dev/null +++ b/libs/clipboard/src/platform/unix/filetype.rs @@ -0,0 +1,188 @@ +use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA}; +use crate::CliprdrError; +use hbb_common::{ + bytes::{Buf, Bytes}, + log, +}; +use std::{ + path::PathBuf, + time::{Duration, SystemTime}, +}; +use utf16string::WStr; + +pub type Inode = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileType { + File, + Directory, + // todo: support symlink + Symlink, +} + +/// read only permission +pub const PERM_READ: u16 = 0o444; +/// read and write permission +pub const PERM_RW: u16 = 0o644; +/// only self can read and readonly +pub const PERM_SELF_RO: u16 = 0o400; +/// rwx +pub const PERM_RWX: u16 = 0o755; +/// max length of file name +pub const MAX_NAME_LEN: usize = 255; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileDescription { + pub conn_id: i32, + pub name: PathBuf, + pub kind: FileType, + pub atime: SystemTime, + pub last_modified: SystemTime, + pub last_metadata_changed: SystemTime, + pub creation_time: SystemTime, + + pub size: u64, + + pub perm: u16, +} + +impl FileDescription { + fn parse_file_descriptor( + bytes: &mut Bytes, + conn_id: i32, + ) -> Result { + let flags = bytes.get_u32_le(); + // skip reserved 32 bytes + bytes.advance(32); + let attributes = bytes.get_u32_le(); + + // in original specification, this is 16 bytes reserved + // we use the last 4 bytes to store the file mode + // skip reserved 12 bytes + bytes.advance(12); + let perm = bytes.get_u32_le() as u16; + + // last write time from 1601-01-01 00:00:00, in 100ns + let last_write_time = bytes.get_u64_le(); + // file size + let file_size_high = bytes.get_u32_le(); + let file_size_low = bytes.get_u32_le(); + // utf16 file name, double \0 terminated, in 520 bytes block + // read with another pointer, and advance the main pointer + let block = bytes.clone(); + bytes.advance(520); + + let block = &block[..520]; + let wstr = WStr::from_utf16le(block).map_err(|e| { + log::error!("cannot convert file descriptor path: {:?}", e); + CliprdrError::ConversionFailure + })?; + + let from_unix = flags & FLAGS_FD_UNIX_MODE != 0; + + let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0; + if !valid_attributes { + return Err(CliprdrError::InvalidRequest { + description: "file description must have valid attributes".to_string(), + }); + } + + // todo: check normal, hidden, system, readonly, archive... + let directory = attributes & 0x10 != 0; + let normal = attributes == 0x80; + let hidden = attributes & 0x02 != 0; + let readonly = attributes & 0x01 != 0; + + let perm = if from_unix { + // as is + perm + // cannot set as is... + } else if normal { + PERM_RWX + } else if readonly { + PERM_READ + } else if hidden { + PERM_SELF_RO + } else if directory { + PERM_RWX + } else { + PERM_RW + }; + + let kind = if directory { + FileType::Directory + } else { + FileType::File + }; + + // to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;` + // We use `true` to for compatibility with Windows. + // let valid_size = flags & FLAGS_FD_SIZE != 0; + let valid_size = true; + let size = if valid_size { + ((file_size_high as u64) << 32) + file_size_low as u64 + } else { + 0 + }; + + let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0; + let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { + let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; + let last_write_time = Duration::from_nanos(last_write_time); + SystemTime::UNIX_EPOCH + last_write_time + } else { + SystemTime::UNIX_EPOCH + }; + + let name = wstr.to_utf8().replace('\\', "/"); + let name = PathBuf::from(name.trim_end_matches('\0')); + + let desc = FileDescription { + conn_id, + name, + kind, + atime: last_modified, + last_modified, + last_metadata_changed: last_modified, + + creation_time: last_modified, + size, + perm, + }; + + Ok(desc) + } + + /// parse file descriptions from a format data response PDU + /// which containing a CSPTR_FILEDESCRIPTORW indicated format data + pub fn parse_file_descriptors( + file_descriptor_pdu: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let mut data = Bytes::from(file_descriptor_pdu); + if data.remaining() < 4 { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with infficient length".to_string(), + }); + } + + let count = data.get_u32_le() as usize; + if data.remaining() == 0 && count == 0 { + return Ok(Vec::new()); + } + + if data.remaining() != 592 * count { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with invalid length".to_string(), + }); + } + + let mut files = Vec::with_capacity(count); + for _ in 0..count { + let desc = Self::parse_file_descriptor(&mut data, conn_id)?; + files.push(desc); + } + + Ok(files) + } +} diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs similarity index 82% rename from libs/clipboard/src/platform/fuse.rs rename to libs/clipboard/src/platform/unix/fuse/cs.rs index c5fe60f56..0f1cf8739 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -31,33 +31,29 @@ use std::{ }; use fuser::{ReplyDirectory, FUSE_ROOT_ID}; -use hbb_common::{ - bytes::{Buf, Bytes}, - log, -}; +use hbb_common::log; use parking_lot::{Condvar, Mutex}; -use utf16string::WStr; -use crate::{send_data, ClipboardFile, CliprdrError}; - -use super::LDAP_EPOCH_DELTA; +use crate::{ + platform::unix::{ + filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX}, + BLOCK_SIZE, + }, + send_data, ClipboardFile, CliprdrError, +}; /// fuse server ready retry max times const READ_RETRY: i32 = 3; -/// block size for fuse, align to our asynchronic request size over FileContentsRequest. -pub const BLOCK_SIZE: u32 = 4 * 1024 * 1024; - -/// read only permission -const PERM_READ: u16 = 0o444; -/// read and write permission -const PERM_RW: u16 = 0o644; -/// only self can read and readonly -const PERM_SELF_RO: u16 = 0o400; -/// rwx -const PERM_RWX: u16 = 0o755; -/// max length of file name -const MAX_NAME_LEN: usize = 255; +impl From for fuser::FileType { + fn from(value: FileType) -> Self { + match value { + FileType::File => Self::RegularFile, + FileType::Directory => Self::Directory, + FileType::Symlink => Self::Symlink, + } + } +} /// fuse client /// this is a proxy to the fuse server @@ -150,9 +146,15 @@ impl fuser::Filesystem for FuseClient { server.release(req, ino, fh, _flags, _lock_owner, _flush, reply) } - fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + fn getattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: Option, + reply: fuser::ReplyAttr, + ) { let mut server = self.server.lock(); - server.getattr(req, ino, reply) + server.getattr(req, ino, fh, reply) } fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) { @@ -247,7 +249,6 @@ impl fuser::Filesystem for FuseServer { if parent_entry.attributes.kind != FileType::Directory { log::error!("fuse: parent is not a directory"); - reply.error(libc::ENOTDIR); return; } @@ -480,7 +481,13 @@ impl fuser::Filesystem for FuseServer { reply.ok(); } - fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + fn getattr( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: Option, + reply: fuser::ReplyAttr, + ) { let files = &self.files; let Some(entry) = files.get(ino as usize - 1) else { reply.error(libc::ENOENT); @@ -527,14 +534,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,16 +553,14 @@ 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; + // to-do: more tests needed loop { let reply = self.rx.recv_timeout(self.timeout).map_err(|e| { log::error!("failed to receive file list from channel: {:?}", e); @@ -590,7 +587,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); @@ -605,160 +605,6 @@ impl FuseServer { } } } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileDescription { - pub conn_id: i32, - pub name: PathBuf, - pub kind: FileType, - pub atime: SystemTime, - pub last_modified: SystemTime, - pub last_metadata_changed: SystemTime, - pub creation_time: SystemTime, - - pub size: u64, - - pub perm: u16, -} - -impl FileDescription { - fn parse_file_descriptor( - bytes: &mut Bytes, - conn_id: i32, - ) -> Result { - let flags = bytes.get_u32_le(); - // skip reserved 32 bytes - bytes.advance(32); - let attributes = bytes.get_u32_le(); - - // in original specification, this is 16 bytes reserved - // we use the last 4 bytes to store the file mode - // skip reserved 12 bytes - bytes.advance(12); - let perm = bytes.get_u32_le() as u16; - - // last write time from 1601-01-01 00:00:00, in 100ns - let last_write_time = bytes.get_u64_le(); - // file size - let file_size_high = bytes.get_u32_le(); - let file_size_low = bytes.get_u32_le(); - // utf16 file name, double \0 terminated, in 520 bytes block - // read with another pointer, and advance the main pointer - let block = bytes.clone(); - bytes.advance(520); - - let block = &block[..520]; - let wstr = WStr::from_utf16le(block).map_err(|e| { - log::error!("cannot convert file descriptor path: {:?}", e); - CliprdrError::ConversionFailure - })?; - - let from_unix = flags & 0x08 != 0; - - let valid_attributes = flags & 0x04 != 0; - if !valid_attributes { - return Err(CliprdrError::InvalidRequest { - description: "file description must have valid attributes".to_string(), - }); - } - - // todo: check normal, hidden, system, readonly, archive... - let directory = attributes & 0x10 != 0; - let normal = attributes == 0x80; - let hidden = attributes & 0x02 != 0; - let readonly = attributes & 0x01 != 0; - - let perm = if from_unix { - // as is - perm - // cannot set as is... - } else if normal { - PERM_RWX - } else if readonly { - PERM_READ - } else if hidden { - PERM_SELF_RO - } else if directory { - PERM_RWX - } else { - PERM_RW - }; - - let kind = if directory { - FileType::Directory - } else { - FileType::File - }; - - let valid_size = flags & 0x40 != 0; - let size = if valid_size { - ((file_size_high as u64) << 32) + file_size_low as u64 - } else { - 0 - }; - - let valid_write_time = flags & 0x20 != 0; - let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { - let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; - let last_write_time = Duration::from_nanos(last_write_time); - SystemTime::UNIX_EPOCH + last_write_time - } else { - SystemTime::UNIX_EPOCH - }; - - let name = wstr.to_utf8().replace('\\', "/"); - let name = PathBuf::from(name.trim_end_matches('\0')); - - let desc = FileDescription { - conn_id, - name, - kind, - atime: last_modified, - last_modified, - last_metadata_changed: last_modified, - - creation_time: last_modified, - size, - perm, - }; - - Ok(desc) - } - - /// parse file descriptions from a format data response PDU - /// which containing a CSPTR_FILEDESCRIPTORW indicated format data - pub fn parse_file_descriptors( - file_descriptor_pdu: Vec, - conn_id: i32, - ) -> Result, CliprdrError> { - let mut data = Bytes::from(file_descriptor_pdu); - if data.remaining() < 4 { - return Err(CliprdrError::InvalidRequest { - description: "file descriptor request with infficient length".to_string(), - }); - } - - let count = data.get_u32_le() as usize; - if data.remaining() == 0 && count == 0 { - return Ok(Vec::new()); - } - - if data.remaining() != 592 * count { - return Err(CliprdrError::InvalidRequest { - description: "file descriptor request with invalid length".to_string(), - }); - } - - let mut files = Vec::with_capacity(count); - for _ in 0..count { - let desc = Self::parse_file_descriptor(&mut data, conn_id)?; - files.push(desc); - } - - Ok(files) - } -} - /// a node in the FUSE file tree #[derive(Debug)] struct FuseNode { @@ -881,7 +727,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, } })?; @@ -902,26 +748,6 @@ impl FuseNode { } } -pub type Inode = u64; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileType { - File, - Directory, - // todo: support symlink - Symlink, -} - -impl From for fuser::FileType { - fn from(value: FileType) -> Self { - match value { - FileType::File => Self::RegularFile, - FileType::Directory => Self::Directory, - FileType::Symlink => Self::Symlink, - } - } -} - #[derive(Debug, Clone)] pub struct InodeAttributes { inode: Inode, @@ -1064,8 +890,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/unix/fuse/mod.rs b/libs/clipboard/src/platform/unix/fuse/mod.rs new file mode 100644 index 000000000..df743004f --- /dev/null +++ b/libs/clipboard/src/platform/unix/fuse/mod.rs @@ -0,0 +1,225 @@ +mod cs; + +use super::filetype::FileDescription; +use crate::{ClipboardFile, CliprdrError}; +use cs::FuseServer; +use fuser::MountOption; +use hbb_common::{config::APP_NAME, log}; +use parking_lot::Mutex; +use std::{ + path::PathBuf, + sync::{mpsc::Sender, Arc}, + time::Duration, +}; + +lazy_static::lazy_static! { + 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) + }; + + static ref FUSE_CONTEXT_CLIENT: Arc>> = Arc::new(Mutex::new(None)); + static ref FUSE_CONTEXT_SERVER: Arc>> = Arc::new(Mutex::new(None)); +} + +static FUSE_TIMEOUT: Duration = Duration::from_secs(3); + +pub fn get_exclude_paths(is_client: bool) -> Arc { + if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + } +} + +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() + } +} + +pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { + let mut fuse_context_lock = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + if fuse_context_lock.is_some() { + return Ok(()); + } + let mount_point = if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + }; + + let mount_point = std::path::PathBuf::from(&*mount_point); + let (server, tx) = FuseServer::new(FUSE_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()); + // to-do: ignore the error if the mount point is already mounted + // Because the sciter version uses separate processes as the controlling side. + 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, + conn_id: 0, + }; + *fuse_context_lock = Some(ctx); + Ok(()) +} + +pub fn uninit_fuse_context(is_client: bool) { + uninit_fuse_context_(is_client) +} + +pub fn format_data_response_to_urls( + is_client: bool, + format_data: Vec, + conn_id: i32, +) -> Result, CliprdrError> { + let mut ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_mut() + .ok_or(CliprdrError::CliprdrInit)? + .format_data_response_to_urls(format_data, conn_id) +} + +pub fn handle_file_content_response( + is_client: bool, + clip: ClipboardFile, +) -> Result<(), CliprdrError> { + // 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(()) +} + +pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .map(|c| c.empty_local_files(conn_id)) + .unwrap_or(false) +} + +struct FuseContext { + server: Arc>, + tx: Sender, + mount_point: PathBuf, + // stores fuse background session handle + session: Mutex>, + // Indicates the connection ID of that set the clipboard content + conn_id: i32, +} + +// 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, + }; + + fs::create_dir(mount_point).ok(); + fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok(); + + if let Err(e) = std::process::Command::new("umount") + .arg(mount_point) + .status() + { + log::warn!("umount {:?} may fail: {:?}", mount_point, e); + } +} + +fn uninit_fuse_context_(is_client: bool) { + if is_client { + let _ = FUSE_CONTEXT_CLIENT.lock().take(); + } else { + let _ = FUSE_CONTEXT_SERVER.lock().take(); + } +} + +impl Drop for FuseContext { + fn drop(&mut self) { + self.session.lock().take().map(|s| s.join()); + log::info!("unmounting clipboard FUSE from {}", self.mount_point.display()); + } +} + +impl FuseContext { + pub fn empty_local_files(&self, conn_id: i32) -> bool { + if conn_id != 0 && self.conn_id != conn_id { + return false; + } + let mut fuse_guard = self.server.lock(); + let _ = fuse_guard.load_file_list(vec![]); + true + } + + pub fn format_data_response_to_urls( + &mut self, + format_data: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; + + let paths = { + let mut fuse_guard = self.server.lock(); + fuse_guard.load_file_list(files)?; + self.conn_id = conn_id; + + 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()) + } +} diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index b609b8cc7..11d62cad8 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -1,3 +1,15 @@ +use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA}; +use crate::{ + platform::unix::{ + FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE, + FLAGS_FD_UNIX_MODE, + }, + CliprdrError, +}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; use std::{ collections::HashSet, fs::File, @@ -7,32 +19,11 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, time::SystemTime, }; - -use hbb_common::{ - bytes::{BufMut, BytesMut}, - log, -}; use utf16string::WString; -use crate::{ - platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA}, - CliprdrError, -}; - -/// has valid file attributes -const FLAGS_FD_ATTRIBUTES: u32 = 0x04; -/// has valid file size -const FLAGS_FD_SIZE: u32 = 0x40; -/// has valid last write time -const FLAGS_FD_LAST_WRITE: u32 = 0x20; -/// show progress -const FLAGS_FD_PROGRESSUI: u32 = 0x4000; -/// transferred from unix, contains file mode -/// P.S. this flag is not used in windows -const FLAGS_FD_UNIX_MODE: u32 = 0x08; - #[derive(Debug)] pub(super) struct LocalFile { + pub relative_root: PathBuf, pub path: PathBuf, pub handle: Option>, @@ -51,9 +42,9 @@ pub(super) struct LocalFile { } impl LocalFile { - pub fn try_open(path: &Path) -> Result { + pub fn try_open(relative_root: &Path, 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 +70,8 @@ impl LocalFile { Ok(Self { name, - path: path.clone(), + relative_root: relative_root.to_path_buf(), + path: path.to_path_buf(), handle, offset, size, @@ -121,7 +113,12 @@ impl LocalFile { let size_high = (self.size >> 32) as u32; let size_low = (self.size & (u32::MAX as u64)) as u32; - let path = self.path.to_string_lossy().to_string(); + let path = self + .path + .strip_prefix(&self.relative_root) + .unwrap_or(&self.path) + .to_string_lossy() + .into_owned(); let wstr: WString = WString::from(&path); let name = wstr.as_bytes(); @@ -172,12 +169,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 +185,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); @@ -219,6 +221,7 @@ impl LocalFile { pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { fn constr_file_lst( + relative_root: &Path, path: &Path, file_list: &mut Vec, visited: &mut HashSet, @@ -227,22 +230,28 @@ 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)?; + let local_file = LocalFile::try_open(relative_root, 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)?; + constr_file_lst(relative_root, &path, file_list, visited)?; } } Ok(()) @@ -251,8 +260,18 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C let mut file_list = Vec::new(); let mut visited = HashSet::new(); + let relative_root = paths + .first() + .ok_or(CliprdrError::InvalidRequest { + description: "empty file list".to_string(), + })? + .parent() + .ok_or(CliprdrError::InvalidRequest { + description: "empty parent".to_string(), + })? + .to_path_buf(); for path in paths { - constr_file_lst(path, &mut file_list, &mut visited)?; + constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?; } Ok(file_list) } @@ -263,7 +282,7 @@ mod file_list_test { use hbb_common::bytes::{BufMut, BytesMut}; - use crate::{platform::fuse::FileDescription, CliprdrError}; + use crate::{platform::unix::filetype::FileDescription, CliprdrError}; use super::LocalFile; @@ -277,6 +296,7 @@ mod file_list_test { #[inline] fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile { LocalFile { + relative_root: PathBuf::from("."), path: PathBuf::from(path), handle: None, name: name.to_string(), diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 34021d6bf..7e7aeccb1 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -1,48 +1,38 @@ -use std::{ - path::{Path, PathBuf}, - sync::{mpsc::Sender, Arc}, - time::Duration, -}; - use dashmap::DashMap; -use fuser::MountOption; -use hbb_common::{ - bytes::{BufMut, BytesMut}, - log, -}; use lazy_static::lazy_static; -use parking_lot::Mutex; -use crate::{ - platform::{fuse::FileDescription, unix::local_file::construct_file_list}, - send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, -}; - -use self::local_file::LocalFile; +mod filetype; +/// use FUSE for file pasting on these platforms #[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 fuse; pub mod local_file; +pub mod serv_files; -#[cfg(target_os = "linux")] -pub mod url; +/// has valid file attributes +pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04; +/// has valid file size +pub const FLAGS_FD_SIZE: u32 = 0x40; +/// has valid last write time +pub const FLAGS_FD_LAST_WRITE: u32 = 0x20; +/// show progress +pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000; +/// transferred from unix, contains file mode +/// P.S. this flag is not used in windows +pub const FLAGS_FD_UNIX_MODE: u32 = 0x08; // 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"; + +/// block size for fuse, align to our asynchronic request size over FileContentsRequest. +pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024; + +// 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; lazy_static! { static ref REMOTE_FORMAT_MAP: DashMap = DashMap::from_iter( @@ -58,541 +48,7 @@ lazy_static! { ); } -fn get_local_format(remote_id: i32) -> Option { +#[inline] +pub fn get_local_format(remote_id: i32) -> Option { REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) } - -fn add_remote_format(local_name: &str, remote_id: i32) { - REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string()); -} - -trait SysClipboard: Send + Sync { - fn start(&self); - - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; - fn get_file_list(&self) -> Vec; -} - -#[cfg(target_os = "linux")] -fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { - #[cfg(feature = "wayland")] - { - unimplemented!() - } - #[cfg(not(feature = "wayland"))] - { - use x11::*; - let x11_clip = X11Clipboard::new(ignore_path)?; - Ok(Box::new(x11_clip) as Box<_>) - } -} - -#[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<_>) -} - -#[derive(Debug)] -enum FileContentsRequest { - Size { - stream_id: i32, - file_idx: usize, - }, - - Range { - stream_id: i32, - file_idx: usize, - offset: u64, - length: u64, - }, -} - -pub struct ClipboardContext { - pub fuse_mount_point: PathBuf, - /// stores fuse background session handle - fuse_handle: Mutex>, - - /// a sender of clipboard file contents pdu to fuse server - fuse_tx: Sender, - fuse_server: Arc>, - - clipboard: Arc, - 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 - })?; - - let (fuse_server, fuse_tx) = FuseServer::new(timeout); - - let fuse_server = Arc::new(Mutex::new(fuse_server)); - - let clipboard = get_sys_clipboard(&fuse_mount_point)?; - let clipboard = Arc::from(clipboard) as Arc<_>; - let local_files = Mutex::new(vec![]); - - Ok(Self { - fuse_mount_point, - fuse_server, - fuse_tx, - fuse_handle: Mutex::new(None), - clipboard, - local_files, - }) - } - - pub fn run(&self) -> Result<(), CliprdrError> { - if !self.is_stopped() { - return Ok(()); - } - - 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); - - let clipboard = self.clipboard.clone(); - - std::thread::spawn(move || { - log::debug!("start listening clipboard"); - clipboard.start(); - }); - - Ok(()) - } - - /// 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); - Ok(()) - } - - fn serve_file_contents( - &self, - conn_id: i32, - request: FileContentsRequest, - ) -> Result<(), CliprdrError> { - let mut file_list = self.local_files.lock(); - - let (file_idx, file_contents_resp) = match request { - FileContentsRequest::Size { - stream_id, - file_idx, - } => { - log::debug!("file contents (size) requested from conn: {}", conn_id); - let Some(file) = file_list.get(file_idx) else { - log::error!( - "invalid file index {} requested from conn: {}", - file_idx, - conn_id - ); - resp_file_contents_fail(conn_id, stream_id); - - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid file index {} requested from conn: {}", - file_idx, conn_id - ), - }); - }; - - log::debug!( - "conn {} requested file-{}: {}", - conn_id, - file_idx, - file.name - ); - - let size = file.size; - ( - file_idx, - ClipboardFile::FileContentsResponse { - msg_flags: 0x1, - stream_id, - requested_data: size.to_le_bytes().to_vec(), - }, - ) - } - FileContentsRequest::Range { - stream_id, - file_idx, - offset, - length, - } => { - log::debug!( - "file contents (range from {} length {}) request from conn: {}", - offset, - length, - conn_id - ); - let Some(file) = file_list.get_mut(file_idx) else { - log::error!( - "invalid file index {} requested from conn: {}", - file_idx, - conn_id - ); - resp_file_contents_fail(conn_id, stream_id); - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid file index {} requested from conn: {}", - file_idx, conn_id - ), - }); - }; - log::debug!( - "conn {} requested file-{}: {}", - conn_id, - file_idx, - file.name - ); - - if offset > file.size { - log::error!("invalid reading offset requested from conn: {}", conn_id); - resp_file_contents_fail(conn_id, stream_id); - - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid reading offset requested from conn: {}", - conn_id - ), - }); - } - let read_size = if offset + length > file.size { - file.size - offset - } else { - length - }; - - let mut buf = vec![0u8; read_size as usize]; - - file.read_exact_at(&mut buf, offset)?; - - ( - file_idx, - ClipboardFile::FileContentsResponse { - msg_flags: 0x1, - stream_id, - requested_data: buf, - }, - ) - } - }; - - 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) { - if !next_file.is_dir { - next_file.load_handle()?; - break; - } - } - Ok(()) - } -} - -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) - } -} - -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) - } - - fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - self.serve(conn_id, msg) - } -} - -fn resp_format_data_failure(conn_id: i32) { - let data = ClipboardFile::FormatDataResponse { - msg_flags: 0x2, - format_data: vec![], - }; - send_data(conn_id, data) -} - -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()); - let fc_format_name = - get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); - let format_list = ClipboardFile::FormatList { - format_list: vec![ - (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), - (FILECONTENTS_FORMAT_ID, fc_format_name), - ], - }; - - send_data(conn_id, format_list); - log::debug!("format list to remote dispatched, conn={}", conn_id); - Ok(()) -} - -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); - for file in files.iter() { - data.put(file.as_bin().as_slice()); - } - - 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 a9112fe62..000000000 --- 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/serv_files.rs b/libs/clipboard/src/platform/unix/serv_files.rs new file mode 100644 index 000000000..a401e0b5c --- /dev/null +++ b/libs/clipboard/src/platform/unix/serv_files.rs @@ -0,0 +1,231 @@ +use super::local_file::LocalFile; +use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use parking_lot::Mutex; +use std::{path::PathBuf, sync::Arc}; + +lazy_static::lazy_static! { + // 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. + static ref CLIP_FILES: Arc> = Default::default(); +} + +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, + + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, +} + +#[derive(Default)] +struct ClipFiles { + files: Vec, + file_list: Vec, + files_pdu: Vec, +} + +impl ClipFiles { + fn clear(&mut self) { + self.files.clear(); + self.file_list.clear(); + self.files_pdu.clear(); + } + + fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> { + let clipboard_paths = clipboard_files + .iter() + .map(|s| PathBuf::from(s)) + .collect::>(); + self.file_list = construct_file_list(&clipboard_paths)?; + self.files = clipboard_files.to_vec(); + Ok(()) + } + + fn build_file_list_pdu(&mut self) { + let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len()); + data.put_u32_le(self.file_list.len() as u32); + for file in self.file_list.iter() { + data.put(file.as_bin().as_slice()); + } + self.files_pdu = data.to_vec() + } + + fn serve_file_contents( + &mut self, + conn_id: i32, + request: FileContentsRequest, + ) -> Result { + let (file_idx, file_contents_resp) = match request { + FileContentsRequest::Size { + stream_id, + file_idx, + } => { + log::debug!("file contents (size) requested from conn: {}", conn_id); + let Some(file) = self.file_list.get(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + let size = file.size; + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: size.to_le_bytes().to_vec(), + }, + ) + } + FileContentsRequest::Range { + stream_id, + file_idx, + offset, + length, + } => { + log::debug!( + "file contents (range from {} length {}) request from conn: {}", + offset, + length, + conn_id + ); + let Some(file) = self.file_list.get_mut(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + if offset > file.size { + log::error!("invalid reading offset requested from conn: {}", conn_id); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid reading offset requested from conn: {}", + conn_id + ), + }); + } + let read_size = if offset + length > file.size { + file.size - offset + } else { + length + }; + + let mut buf = vec![0u8; read_size as usize]; + + file.read_exact_at(&mut buf, offset)?; + + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: buf, + }, + ) + } + }; + + log::debug!("file contents sent to conn: {}", conn_id); + // hot reload next file + for next_file in self.file_list.iter_mut().skip(file_idx + 1) { + if !next_file.is_dir { + next_file.load_handle()?; + break; + } + } + Ok(file_contents_resp) + } +} + +#[inline] +pub fn clear_files() { + CLIP_FILES.lock().clear(); +} + +pub fn read_file_contents( + 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}"), + }); + }; + + CLIP_FILES.lock().serve_file_contents(conn_id, fcr) +} + +pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> { + let mut files_lock = CLIP_FILES.lock(); + if files_lock.files == files { + return Ok(()); + } + files_lock.sync_files(files)?; + Ok(files_lock.build_file_list_pdu()) +} + +pub fn get_file_list_pdu() -> Vec { + CLIP_FILES.lock().files_pdu.clone() +} diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs deleted file mode 100644 index 126a341cd..000000000 --- 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 606ff6719..000000000 --- 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 3b931a4a6..1d8506ead 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -614,6 +614,7 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { e => Err(CliprdrError::Unknown(e)), } } + pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool { unsafe { TRUE == empty_cliprdr(context, conn_id as u32) } } diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index dc82f1ae2..132fbcc13 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.7" +version = "1.3.8" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index f27fd6d63..775108c8f 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.7 +pkgver=1.3.8 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index d56838d3c..add593685 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.7 +Version: 1.3.8 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 771c8a12e..b30d0f9de 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.7 +Version: 1.3.8 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index eb4a9a7ad..502340946 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.7 +Version: 1.3.8 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/src/client.rs b/src/client.rs index bdef9c7ed..319b3f3da 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,9 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::clipboard_listener; use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use clipboard_master::{CallbackResult, ClipboardHandler}; +use clipboard_master::CallbackResult; #[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -15,17 +17,25 @@ use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, ffi::c_void, - io, net::SocketAddr, ops::Deref, str::FromStr, sync::{ - mpsc::{self, RecvTimeoutError, Sender}, + mpsc::{self, RecvTimeoutError}, Arc, Mutex, RwLock, }, }; 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")))] @@ -62,14 +72,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")))] @@ -128,14 +130,19 @@ pub(crate) struct ClientClipboardContext; pub(crate) struct ClientClipboardContext { pub cfg: SessionPermissionConfig, pub tx: UnboundedSender, + #[cfg(feature = "unix-file-copy-paste")] + pub is_file_supported: bool, } /// Client of the remote desktop. 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, } @@ -151,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"; @@ -167,6 +174,8 @@ pub fn get_key_state(key: enigo::Key) -> bool { } impl Client { + const CLIENT_CLIPBOARD_NAME: &'static str = "client-clipboard"; + /// Start a new connection. pub async fn start( peer: &str, @@ -657,7 +666,13 @@ 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; + } + + #[inline] + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + pub fn set_is_file_clipboard_required(b: bool) { + CLIPBOARD_STATE.lock().unwrap().is_file_required = b; } #[cfg(not(target_os = "ios"))] @@ -673,68 +688,55 @@ impl Client { if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; } - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(not(target_os = "android"))] + clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME); + CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + clipboard::platform::unix::fuse::uninit_fuse_context(true); } // `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; } let (tx_cb_result, rx_cb_result) = mpsc::channel(); - let handler = ClientClipboardHandler { - ctx: None, - tx_cb_result, - #[cfg(not(feature = "flutter"))] - client_clip_ctx: _client_clip_ctx, - }; - - let (tx_start_res, rx_start_res) = mpsc::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)) => { - log::error!("{}", err); - return None; - } - Err(e) => { - log::error!("Failed to create clipboard listener: {}", e); - return None; - } - }; + if let Err(e) = + clipboard_listener::subscribe(Self::CLIENT_CLIPBOARD_NAME.to_owned(), tx_cb_result) + { + log::error!("Failed to subscribe clipboard listener: {}", e); + return None; + } clipboard_lock.running = true; - let (tx_started, rx_started) = unbounded_channel(); - log::info!("Start text clipboard loop"); + log::info!("Start client clipboard loop"); std::thread::spawn(move || { - let mut is_sent = false; + let mut handler = ClientClipboardHandler { + ctx: None, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: _client_clip_ctx, + }; + 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::Next) => { + handler.check_clipboard(); + } Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); break; @@ -744,13 +746,14 @@ impl Client { break; } Err(RecvTimeoutError::Timeout) => {} - _ => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } } } - log::info!("Stop text clipboard loop"); - shutdown.signal(); - h.join().ok(); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; }); Some(rx_started) @@ -758,31 +761,31 @@ 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; } clipboard_lock.running = true; - log::info!("Start text clipboard loop"); + log::info!("Start client 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; + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; }); None @@ -790,10 +793,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, } } @@ -802,62 +808,105 @@ impl TextClipboardState { #[cfg(not(any(target_os = "android", target_os = "ios")))] struct ClientClipboardHandler { ctx: Option, - tx_cb_result: Sender, #[cfg(not(feature = "flutter"))] client_clip_ctx: Option, } #[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) + } + } + + fn check_clipboard(&mut self) { + if CLIPBOARD_STATE.lock().unwrap().running { + #[cfg(feature = "unix-file-copy-paste")] + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { + if !urls.is_empty() { + if self.is_file_required() { + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + let msg = crate::clipboard_file::clip_2_msg( + unix_file_clip::get_format_list(), + ); + self.send_msg(msg, true); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } + } + return; + } + } + } + + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { + if self.is_text_required() { + self.send_msg(msg, 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 { + if ctx.is_file_supported { + 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)); } } } -#[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); - } - } - CallbackResult::Next - } - - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - self.tx_cb_result - .send(CallbackResult::StopWithError(error)) - .ok(); - CallbackResult::Next - } -} - /// Audio handler for the [`Client`]. #[derive(Default)] pub struct AudioHandler { @@ -1813,6 +1862,12 @@ 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, 0); + } + if !name.contains("block-input") { self.save_config(config); } @@ -2338,6 +2393,10 @@ impl LoginConfigHandler { }) .ok() } + + pub fn get_id(&self) -> &str { + &self.id + } } /// Media data. @@ -3240,7 +3299,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 fb7cba3c5..23d2f4094 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,9 @@ 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(target_os = "windows")] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; #[cfg(not(target_os = "ios"))] @@ -44,9 +36,18 @@ use hbb_common::{ }, Stream, }; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] 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, @@ -63,7 +64,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), is_connected: bool, first_frame: bool, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: i32, // used for file clipboard data_count: Arc, video_format: CodecFormat, @@ -107,7 +108,7 @@ impl Remote { last_update_jobs_status: (Instant::now(), Default::default()), is_connected: false, first_frame: false, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), video_format: CodecFormat::Unknown, @@ -122,7 +123,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() { @@ -175,26 +176,33 @@ impl Remote { } // just build for now - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] + #[cfg(not(any(target_os = "windows", feature = "unix-file-copy-paste")))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] let (_tx_holder, rx) = mpsc::unbounded_channel(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx)); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None); + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { let is_conn_not_default = self.handler.is_file_transfer() || self.handler.is_port_forward() || self.handler.is_rdp(); if !is_conn_not_default { - log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); - (self.client_conn_id, rx_clip_client_lock) = + (self.client_conn_id, rx_clip_client_holder.0) = clipboard::get_rx_cliprdr_client(&self.handler.get_id()); + log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); + let client_conn_id = self.client_conn_id; + rx_clip_client_holder.1 = Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(client_conn_id); + }), + }); }; } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let mut rx_clip_client = rx_clip_client_lock.lock().await; + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client = rx_clip_client_holder.0.lock().await; let mut status_timer = crate::rustdesk_interval(time::interval(Duration::new(1, 0))); @@ -242,8 +250,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", feature = "unix-file-copy-paste"))] + self.handle_local_clipboard_msg(&mut peer, _msg).await; } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -323,18 +331,13 @@ impl Remote { Client::try_stop_clipboard(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] if _set_disconnected_ok { - let conn_id = self.client_conn_id; - log::debug!("try empty cliprdr for conn_id {}", conn_id); - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(conn_id)?; - Ok(()) - }); + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_local_clipboard_msg( &self, peer: &mut crate::client::FramedStream, @@ -365,8 +368,12 @@ 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 @@ -509,7 +516,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 +1228,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")] @@ -1233,6 +1240,10 @@ impl Remote { crate::client::ClientClipboardContext { cfg: self.handler.get_permission_config(), tx: self.sender.clone(), + #[cfg(feature = "unix-file-copy-paste")] + is_file_supported: crate::is_support_file_copy_paste( + &peer_version, + ), }, )); // To make sure current text clipboard data is updated. @@ -1264,6 +1275,9 @@ impl Remote { #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + // on connection established client #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1317,9 +1331,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 { @@ -1484,6 +1498,8 @@ impl Remote { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { @@ -1502,7 +1518,16 @@ impl Remote { if !p.enabled && self.handler.is_file_transfer() { return true; } + #[cfg(all(feature = "flutter", 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, + self.client_conn_id, + ); + } } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); @@ -1922,24 +1947,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 { @@ -1956,20 +1976,34 @@ impl Remote { }; let is_stopping_allowed = clip.is_beginning_message(); - let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v; + let file_transfer_enabled = self.handler.is_file_clipboard_required(); let stop = is_stopping_allowed && !file_transfer_enabled; log::debug!( "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); }; - let _ = ContextSend::proc(|context| -> ResultType<()> { - context - .server_clip_file(self.client_conn_id, clip) - .map_err(|e| e.into()) - }); + #[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 crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) { + if let Some(msg) = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ) { + allow_err!(_peer.send(&msg).await); + } + } } } diff --git a/src/clipboard.rs b/src/clipboard.rs index ac3a83f00..4ab4bc666 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,15 +1,14 @@ #[cfg(not(target_os = "android"))] use arboard::{ClipboardData, ClipboardFormat}; -#[cfg(not(target_os = "android"))] -use clipboard_master::{ClipboardHandler, Master, Shutdown}; use hbb_common::{bail, log, message_proto::*, ResultType}; use std::{ - sync::{mpsc::Sender, Arc, Mutex}, - thread::JoinHandle, + sync::{Arc, Mutex}, time::Duration, }; 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. @@ -43,115 +42,12 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::ImageRgba, ClipboardFormat::ImagePng, ClipboardFormat::ImageSvg, + #[cfg(feature = "unix-file-copy-paste")] + ClipboardFormat::FileUrl, ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET), 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 +75,73 @@ 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 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, _conn_id: i32) { + #[cfg(target_os = "linux")] + std::thread::spawn(move || { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + return; + } + } + } + if let Some(mut ctx) = ctx.as_mut() { + use clipboard::platform::unix; + if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + ctx.try_empty_clipboard_files(_side); + } + } + }); +} + +#[cfg(target_os = "windows")] +pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) { + log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id); + let _ = clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(conn_id)?; + Ok(()) + }); +} + #[cfg(target_os = "windows")] pub fn check_clipboard_cm() -> ResultType { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); @@ -203,10 +166,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 +208,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 +259,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 +282,26 @@ impl ClipboardContext { } pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let data = self.get_formats_filter(SUPPORTED_FORMATS, side, force)?; + // We have a seperate service named `file-clipboard` to handle file copy-paste. + // We need to read the file urls because file copy may set the other clipboard formats such as text. + #[cfg(feature = "unix-file-copy-paste")] + { + if data.iter().any(|c| matches!(c, ClipboardData::FileUrl(_))) { + return Ok(vec![]); + } + } + Ok(data) + } + + 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 +318,98 @@ 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, + )?; + 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, side: ClipboardSide) { + let _lock = ARBOARD_MTX.lock().unwrap(); + if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) { + #[cfg(target_os = "linux")] + let exclude_path = + clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client); + #[cfg(target_os = "macos")] + let exclude_path: Arc = Default::default(); + let urls = data + .into_iter() + .filter_map(|c| match c { + ClipboardData::FileUrl(urls) => Some( + urls.into_iter() + .filter(|s| s.starts_with(&*exclude_path)) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>(); + if !urls.is_empty() { + // FIXME: + // The host-side clear file clipboard `let _ = self.inner.clear();`, + // does not work on KDE Plasma for the installed version. + + // Don't use `hbb_common::platform::linux::is_kde()` here. + // It's not correct in the server process. + #[cfg(target_os = "linux")] + let is_kde_x11 = { + let is_kde = std::process::Command::new("sh") + .arg("-c") + .arg("ps -e | grep -E kded[0-9]+ | grep -v grep") + .stdout(std::process::Stdio::piped()) + .output() + .map(|o| !o.stdout.is_empty()) + .unwrap_or(false); + is_kde && crate::platform::linux::is_x11() + }; + #[cfg(target_os = "macos")] + let is_kde_x11 = false; + let clear_holder_text = if is_kde_x11 { + "RustDesk placeholder to clear the file clipbard" + } else { + "" + } + .to_string(); + self.inner + .set_formats(&[ + ClipboardData::Text(clear_holder_text), + ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + )), + ]) + .ok(); + } + } + } } pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { @@ -427,36 +493,6 @@ impl std::fmt::Display for ClipboardSide { } } -#[cfg(not(target_os = "android"))] -pub fn start_clipbard_master_thread( - handler: impl ClipboardHandler + Send + 'static, - tx_start_res: Sender<(Option, String)>, -) -> JoinHandle<()> { - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. - let h = std::thread::spawn(move || match Master::new(handler) { - Ok(mut master) => { - tx_start_res - .send((Some(master.shutdown_channel()), "".to_owned())) - .ok(); - log::debug!("Clipboard listener started"); - if let Err(err) = master.run() { - log::error!("Failed to run clipboard listener: {}", err); - } else { - log::debug!("Clipboard listener stopped"); - } - } - Err(err) => { - tx_start_res - .send(( - None, - format!("Failed to create clipboard listener: {}", err), - )) - .ok(); - } - }); - h -} - pub use proto::get_msg_if_not_support_multi_clip; mod proto { #[cfg(not(target_os = "android"))] @@ -671,3 +707,140 @@ pub fn get_clipboards_msg(client: bool) -> Option { msg.set_multi_clipboards(clipboards); Some(msg) } + +// 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"))] +pub mod clipboard_listener { + use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; + use hbb_common::{bail, log, 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<()> { + log::info!("Subscribe clipboard listener: {}", &name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + listener_lock + .subscribers + .lock() + .unwrap() + .insert(name.clone(), tx); + + if listener_lock.handle.is_none() { + log::info!("Start clipboard listener thread"); + let handler = Handler { + subscribers: listener_lock.subscribers.clone(), + }; + let (tx_start_res, rx_start_res) = channel(); + let h = 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)); + log::info!("Clipboard listener thread started"); + } + + log::info!("Clipboard listener subscribed: {}", name); + Ok(()) + } + + pub fn unsubscribe(name: &str) { + log::info!("Unsubscribe clipboard listener: {}", name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + let is_empty = { + let mut sub_lock = listener_lock.subscribers.lock().unwrap(); + if let Some(tx) = sub_lock.remove(name) { + tx.send(CallbackResult::Stop).ok(); + } + sub_lock.is_empty() + }; + if is_empty { + if let Some((shutdown, h)) = listener_lock.handle.take() { + log::info!("Stop clipboard listener thread"); + shutdown.signal(); + h.join().ok(); + log::info!("Clipboard listener thread stopped"); + } + } + log::info!("Clipboard listener unsubscribed: {}", name); + } + + fn start_clipbard_master_thread( + handler: impl ClipboardHandler + Send + 'static, + tx_start_res: Sender<(Option, String)>, + ) -> JoinHandle<()> { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. + let h = std::thread::spawn(move || match Master::new(handler) { + Ok(mut master) => { + tx_start_res + .send((Some(master.shutdown_channel()), "".to_owned())) + .ok(); + log::debug!("Clipboard listener started"); + if let Err(err) = master.run() { + log::error!("Failed to run clipboard listener: {}", err); + } else { + log::debug!("Clipboard listener stopped"); + } + } + Err(err) => { + tx_start_res + .send(( + None, + format!("Failed to create clipboard listener: {}", err), + )) + .ok(); + } + }); + h + } +} diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index 4548cdbea..d7c72f981 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -189,3 +189,206 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { _ => None, } } + +#[cfg(feature = "unix-file-copy-paste")] +pub mod unix_file_clip { + use crate::clipboard::try_empty_clipboard_files; + + use super::{ + super::clipboard::{update_clipboard_files, ClipboardSide}, + *, + }; + #[cfg(target_os = "linux")] + use clipboard::platform::unix::fuse; + use clipboard::platform::unix::{ + get_local_format, serv_files, FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME, + FILEDESCRIPTORW_FORMAT_NAME, FILEDESCRIPTOR_FORMAT_ID, + }; + use hbb_common::log; + use std::sync::{Arc, Mutex}; + + 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![], + }) + } + + pub fn serve_clip_messages( + side: ClipboardSide, + clip: ClipboardFile, + conn_id: i32, + ) -> Option { + log::debug!("got clipfile from client peer"); + match clip { + ClipboardFile::MonitorReady => { + log::debug!("client is ready for clipboard"); + } + ClipboardFile::FormatList { 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); + let format_data = serv_files::get_file_list_pdu(); + if !format_data.is_empty() { + return Some(clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + })); + } + // empty file list, send failure message + return Some(msg_resp_format_data_failure()); + } + #[cfg(target_os = "linux")] + 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"); + if fuse::init_fuse_context(true).is_ok() { + match fuse::format_data_response_to_urls( + side == ClipboardSide::Client, + format_data, + conn_id, + ) { + Ok(files) => { + update_clipboard_files(files, side); + } + Err(e) => { + log::error!("failed to parse file descriptors: {:?}", e); + } + } + } else { + // send error message to server + } + } + 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 serv_files::read_file_contents( + 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)); + } + } + } + #[cfg(target_os = "linux")] + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + .. + } => { + log::debug!( + "file contents response: msg_flags: {}, stream_id: {}", + msg_flags, + stream_id, + ); + if fuse::init_fuse_context(true).is_ok() { + hbb_common::allow_err!(fuse::handle_file_content_response( + side == ClipboardSide::Client, + clip + )); + } else { + // send error message to server + } + } + ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => { + // unreachable, but still log it + log::debug!( + "notify callback: type: {}, title: {}, text: {}", + r#type, + title, + text + ); + } + ClipboardFile::TryEmpty => { + try_empty_clipboard_files(side, conn_id); + } + _ => { + log::error!("unsupported clipboard file type"); + } + } + None + } +} diff --git a/src/common.rs b/src/common.rs index 14db9b5d8..bcd7d3647 100644 --- a/src/common.rs +++ b/src/common.rs @@ -89,7 +89,7 @@ lazy_static::lazy_static! { pub struct SimpleCallOnReturn { pub b: bool, - pub f: Box, + pub f: Box, } impl Drop for SimpleCallOnReturn { @@ -127,6 +127,18 @@ pub fn is_support_multi_ui_session_num(ver: i64) -> bool { ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION) } +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste(ver: &str) -> bool { + is_support_file_copy_paste_num(hbb_common::get_version_number(ver)) +} + +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.3.8") +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { @@ -751,7 +763,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")))] @@ -1057,7 +1068,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 255a00e0f..2bc8066ed 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1305,9 +1305,26 @@ 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_file_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 crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version) + && 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 9e23b7b02..5c1925dfd 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 f1deb5ba8..5f533cd94 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 ba1682f3d..117b700c1 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 8ae482500..a3cb65174 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,18 +1,27 @@ use super::*; #[cfg(not(target_os = "android"))] +use crate::clipboard::clipboard_listener; +#[cfg(not(target_os = "android"))] pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; 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")] +pub use crate::{ + clipboard::{check_clipboard_files, FILE_CLIPBOARD_NAME as FILE_NAME}, + clipboard_file::unix_file_clip, +}; +#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] +use clipboard::platform::unix::fuse::{init_fuse_context, uninit_fuse_context}; #[cfg(not(target_os = "android"))] -use clipboard_master::{CallbackResult, 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 +32,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 +44,51 @@ 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(all(feature = "unix-file-copy-paste", target_os = "linux"))] + let _fuse_call_on_ret = { + if sp.name() == FILE_NAME { + Some(init_fuse_context(false).map(|_| crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + uninit_fuse_context(false); + }), + })) + } else { + None + } + }; + 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)?; + 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; @@ -78,36 +97,40 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { bail!("Clipboard listener stopped with error: {}", err); } Err(RecvTimeoutError::Timeout) => {} - _ => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } } } - 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 let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) { + if !urls.is_empty() { + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + // 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() + )); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } + } } } - 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 +167,7 @@ impl Handler { } } } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 326d12877..4ac552a42 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"))] @@ -6,8 +8,6 @@ use crate::clipboard_file::*; #[cfg(target_os = "android")] use crate::keyboard::client::map_key_to_control_key; #[cfg(target_os = "linux")] -use crate::platform::linux::is_x11; -#[cfg(target_os = "linux")] use crate::platform::linux_desktop_manager; #[cfg(any(target_os = "windows", target_os = "linux"))] use crate::platform::WallPaperRemover; @@ -441,6 +441,28 @@ 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_clip_holder; + let mut rx_clip; + let _tx_clip: mpsc::UnboundedSender; + #[cfg(feature = "unix-file-copy-paste")] + { + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(id), + crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(id); + }), + }, + ); + rx_clip = rx_clip_holder.0.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 @@ -488,6 +510,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); @@ -513,6 +541,18 @@ 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(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); + } } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; @@ -527,7 +567,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); } @@ -740,9 +780,26 @@ impl Connection { } } } + clip_file = rx_clip.recv() => match clip_file { + Some(_clip) => { + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&conn.lr.version) + { + conn.handle_file_clip(_clip).await; + } + } + None => { + // + } + }, } } + #[cfg(feature = "unix-file-copy-paste")] + { + conn.try_empty_file_clipboard(); + } + if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { let _ = Self::turn_off_privacy_to_msg(id); @@ -1202,15 +1259,20 @@ 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)); + let is_both_windows = cfg!(target_os = "windows") + && self.lr.my_platform == whoami::Platform::Windows.to_string(); + #[cfg(feature = "unix-file-copy-paste")] + let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version); + #[cfg(not(feature = "unix-file-copy-paste"))] + let is_unix_and_peer_supported = false; + // to-do: add file clipboard support for macos + let is_both_macos = cfg!(target_os = "macos") + && self.lr.my_platform == whoami::Platform::MacOS.to_string(); + let has_file_clipboard = + is_both_windows || (is_unix_and_peer_supported && !is_both_macos); + platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); } #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] @@ -1375,6 +1437,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); } @@ -1455,11 +1521,18 @@ 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.clipboard_enabled() + && self.file_transfer_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) != "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(), @@ -2113,12 +2186,23 @@ 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"))] - if let Some(clip) = msg_2_clip(_clip) { - log::debug!("got clipfile from client peer"); - self.send_to_cm(ipc::Data::ClipboardFile(clip)) + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + Some(message::Union::Cliprdr(clip)) => { + if let Some(clip) = msg_2_clip(clip) { + #[cfg(target_os = "windows")] + { + self.send_to_cm(ipc::Data::ClipboardFile(clip)); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&self.lr.version) { + if let Some(msg) = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ) { + self.send(msg).await; + } + } } } Some(message::Union::FileAction(fa)) => { @@ -2911,13 +2995,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() { @@ -2941,6 +3038,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(), @@ -3330,6 +3433,41 @@ impl Connection { } false } + + #[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_keyboard_enabled = self.peer_keyboard_enabled(); + let file_transfer_enabled = self.file_transfer_enabled(); + let stop = is_stopping_allowed && !file_transfer_enabled; + log::debug!( + "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, file_transfer_enabled); + if !stop { + use hbb_common::config::keys::OPTION_ONE_WAY_FILE_TRANSFER; + // Note: Code will not reach here if `crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"` is true. + // Because `file-clipboard` service will not be subscribed. + // But we still check it here to keep the same logic to windows version in `ui_cm_interface.rs`. + if clip.is_beginning_message() + && crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y" + { + // If one way file transfer is enabled, don't send clipboard file to client + } else { + // 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) { + try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 5560cb95e..6c64e0d9f 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -21,12 +21,6 @@ pub fn init() { } fn map_err_scrap(err: String) -> io::Error { - // to-do: Remove this the following log - log::error!( - "REMOVE ME ===================================== wayland scrap error {}", - &err - ); - // to-do: Handle error better, do not restart server if err.starts_with("Did not receive a reply") { log::error!("Fatal pipewire error, {}", &err); diff --git a/src/ui/header.tis b/src/ui/header.tis index 4b634cf54..56f3af21e 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -174,6 +174,13 @@ 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 { + is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions.has_file_clipboard; + } + return
  • {translate('Adjust Window')}
  • @@ -201,7 +208,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 0296d82bd..d57da2267 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 a3373f8cc..c1e25362a 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -3,8 +3,11 @@ use crate::ipc::ClipboardNonFile; #[cfg(not(any(target_os = "android", target_os = "ios")))] 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"))] +use crate::{ + clipboard::ClipboardSide, + ipc::{self, Data}, +}; +#[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 +74,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 +172,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,12 +193,9 @@ 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)?; - Ok(()) - }); + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Host, id); } #[cfg(any(target_os = "android"))] @@ -345,31 +345,40 @@ 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"))] - let rx_clip1; + #[cfg(target_os = "windows")] + let rx_clip_holder; 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); - rx_clip = rx_clip1.lock().await; + let conn_id = self.conn_id; + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(conn_id), + Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(conn_id); + }), + }), + ); + rx_clip = rx_clip_holder.0.lock().await; } else { log::debug!("Clipboard is enabled from client peer, actually useless: type 2"); let rx_clip2; (_tx_clip, rx_clip2) = unbounded_channel::(); - rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); - rx_clip = rx_clip1.lock().await; + rx_clip_holder = (Arc::new(TokioMutex::new(rx_clip2)), None); + rx_clip = rx_clip_holder.0.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 +406,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 +447,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 +549,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 +564,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 +608,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 +629,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 323b651fe..1aa4130de 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 +331,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 +368,13 @@ impl Session { && !self.lc.read().unwrap().disable_clipboard.v } + #[cfg(any(target_os = "windows", 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) {