patch: linux fuse unmount

todo: grosely exit

Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
This commit is contained in:
ClSlaid 2023-09-09 19:24:38 +08:00
parent e5bcfeaad5
commit 3a21efbaae
No known key found for this signature in database
GPG Key ID: E0A5F564C51C056E
11 changed files with 185 additions and 72 deletions

1
Cargo.lock generated
View File

@ -937,6 +937,7 @@ dependencies = [
"hbb_common", "hbb_common",
"lazy_static", "lazy_static",
"libc", "libc",
"once_cell",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"rand 0.8.5", "rand 0.8.5",

View File

@ -434,9 +434,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Mute')))); child: Text(translate('Mute'))));
} }
// file copy and paste // file copy and paste
if (Platform.isWindows && if (perms['file'] != false) {
pi.platform == kPeerPlatformWindows &&
perms['file'] != false) {
final option = 'enable-file-transfer'; final option = 'enable-file-transfer';
final value = final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);

View File

@ -18,6 +18,7 @@ hbb_common = { path = "../hbb_common" }
parking_lot = {version = "0.12"} parking_lot = {version = "0.12"}
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
once_cell = "1.18"
x11rb = {version = "0.12", features = ["all-extensions"]} x11rb = {version = "0.12", features = ["all-extensions"]}
rand = {version = "0.8"} rand = {version = "0.8"}
fuser = {version = "0.13"} fuser = {version = "0.13"}

View File

@ -558,9 +558,10 @@ impl FuseServer {
// get files and directory path right in root of FUSE fs // get files and directory path right in root of FUSE fs
pub fn list_root(&self) -> Vec<PathBuf> { pub fn list_root(&self) -> Vec<PathBuf> {
let files = self.files.read(); let files = self.files.read();
let mut paths = Vec::new(); let children = &files[0].children;
for file in files.iter().filter(|f| f.parent == Some(FUSE_ROOT_ID)) { let mut paths = Vec::with_capacity(children.len());
paths.push(PathBuf::from(&file.name)); for idx in children.iter().copied() {
paths.push(PathBuf::from(&files[idx as usize].name));
} }
paths paths
} }
@ -1083,19 +1084,6 @@ struct FuseNode {
} }
impl 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 { pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
Self { Self {
conn_id: desc.conn_id, conn_id: desc.conn_id,
@ -1281,10 +1269,15 @@ impl InodeAttributes {
impl From<&InodeAttributes> for fuser::FileAttr { impl From<&InodeAttributes> for fuser::FileAttr {
fn from(value: &InodeAttributes) -> Self { 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 { Self {
ino: value.inode, ino: value.inode,
size: value.size, size: value.size,
blocks: value.size.div_ceil(BLOCK_SIZE as u64), blocks,
atime: value.last_accessed, atime: value.last_accessed,
mtime: value.last_modified, mtime: value.last_modified,
ctime: value.last_metadata_changed, ctime: value.last_metadata_changed,

View File

@ -3,7 +3,10 @@ use std::{
fs::File, fs::File,
os::unix::prelude::FileExt, os::unix::prelude::FileExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc}, sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
@ -14,7 +17,7 @@ use hbb_common::{
log, log,
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::RwLock; use parking_lot::{Mutex, RwLock};
use utf16string::WString; use utf16string::WString;
use crate::{send_data, ClipboardFile, CliprdrError, CliprdrServiceContext}; 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 { trait SysClipboard: Send + Sync {
fn wait_file_list(&self) -> Result<Vec<PathBuf>, CliprdrError>; fn wait_file_list(&self) -> Result<Option<Vec<PathBuf>>, CliprdrError>;
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>;
fn stop(&self);
fn start(&self);
} }
fn get_sys_clipboard() -> Result<Box<dyn SysClipboard>, CliprdrError> { fn get_sys_clipboard() -> Result<Box<dyn SysClipboard>, CliprdrError> {
@ -300,6 +305,12 @@ pub struct CliprdrClient {
pub context: Arc<ClipboardContext>, pub context: Arc<ClipboardContext>,
} }
impl Drop for CliprdrClient {
fn drop(&mut self) {
self.context.ref_decrease();
}
}
impl CliprdrServiceContext for CliprdrClient { impl CliprdrServiceContext for CliprdrClient {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
self.context.set_is_stopped() self.context.set_is_stopped()
@ -315,11 +326,12 @@ impl CliprdrServiceContext for CliprdrClient {
} }
pub struct ClipboardContext { pub struct ClipboardContext {
pub stop: AtomicBool, pub client_count: AtomicUsize,
pub fuse_mount_point: PathBuf, pub fuse_mount_point: PathBuf,
fuse_server: Arc<FuseServer>, fuse_server: Arc<FuseServer>,
file_list: RwLock<Vec<LocalFile>>, file_list: RwLock<Vec<LocalFile>>,
clipboard: Arc<dyn SysClipboard>, clipboard: Arc<dyn SysClipboard>,
fuse_handle: Mutex<Option<fuser::BackgroundSession>>,
} }
impl ClipboardContext { impl ClipboardContext {
@ -331,53 +343,97 @@ impl ClipboardContext {
})?; })?;
let fuse_server = Arc::new(FuseServer::new(timeout)); let fuse_server = Arc::new(FuseServer::new(timeout));
let clipboard = get_sys_clipboard()?; let clipboard = get_sys_clipboard()?;
let clipboard = Arc::from(clipboard); let clipboard = Arc::from(clipboard);
let file_list = RwLock::new(vec![]); let file_list = RwLock::new(vec![]);
Ok(Self { Ok(Self {
stop: AtomicBool::new(false), client_count: AtomicUsize::new(0),
fuse_mount_point, fuse_mount_point,
fuse_server, fuse_server,
fuse_handle: Mutex::new(None),
file_list, file_list,
clipboard, clipboard,
}) })
} }
pub fn client(self: Arc<Self>) -> CliprdrClient { pub fn client(self: Arc<Self>) -> Result<CliprdrClient, CliprdrError> {
CliprdrClient { context: self } 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 /// set context to be inactive
pub fn mount(&self) -> Result<(), CliprdrError> { pub fn ref_decrease(&self) {
let mount_opts = [ if self.client_count.fetch_sub(1, Ordering::Relaxed) > 1 {
MountOption::FSName("rustdesk-cliprdr-fs".to_string()), return;
MountOption::RO, }
MountOption::NoAtime,
]; let mut fuse_handle = self.fuse_handle.lock();
let fuse_client = self.fuse_server.client(); if let Some(fuse_handle) = fuse_handle.take() {
fuser::mount2(fuse_client, self.fuse_mount_point.clone(), &mount_opts).map_err(|e| { fuse_handle.join();
log::error!("failed to mount fuse: {:?}", e); }
CliprdrError::CliprdrInit self.clipboard.stop();
}) std::fs::remove_dir(&self.fuse_mount_point).unwrap();
} }
/// set clipboard data from file list /// set clipboard data from file list
pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
let prefix = self.fuse_mount_point.clone(); let prefix = self.fuse_mount_point.clone();
let paths: Vec<PathBuf> = paths.iter().cloned().map(|p| prefix.join(p)).collect(); let paths: Vec<PathBuf> = paths.iter().cloned().map(|p| prefix.join(p)).collect();
log::debug!("setting clipboard with paths: {:?}", paths);
self.clipboard.set_file_list(&paths) self.clipboard.set_file_list(&paths)
} }
pub fn listen_clipboard(&self) -> Result<(), CliprdrError> { 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 let filtered: Vec<_> = v
.into_iter() .into_iter()
.filter(|pb| !pb.starts_with(&self.fuse_mount_point)) .filter(|pb| !pb.starts_with(&self.fuse_mount_point))
.collect(); .collect();
log::debug!("clipboard file list update (filtered): {:?}", filtered);
if filtered.is_empty() { if filtered.is_empty() {
continue; continue;
} }
log::debug!("send file list update to remote");
// construct format list update and send // construct format list update and send
let data = ClipboardFile::FormatList { 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(()) Ok(())
} }
fn send_format_list(&self, conn_id: i32) -> Result<(), CliprdrError> { 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()?; 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 let filtered: Vec<_> = data
.into_iter() .into_iter()
.filter(|pb| !pb.starts_with(&self.fuse_mount_point)) .filter(|pb| !pb.starts_with(&self.fuse_mount_point))
.collect(); .collect();
if filtered.is_empty() { if filtered.is_empty() {
log::debug!("no files in format list, skip sending");
return Ok(()); return Ok(());
} }
@ -416,11 +481,19 @@ impl ClipboardContext {
}; };
send_data(conn_id, format_list); send_data(conn_id, format_list);
log::debug!("format list to remote dispatched, conn={}", conn_id);
Ok(()) Ok(())
} }
fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { 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()?; 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 let filtered: Vec<_> = data
.into_iter() .into_iter()
.filter(|pb| !pb.starts_with(&self.fuse_mount_point)) .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) 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 { impl ClipboardContext {
pub fn set_is_stopped(&self) -> Result<(), CliprdrError> { pub fn set_is_stopped(&self) -> Result<(), CliprdrError> {
// do nothing // do nothing

View File

@ -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 x11_clipboard::Clipboard;
use x11rb::protocol::xproto::Atom; use x11rb::protocol::xproto::Atom;
@ -7,15 +11,21 @@ use crate::CliprdrError;
use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard}; use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard};
static X11_CLIPBOARD: OnceCell<Clipboard> = OnceCell::new();
fn get_clip() -> Result<&'static Clipboard, CliprdrError> {
X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit))
}
pub struct X11Clipboard { pub struct X11Clipboard {
stop: AtomicBool,
text_uri_list: Atom, text_uri_list: Atom,
gnome_copied_files: Atom, gnome_copied_files: Atom,
clipboard: Clipboard,
} }
impl X11Clipboard { impl X11Clipboard {
pub fn new() -> Result<Self, CliprdrError> { pub fn new() -> Result<Self, CliprdrError> {
let clipboard = Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)?; let clipboard = get_clip()?;
let text_uri_list = clipboard let text_uri_list = clipboard
.setter .setter
.get_atom("text/uri-list") .get_atom("text/uri-list")
@ -25,33 +35,37 @@ impl X11Clipboard {
.get_atom("x-special/gnome-copied-files") .get_atom("x-special/gnome-copied-files")
.map_err(|_| CliprdrError::CliprdrInit)?; .map_err(|_| CliprdrError::CliprdrInit)?;
Ok(Self { Ok(Self {
stop: AtomicBool::new(false),
text_uri_list, text_uri_list,
gnome_copied_files, gnome_copied_files,
clipboard,
}) })
} }
fn load(&self, target: Atom) -> Result<Vec<u8>, CliprdrError> { fn load(&self, target: Atom) -> Result<Vec<u8>, CliprdrError> {
let clip = self.clipboard.setter.atoms.clipboard; let clip = get_clip()?.setter.atoms.clipboard;
let prop = self.clipboard.setter.atoms.property; let prop = get_clip()?.setter.atoms.property;
self.clipboard get_clip()?
.load_wait(clip, target, prop) .load_wait(clip, target, prop)
.map_err(|_| CliprdrError::ConversionFailure) .map_err(|_| CliprdrError::ConversionFailure)
} }
fn store_batch(&self, batch: Vec<(Atom, Vec<u8>)>) -> Result<(), CliprdrError> { fn store_batch(&self, batch: Vec<(Atom, Vec<u8>)>) -> Result<(), CliprdrError> {
let clip = self.clipboard.setter.atoms.clipboard; let clip = get_clip()?.setter.atoms.clipboard;
self.clipboard get_clip()?
.store_batch(clip, batch) .store_batch(clip, batch)
.map_err(|_| CliprdrError::ClipboardInternalError) .map_err(|_| CliprdrError::ClipboardInternalError)
} }
} }
impl SysClipboard for X11Clipboard { impl SysClipboard for X11Clipboard {
fn wait_file_list(&self) -> Result<Vec<PathBuf>, CliprdrError> { fn wait_file_list(&self) -> Result<Option<Vec<PathBuf>>, CliprdrError> {
if self.stop.load(Ordering::Relaxed) {
return Ok(None);
}
let v = self.load(self.text_uri_list)?; let v = self.load(self.text_uri_list)?;
// loading 'text/uri-list' should be enough? // 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> { fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
@ -66,4 +80,12 @@ impl SysClipboard for X11Clipboard {
self.store_batch(batch) self.store_batch(batch)
.map_err(|_| CliprdrError::ClipboardInternalError) .map_err(|_| CliprdrError::ClipboardInternalError)
} }
fn stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
fn start(&self) {
self.stop.store(false, Ordering::Relaxed);
}
} }

View File

@ -27,6 +27,8 @@ pub fn create_cliprdr_context(
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> { ) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
use std::sync::Arc; use std::sync::Arc;
use hbb_common::{anyhow, log};
if !enable_files { if !enable_files {
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); 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 timeout = std::time::Duration::from_secs(response_wait_timeout_secs as u64);
let mut tmp_path = std::env::temp_dir(); let mut tmp_path = std::env::temp_dir();
tmp_path.push("rustdesk-cliprdr"); 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 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(); Ok(Box::new(client) as Box<_>)
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<_>)
} }
struct DummyCliprdrContext {} struct DummyCliprdrContext {}

View File

@ -1,5 +1,5 @@
use super::{input_service::*, *}; use super::{input_service::*, *};
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "linux"))]
use crate::clipboard_file::*; use crate::clipboard_file::*;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::common::update_clipboard; use crate::common::update_clipboard;
@ -175,7 +175,7 @@ pub struct Connection {
// by peer // by peer
disable_audio: bool, disable_audio: bool,
// by peer // by peer
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "linux"))]
enable_file_transfer: bool, enable_file_transfer: bool,
// by peer // by peer
audio_sender: Option<MediaSender>, audio_sender: Option<MediaSender>,
@ -310,7 +310,7 @@ impl Connection {
show_remote_cursor: false, show_remote_cursor: false,
ip: "".to_owned(), ip: "".to_owned(),
disable_audio: false, disable_audio: false,
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "linux"))]
enable_file_transfer: false, enable_file_transfer: false,
disable_clipboard: false, disable_clipboard: false,
disable_keyboard: false, disable_keyboard: false,
@ -457,7 +457,7 @@ impl Connection {
ipc::Data::RawMessage(bytes) => { ipc::Data::RawMessage(bytes) => {
allow_err!(conn.stream.send_raw(bytes).await); allow_err!(conn.stream.send_raw(bytes).await);
} }
#[cfg(windows)] #[cfg(any(target_os="windows", target_os="linux"))]
ipc::Data::ClipboardFile(clip) => { ipc::Data::ClipboardFile(clip) => {
allow_err!(conn.stream.send(&clip_2_msg(clip)).await); allow_err!(conn.stream.send(&clip_2_msg(clip)).await);
} }
@ -1156,7 +1156,7 @@ impl Connection {
self.audio && !self.disable_audio self.audio && !self.disable_audio
} }
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "linux"))]
fn file_transfer_enabled(&self) -> bool { fn file_transfer_enabled(&self) -> bool {
self.file && self.enable_file_transfer self.file && self.enable_file_transfer
} }
@ -1706,7 +1706,7 @@ impl Connection {
} }
Some(message::Union::Cliprdr(_clip)) => Some(message::Union::Cliprdr(_clip)) =>
{ {
#[cfg(windows)] #[cfg(any(target_os = "windows", target_os = "linux"))]
if let Some(clip) = msg_2_clip(_clip) { if let Some(clip) = msg_2_clip(_clip) {
self.send_to_cm(ipc::Data::ClipboardFile(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 let Ok(q) = o.enable_file_transfer.enum_value() {
if q != BoolOption::NotSet { if q != BoolOption::NotSet {
self.enable_file_transfer = q == BoolOption::Yes; self.enable_file_transfer = q == BoolOption::Yes;

View File

@ -570,7 +570,7 @@ pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
} }
}); });
#[cfg(target_os = "windows")] #[cfg(any(target_os = "windows", target_os = "linux"))]
ContextSend::enable(Config::get_option("enable-file-transfer").is_empty()); ContextSend::enable(Config::get_option("enable-file-transfer").is_empty());
match ipc::new_listener("_cm").await { match ipc::new_listener("_cm").await {

View File

@ -594,7 +594,13 @@ pub fn current_is_wayland() -> bool {
#[inline] #[inline]
pub fn get_new_version() -> String { 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] #[inline]
@ -999,7 +1005,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc:
let mut mouse_time = 0; let mut mouse_time = 0;
#[cfg(not(feature = "flutter"))] #[cfg(not(feature = "flutter"))]
let mut id = "".to_owned(); let mut id = "".to_owned();
#[cfg(target_os = "windows")] #[cfg(any(target_os = "windows", target_os = "linux"))]
let mut enable_file_transfer = "".to_owned(); let mut enable_file_transfer = "".to_owned();
loop { loop {
@ -1022,7 +1028,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc:
*OPTIONS.lock().unwrap() = v; *OPTIONS.lock().unwrap() = v;
*OPTION_SYNCED.lock().unwrap() = true; *OPTION_SYNCED.lock().unwrap() = true;
#[cfg(target_os="windows")] #[cfg(any(target_os="windows", target_os="linux"))]
{ {
let b = OPTIONS.lock().unwrap().get("enable-file-transfer").map(|x| x.to_string()).unwrap_or_default(); let b = OPTIONS.lock().unwrap().get("enable-file-transfer").map(|x| x.to_string()).unwrap_or_default();
if b != enable_file_transfer { if b != enable_file_transfer {

View File

@ -1254,7 +1254,7 @@ impl<T: InvokeUiSession> Session<T> {
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>) { pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>) {
// It is ok to call this function multiple times. // 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() { if !handler.is_file_transfer() && !handler.is_port_forward() {
clipboard::ContextSend::enable(true); clipboard::ContextSend::enable(true);
} }