fix(fuse): umount (#15426)

* fix(clipboard): clean up stale Linux FUSE mounts

Recover Linux file clipboard FUSE mount points before remounting and stop treating a cached
context as valid when the mount has already gone away.

This fixes the desktop file manager copy failure that shows dialogs such as
"Error while copying a" and "There was an error copying the file into xxx".

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(clipboard): fuse, reduce dups

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: clear Linux file clipboard before unmounting FUSE

Ensure Linux client teardown clears RustDesk file clipboard URLs while
the FUSE context is still available. Also prefer fusermount before
umount to avoid noisy unprivileged teardown attempts.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(clipboard): return and log errors

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-06-26 16:17:44 +08:00
committed by GitHub
parent 989bf80fe8
commit 001848bf2f
9 changed files with 444 additions and 93 deletions

View File

@@ -941,21 +941,23 @@ impl Client {
#[cfg(not(target_os = "ios"))]
fn try_stop_clipboard() {
// There's a bug here.
// If session is closed by the peer, `has_sessions_running()` will always return true.
// It's better to check if the active session number.
// But it's not a problem, because the clipboard thread does not consume CPU.
//
// If we want to fix it, we can add a flag to indicate if session is active.
// But I think it's not necessary to introduce complexity at this point.
// Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions
// should block clipboard cleanup.
#[cfg(feature = "flutter")]
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) {
return;
}
#[cfg(not(target_os = "android"))]
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
CLIPBOARD_STATE.lock().unwrap().running = false;
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync(
crate::clipboard::ClipboardSide::Client,
0,
) {
log::error!("Failed to empty client clipboard files: {}", e);
}
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
clipboard::platform::unix::fuse::uninit_fuse_context(true);
}

View File

@@ -360,6 +360,8 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
if self.handler.is_default() && _set_disconnected_ok {
// Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is
// unmounted. Keep this async path for other file-clipboard platforms.
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
}
}

View File

@@ -151,41 +151,50 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
#[cfg(feature = "unix-file-copy-paste")]
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
std::thread::spawn(move || {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
match ClipboardContext::new() {
Ok(x) => {
*ctx = Some(x);
}
Err(e) => {
log::error!("Failed to create clipboard context: {}", e);
return;
}
}
}
#[allow(unused_mut)]
if let Some(mut ctx) = ctx.as_mut() {
#[cfg(target_os = "linux")]
{
use clipboard::platform::unix;
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
ctx.try_empty_clipboard_files(_side);
}
}
#[cfg(target_os = "macos")]
{
ctx.try_empty_clipboard_files(_side);
// No need to make sure the context is enabled.
clipboard::ContextSend::proc(|context| -> ResultType<()> {
context.empty_clipboard(_conn_id).ok();
Ok(())
})
.ok();
}
if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
log::error!("Failed to empty clipboard files: {}", e);
}
});
}
#[cfg(feature = "unix-file-copy-paste")]
pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
match ClipboardContext::new() {
Ok(x) => {
*ctx = Some(x);
}
Err(e) => {
log::error!("Failed to create clipboard context: {}", e);
bail!("Failed to create clipboard context: {}", e);
}
}
}
#[allow(unused_mut)]
if let Some(mut ctx) = ctx.as_mut() {
#[cfg(target_os = "linux")]
{
use clipboard::platform::unix;
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
ctx.try_empty_clipboard_files(_side);
}
}
#[cfg(target_os = "macos")]
{
ctx.try_empty_clipboard_files(_side);
// No need to make sure the context is enabled.
clipboard::ContextSend::proc(|context| -> ResultType<()> {
if !context.empty_clipboard(_conn_id)? {
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
}
Ok(())
})?;
}
}
Ok(())
}
#[cfg(target_os = "windows")]
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);

View File

@@ -332,12 +332,16 @@ pub mod unix_file_clip {
log::debug!("format data response: msg_flags: {}", msg_flags);
if msg_flags != 0x1 {
// return failure message?
log::error!(
"peer reported clipboard format data failure: {}",
msg_flags
);
return vec![];
}
log::debug!("parsing file descriptors");
if fuse::init_fuse_context(true).is_ok() {
match fuse::format_data_response_to_urls(
match fuse::init_fuse_context(side == ClipboardSide::Client) {
Ok(()) => match fuse::format_data_response_to_urls(
side == ClipboardSide::Client,
format_data,
conn_id,
@@ -348,9 +352,10 @@ pub mod unix_file_clip {
Err(e) => {
log::error!("failed to parse file descriptors: {:?}", e);
}
},
Err(e) => {
log::error!("failed to initialize clipboard FUSE context: {:?}", e);
}
} else {
// send error message to server
}
}
ClipboardFile::FileContentsRequest {
@@ -386,6 +391,7 @@ pub mod unix_file_clip {
ClipboardFile::FileContentsResponse {
msg_flags,
stream_id,
requested_data,
..
} => {
log::debug!(
@@ -393,13 +399,15 @@ pub mod unix_file_clip {
msg_flags,
stream_id,
);
if fuse::init_fuse_context(true).is_ok() {
hbb_common::allow_err!(fuse::handle_file_content_response(
side == ClipboardSide::Client,
clip
));
} else {
// send error message to server
let response = ClipboardFile::FileContentsResponse {
msg_flags,
stream_id,
requested_data,
};
if let Err(e) =
fuse::handle_file_content_response(side == ClipboardSide::Client, response)
{
log::error!("failed to handle file contents response: {:?}", e);
}
}
ClipboardFile::NotifyCallback {

View File

@@ -2297,6 +2297,16 @@ pub mod sessions {
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
})
}
#[inline]
#[cfg(not(target_os = "ios"))]
pub fn has_connected_sessions_running(conn_type: ConnType) -> bool {
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
*r#type == conn_type
&& s.session_handlers.read().unwrap().len() != 0
&& s.connection_round_state.lock().unwrap().is_connected()
})
}
}
pub(super) mod async_tasks {

View File

@@ -128,6 +128,10 @@ impl ConnectionRoundState {
true
}
}
pub fn is_connected(&self) -> bool {
matches!(self.state, ConnectionState::Connected)
}
}
impl Default for ConnectionRoundState {