From 3a21efbaae56b49b036551c63f7c69583955f98f Mon Sep 17 00:00:00 2001 From: ClSlaid Date: Sat, 9 Sep 2023 19:24:38 +0800 Subject: [PATCH] patch: linux fuse unmount todo: grosely exit Signed-off-by: ClSlaid --- Cargo.lock | 1 + flutter/lib/common/widgets/toolbar.dart | 4 +- libs/clipboard/Cargo.toml | 1 + libs/clipboard/src/platform/fuse.rs | 27 ++--- libs/clipboard/src/platform/linux/mod.rs | 122 +++++++++++++++++++---- libs/clipboard/src/platform/linux/x11.rs | 44 ++++++-- libs/clipboard/src/platform/mod.rs | 28 ++++-- src/server/connection.rs | 14 +-- src/ui_cm_interface.rs | 2 +- src/ui_interface.rs | 12 ++- src/ui_session_interface.rs | 2 +- 11 files changed, 185 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b5074999..20aa5be7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -937,6 +937,7 @@ dependencies = [ "hbb_common", "lazy_static", "libc", + "once_cell", "parking_lot", "percent-encoding", "rand 0.8.5", diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 4cf6fd3ea..1da7ac2c7 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -434,9 +434,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Mute')))); } // file copy and paste - if (Platform.isWindows && - pi.platform == kPeerPlatformWindows && - perms['file'] != false) { + if (perms['file'] != false) { final option = 'enable-file-transfer'; final value = bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index 827503b4f..42bb06a1d 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -18,6 +18,7 @@ hbb_common = { path = "../hbb_common" } parking_lot = {version = "0.12"} [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +once_cell = "1.18" x11rb = {version = "0.12", features = ["all-extensions"]} rand = {version = "0.8"} fuser = {version = "0.13"} diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/fuse.rs index e78d865b0..e4fa0a09f 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/fuse.rs @@ -558,9 +558,10 @@ impl FuseServer { // get files and directory path right in root of FUSE fs pub fn list_root(&self) -> Vec { let files = self.files.read(); - let mut paths = Vec::new(); - for file in files.iter().filter(|f| f.parent == Some(FUSE_ROOT_ID)) { - paths.push(PathBuf::from(&file.name)); + let children = &files[0].children; + let mut paths = Vec::with_capacity(children.len()); + for idx in children.iter().copied() { + paths.push(PathBuf::from(&files[idx as usize].name)); } paths } @@ -1083,19 +1084,6 @@ struct FuseNode { } impl FuseNode { - pub fn new(name: &str, inode: Inode, attributes: InodeAttributes, conn_id: i32) -> Self { - Self { - conn_id, - stream_id: rand::random(), - inode, - name: name.to_owned(), - parent: None, - attributes, - children: Vec::new(), - file_handlers: FileHandles::new(), - } - } - pub fn from_description(inode: Inode, desc: FileDescription) -> Self { Self { conn_id: desc.conn_id, @@ -1281,10 +1269,15 @@ impl InodeAttributes { impl From<&InodeAttributes> for fuser::FileAttr { fn from(value: &InodeAttributes) -> Self { + let blocks = if value.size % BLOCK_SIZE as u64 == 0 { + value.size / BLOCK_SIZE as u64 + } else { + value.size / BLOCK_SIZE as u64 + 1 + }; Self { ino: value.inode, size: value.size, - blocks: value.size.div_ceil(BLOCK_SIZE as u64), + blocks, atime: value.last_accessed, mtime: value.last_modified, ctime: value.last_metadata_changed, diff --git a/libs/clipboard/src/platform/linux/mod.rs b/libs/clipboard/src/platform/linux/mod.rs index 57a5f41e5..0c53f0c9e 100644 --- a/libs/clipboard/src/platform/linux/mod.rs +++ b/libs/clipboard/src/platform/linux/mod.rs @@ -3,7 +3,10 @@ use std::{ fs::File, os::unix::prelude::FileExt, path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, time::{Duration, SystemTime}, }; @@ -14,7 +17,7 @@ use hbb_common::{ log, }; use lazy_static::lazy_static; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use utf16string::WString; use crate::{send_data, ClipboardFile, CliprdrError, CliprdrServiceContext}; @@ -44,8 +47,10 @@ fn add_remote_format(local_name: &str, remote_id: i32) { } trait SysClipboard: Send + Sync { - fn wait_file_list(&self) -> Result, CliprdrError>; + fn wait_file_list(&self) -> Result>, CliprdrError>; fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; + fn stop(&self); + fn start(&self); } fn get_sys_clipboard() -> Result, CliprdrError> { @@ -300,6 +305,12 @@ pub struct CliprdrClient { pub context: Arc, } +impl Drop for CliprdrClient { + fn drop(&mut self) { + self.context.ref_decrease(); + } +} + impl CliprdrServiceContext for CliprdrClient { fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { self.context.set_is_stopped() @@ -315,11 +326,12 @@ impl CliprdrServiceContext for CliprdrClient { } pub struct ClipboardContext { - pub stop: AtomicBool, + pub client_count: AtomicUsize, pub fuse_mount_point: PathBuf, fuse_server: Arc, file_list: RwLock>, clipboard: Arc, + fuse_handle: Mutex>, } impl ClipboardContext { @@ -331,53 +343,97 @@ impl ClipboardContext { })?; let fuse_server = Arc::new(FuseServer::new(timeout)); + let clipboard = get_sys_clipboard()?; let clipboard = Arc::from(clipboard); let file_list = RwLock::new(vec![]); Ok(Self { - stop: AtomicBool::new(false), + client_count: AtomicUsize::new(0), fuse_mount_point, fuse_server, + fuse_handle: Mutex::new(None), file_list, clipboard, }) } - pub fn client(self: Arc) -> CliprdrClient { - CliprdrClient { context: self } + pub fn client(self: Arc) -> Result { + if self.client_count.fetch_add(1, Ordering::Relaxed) == 0 { + let mut fuse_handle = self.fuse_handle.lock(); + + if fuse_handle.is_none() { + let mount_path = &self.fuse_mount_point; + create_if_not_exists(mount_path); + + let mnt_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::RO, + MountOption::NoAtime, + ]; + log::info!( + "mounting clipboard FUSE to {}", + self.fuse_mount_point.display() + ); + + let new_handle = + fuser::spawn_mount2(self.fuse_server.client(), mount_path, &mnt_opts).map_err( + |e| { + log::error!("failed to mount cliprdr fuse: {:?}", e); + CliprdrError::CliprdrInit + }, + )?; + *fuse_handle = Some(new_handle); + } + + self.clipboard.start(); + let clip = self.clone(); + std::thread::spawn(move || { + let res = clip.listen_clipboard(); + if let Err(e) = res { + log::error!("failed to listen clipboard: {:?}", e); + } + log::info!("stopped listening clipboard"); + }); + } + + Ok(CliprdrClient { context: self }) } - // mount and run fuse server, blocking - pub fn mount(&self) -> Result<(), CliprdrError> { - let mount_opts = [ - MountOption::FSName("rustdesk-cliprdr-fs".to_string()), - MountOption::RO, - MountOption::NoAtime, - ]; - let fuse_client = self.fuse_server.client(); - fuser::mount2(fuse_client, self.fuse_mount_point.clone(), &mount_opts).map_err(|e| { - log::error!("failed to mount fuse: {:?}", e); - CliprdrError::CliprdrInit - }) + /// set context to be inactive + pub fn ref_decrease(&self) { + if self.client_count.fetch_sub(1, Ordering::Relaxed) > 1 { + return; + } + + let mut fuse_handle = self.fuse_handle.lock(); + if let Some(fuse_handle) = fuse_handle.take() { + fuse_handle.join(); + } + self.clipboard.stop(); + std::fs::remove_dir(&self.fuse_mount_point).unwrap(); } /// 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) } pub fn listen_clipboard(&self) -> Result<(), CliprdrError> { - while let Ok(v) = self.clipboard.wait_file_list() { + log::debug!("start listening clipboard"); + while let Some(v) = self.clipboard.wait_file_list()? { let filtered: Vec<_> = v .into_iter() .filter(|pb| !pb.starts_with(&self.fuse_mount_point)) .collect(); + log::debug!("clipboard file list update (filtered): {:?}", filtered); if filtered.is_empty() { continue; } + log::debug!("send file list update to remote"); // construct format list update and send let data = ClipboardFile::FormatList { @@ -390,18 +446,27 @@ impl ClipboardContext { ], }; - send_data(0, data) + send_data(0, data); + log::debug!("format list update sent"); } Ok(()) } fn send_format_list(&self, conn_id: i32) -> Result<(), CliprdrError> { + log::debug!("send format list to remote, conn={}", conn_id); let data = self.clipboard.wait_file_list()?; + if data.is_none() { + log::debug!("clipboard disabled, skip sending"); + return Ok(()); + } + let data = data.unwrap(); + let filtered: Vec<_> = data .into_iter() .filter(|pb| !pb.starts_with(&self.fuse_mount_point)) .collect(); if filtered.is_empty() { + log::debug!("no files in format list, skip sending"); return Ok(()); } @@ -416,11 +481,19 @@ impl ClipboardContext { }; send_data(conn_id, format_list); + log::debug!("format list to remote dispatched, conn={}", conn_id); Ok(()) } fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { + log::debug!("send file list to remote, conn={}", conn_id); let data = self.clipboard.wait_file_list()?; + if data.is_none() { + log::debug!("clipboard disabled, skip sending"); + return Ok(()); + } + + let data = data.unwrap(); let filtered: Vec<_> = data .into_iter() .filter(|pb| !pb.starts_with(&self.fuse_mount_point)) @@ -576,6 +649,13 @@ fn resp_file_contents_fail(conn_id: i32, stream_id: i32) { send_data(conn_id, resp) } +fn create_if_not_exists(path: &PathBuf) { + if std::fs::metadata(path).is_ok() { + return; + } + std::fs::create_dir(path).unwrap(); +} + impl ClipboardContext { pub fn set_is_stopped(&self) -> Result<(), CliprdrError> { // do nothing diff --git a/libs/clipboard/src/platform/linux/x11.rs b/libs/clipboard/src/platform/linux/x11.rs index 151b4ae8a..99a711623 100644 --- a/libs/clipboard/src/platform/linux/x11.rs +++ b/libs/clipboard/src/platform/linux/x11.rs @@ -1,5 +1,9 @@ -use std::path::PathBuf; +use std::{ + path::PathBuf, + sync::atomic::{AtomicBool, Ordering}, +}; +use once_cell::sync::OnceCell; use x11_clipboard::Clipboard; use x11rb::protocol::xproto::Atom; @@ -7,15 +11,21 @@ use crate::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 { + stop: AtomicBool, text_uri_list: Atom, gnome_copied_files: Atom, - clipboard: Clipboard, } impl X11Clipboard { pub fn new() -> Result { - let clipboard = Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)?; + let clipboard = get_clip()?; let text_uri_list = clipboard .setter .get_atom("text/uri-list") @@ -25,33 +35,37 @@ impl X11Clipboard { .get_atom("x-special/gnome-copied-files") .map_err(|_| CliprdrError::CliprdrInit)?; Ok(Self { + stop: AtomicBool::new(false), text_uri_list, gnome_copied_files, - clipboard, }) } fn load(&self, target: Atom) -> Result, CliprdrError> { - let clip = self.clipboard.setter.atoms.clipboard; - let prop = self.clipboard.setter.atoms.property; - self.clipboard + let clip = get_clip()?.setter.atoms.clipboard; + let prop = get_clip()?.setter.atoms.property; + get_clip()? .load_wait(clip, target, prop) .map_err(|_| CliprdrError::ConversionFailure) } fn store_batch(&self, batch: Vec<(Atom, Vec)>) -> Result<(), CliprdrError> { - let clip = self.clipboard.setter.atoms.clipboard; - self.clipboard + let clip = get_clip()?.setter.atoms.clipboard; + get_clip()? .store_batch(clip, batch) .map_err(|_| CliprdrError::ClipboardInternalError) } } impl SysClipboard for X11Clipboard { - fn wait_file_list(&self) -> Result, CliprdrError> { + fn wait_file_list(&self) -> Result>, CliprdrError> { + if self.stop.load(Ordering::Relaxed) { + return Ok(None); + } let v = self.load(self.text_uri_list)?; // loading 'text/uri-list' should be enough? - parse_plain_uri_list(v) + let p = parse_plain_uri_list(v)?; + Ok(Some(p)) } fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { @@ -66,4 +80,12 @@ impl SysClipboard for X11Clipboard { self.store_batch(batch) .map_err(|_| CliprdrError::ClipboardInternalError) } + + fn stop(&self) { + self.stop.store(true, Ordering::Relaxed); + } + + fn start(&self) { + self.stop.store(false, Ordering::Relaxed); + } } diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index f63df87a3..7933ced4b 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -27,6 +27,8 @@ pub fn create_cliprdr_context( ) -> crate::ResultType> { use std::sync::Arc; + use hbb_common::{anyhow, log}; + if !enable_files { return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); } @@ -34,16 +36,26 @@ pub fn create_cliprdr_context( let timeout = std::time::Duration::from_secs(response_wait_timeout_secs as u64); let mut tmp_path = std::env::temp_dir(); tmp_path.push("rustdesk-cliprdr"); - let rd_mnt = tmp_path; - std::fs::create_dir(rd_mnt.clone())?; + + log::info!("check mount point existence"); + let rd_mnt = if !tmp_path.exists() { + log::info!("create mount point: {}", tmp_path.display()); + std::fs::create_dir_all(tmp_path.clone())?; + tmp_path + } else if !tmp_path.is_dir() { + log::error!("{} is occupied and is not a directory", tmp_path.display()); + return Err(CliprdrError::CliprdrInit.into()); + } else { + tmp_path + }; + let linux_ctx = Arc::new(linux::ClipboardContext::new(timeout, rd_mnt)?); + let client = linux_ctx.client().map_err(|e| { + log::error!("create clipboard client: {:?}", e); + anyhow::anyhow!("create clipboard client: {:?}", e) + })?; - let fuse_ctx = linux_ctx.clone(); - std::thread::spawn(move || fuse_ctx.mount()); - let clipboard_listen_ctx = linux_ctx.clone(); - std::thread::spawn(move || clipboard_listen_ctx.listen_clipboard()); - - Ok(Box::new(linux_ctx.client()) as Box<_>) + Ok(Box::new(client) as Box<_>) } struct DummyCliprdrContext {} diff --git a/src/server/connection.rs b/src/server/connection.rs index c9f07eb78..d9df11231 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,5 +1,5 @@ use super::{input_service::*, *}; -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "linux"))] use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; @@ -175,7 +175,7 @@ pub struct Connection { // by peer disable_audio: bool, // by peer - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] enable_file_transfer: bool, // by peer audio_sender: Option, @@ -310,7 +310,7 @@ impl Connection { show_remote_cursor: false, ip: "".to_owned(), disable_audio: false, - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] enable_file_transfer: false, disable_clipboard: false, disable_keyboard: false, @@ -457,7 +457,7 @@ impl Connection { ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } - #[cfg(windows)] + #[cfg(any(target_os="windows", target_os="linux"))] ipc::Data::ClipboardFile(clip) => { allow_err!(conn.stream.send(&clip_2_msg(clip)).await); } @@ -1156,7 +1156,7 @@ impl Connection { self.audio && !self.disable_audio } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] fn file_transfer_enabled(&self) -> bool { self.file && self.enable_file_transfer } @@ -1706,7 +1706,7 @@ impl Connection { } Some(message::Union::Cliprdr(_clip)) => { - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] if let Some(clip) = msg_2_clip(_clip) { self.send_to_cm(ipc::Data::ClipboardFile(clip)) } @@ -2156,7 +2156,7 @@ impl Connection { } } } - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 02bf5721b..207d73772 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -570,7 +570,7 @@ pub async fn start_ipc(cm: ConnectionManager) { } }); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "linux"))] ContextSend::enable(Config::get_option("enable-file-transfer").is_empty()); match ipc::new_listener("_cm").await { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ed2b4f4fc..74ca6ac40 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -594,7 +594,13 @@ pub fn current_is_wayland() -> bool { #[inline] pub fn get_new_version() -> String { - (*SOFTWARE_UPDATE_URL.lock().unwrap().rsplit('/').next().unwrap_or("")).to_string() + (*SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .rsplit('/') + .next() + .unwrap_or("")) + .to_string() } #[inline] @@ -999,7 +1005,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver Session { #[tokio::main(flavor = "current_thread")] pub async fn io_loop(handler: Session) { // It is ok to call this function multiple times. - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "linux"))] if !handler.is_file_transfer() && !handler.is_port_forward() { clipboard::ContextSend::enable(true); }