Merge branch 'feat/x11/clipboard-file/init' into feat/osx/clipboard-file

Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
This commit is contained in:
ClSlaid 2023-10-30 15:26:03 +08:00
commit 62563ad8a1
No known key found for this signature in database
GPG Key ID: E0A5F564C51C056E
25 changed files with 271 additions and 191 deletions

View File

@ -32,6 +32,13 @@ linux_headless = ["pam" ]
virtual_display_driver = ["virtual_display"] virtual_display_driver = ["virtual_display"]
plugin_framework = [] plugin_framework = []
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"] linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
unix-file-copy-paste = [
"dep:x11-clipboard",
"dep:x11rb",
"dep:percent-encoding",
"dep:once_cell",
"clipboard/unix-file-copy-paste",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -132,10 +139,10 @@ dbus = "0.9"
dbus-crossroads = "0.5" dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam", optional = true } pam = { git="https://github.com/fufesou/pam", optional = true }
users = { version = "0.11" } users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch"} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"]} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
percent-encoding = "2.3" percent-encoding = {version = "2.3", optional = true}
once_cell = "1.18" once_cell = {version = "1.18", optional = true}
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13" android_logger = "0.13"

View File

@ -24,18 +24,21 @@ else:
flutter_build_dir_2 = f'flutter/{flutter_build_dir}' flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
skip_cargo = False skip_cargo = False
def get_arch() -> str: def get_arch() -> str:
custom_arch = os.environ.get("ARCH") custom_arch = os.environ.get("ARCH")
if custom_arch is None: if custom_arch is None:
return "amd64" return "amd64"
return custom_arch return custom_arch
def system2(cmd): def system2(cmd):
err = os.system(cmd) err = os.system(cmd)
if err != 0: if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.") print(f"Error occurred when executing: {cmd}. Exiting.")
sys.exit(-1) sys.exit(-1)
def get_version(): def get_version():
with open("Cargo.toml", encoding="utf-8") as fh: with open("Cargo.toml", encoding="utf-8") as fh:
for line in fh: for line in fh:
@ -123,6 +126,11 @@ def make_parser():
action='store_true', action='store_true',
help='Build windows portable' help='Build windows portable'
) )
parser.add_argument(
'--unix-file-copy-paste',
action='store_true',
help='Build with unix file copy paste feature'
)
parser.add_argument( parser.add_argument(
'--flatpak', '--flatpak',
action='store_true', action='store_true',
@ -185,6 +193,7 @@ def download_extract_features(features, res_dir):
import re import re
proxy = '' proxy = ''
def req(url): def req(url):
if not proxy: if not proxy:
return url return url
@ -196,9 +205,9 @@ def download_extract_features(features, res_dir):
for (feat, feat_info) in features.items(): for (feat, feat_info) in features.items():
includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else [] includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else []
includes = [ re.compile(p) for p in includes ] includes = [re.compile(p) for p in includes]
excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else [] excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else []
excludes = [ re.compile(p) for p in excludes ] excludes = [re.compile(p) for p in excludes]
print(f'{feat} download begin') print(f'{feat} download begin')
download_filename = feat_info['zip_url'].split('/')[-1] download_filename = feat_info['zip_url'].split('/')[-1]
@ -272,6 +281,8 @@ def get_features(args):
features.append('flatpak') features.append('flatpak')
if args.appimage: if args.appimage:
features.append('appimage') features.append('appimage')
if args.unix_file_copy_paste:
features.append('unix-file-copy-paste')
print("features:", features) print("features:", features)
return features return features
@ -350,6 +361,7 @@ def build_flutter_deb(version, features):
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..") os.chdir("..")
def build_deb_from_folder(version, binary_folder): def build_deb_from_folder(version, binary_folder):
os.chdir('flutter') os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/') system2('mkdir -p tmpdeb/usr/bin/')
@ -388,10 +400,12 @@ def build_deb_from_folder(version, binary_folder):
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..") os.chdir("..")
def build_flutter_dmg(version, features): def build_flutter_dmg(version, features):
if not skip_cargo: if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') system2(
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
# copy dylib # copy dylib
system2( system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
@ -557,7 +571,8 @@ def main():
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
'''.format(pa)) '''.format(pa))
system2('create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version) system2(
'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
os.rename('RustDesk %s.dmg' % os.rename('RustDesk %s.dmg' %
version, 'rustdesk-%s.dmg' % version) version, 'rustdesk-%s.dmg' % version)
if pa: if pa:
@ -577,7 +592,7 @@ def main():
else: else:
print('Not signed') print('Not signed')
else: else:
# buid deb package # build deb package
system2( system2(
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
system2('dpkg-deb -R rustdesk.deb tmpdeb') system2('dpkg-deb -R rustdesk.deb tmpdeb')

View File

@ -436,7 +436,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Mute')))); child: Text(translate('Mute'))));
} }
// file copy and paste // file copy and paste
if (perms['file'] != false) { if (perms['file'] != false &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
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

@ -22,6 +22,7 @@ const String kPlatformAdditionsIsWayland = "is_wayland";
const String kPlatformAdditionsHeadless = "headless"; const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed"; const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays"; const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformLinux = "Linux";

View File

@ -9,6 +9,21 @@ build = "build.rs"
[build-dependencies] [build-dependencies]
cc = "1.0" cc = "1.0"
[features]
default = []
unix-file-copy-paste = [
"dep:x11rb",
"dep:x11-clipboard",
"dep:rand",
"dep:fuser",
"dep:libc",
"dep:dashmap",
"dep:percent-encoding",
"dep:utf16string",
"dep:once_cell",
"dep:cacao"
]
[dependencies] [dependencies]
thiserror = "1.0" thiserror = "1.0"
lazy_static = "1.4" lazy_static = "1.4"
@ -18,17 +33,18 @@ 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]
rand = {version = "0.8"} x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
fuser = {version = "0.13"} rand = {version = "0.8", optional = true}
libc = {version = "0.2"} fuser = {version = "0.13", optional = true}
dashmap = "5.5" libc = {version = "0.2", optional = true}
percent-encoding = "2.3" dashmap = {version ="5.5", optional = true}
utf16string = "0.2" utf16string = {version = "0.2", optional = true}
once_cell = "1.18" once_cell = {version = "1.18", optional = true}
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch"} percent-encoding = {version ="2.3", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"]} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls"} cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}

View File

@ -47,6 +47,19 @@ impl ContextSend {
} }
} }
/// make sure the clipboard context is enabled.
pub fn make_sure_enabled() -> ResultType<()> {
let mut lock = CONTEXT_SEND.addr.lock().unwrap();
if lock.is_some() {
return Ok(());
}
let ctx = crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS)?;
*lock = Some(ctx);
log::info!("clipboard context for file transfer recreated.");
Ok(())
}
pub fn proc<F: FnOnce(&mut Box<dyn CliprdrServiceContext>) -> ResultType<()>>( pub fn proc<F: FnOnce(&mut Box<dyn CliprdrServiceContext>) -> ResultType<()>>(
f: F, f: F,
) -> ResultType<()> { ) -> ResultType<()> {

View File

@ -108,6 +108,7 @@ pub enum ClipboardFile {
struct MsgChannel { struct MsgChannel {
peer_id: String, peer_id: String,
conn_id: i32, conn_id: i32,
#[allow(dead_code)]
sender: UnboundedSender<ClipboardFile>, sender: UnboundedSender<ClipboardFile>,
receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>, receiver: Arc<TokioMutex<UnboundedReceiver<ClipboardFile>>>,
} }
@ -193,6 +194,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
} }
} }
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
#[inline] #[inline]
fn send_data(conn_id: i32, data: ClipboardFile) { fn send_data(conn_id: i32, data: ClipboardFile) {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -204,7 +206,7 @@ fn send_data(conn_id: i32, data: ClipboardFile) {
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] #[inline]
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) { fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
// no need to handle result here // no need to handle result here
@ -218,6 +220,7 @@ fn send_data_to_channel(conn_id: i32, data: ClipboardFile) {
} }
} }
#[cfg(feature = "unix-file-copy-paste")]
#[inline] #[inline]
fn send_data_to_all(data: ClipboardFile) { fn send_data_to_all(data: ClipboardFile) {
// no need to handle result here // no need to handle result here

View File

@ -14,45 +14,53 @@ pub fn create_cliprdr_context(
Ok(boxed) Ok(boxed)
} }
#[cfg(feature = "unix-file-copy-paste")]
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
/// use FUSE for file pasting on these platforms /// use FUSE for file pasting on these platforms
pub mod fuse; pub mod fuse;
#[cfg(feature = "unix-file-copy-paste")]
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
pub mod unix; pub mod unix;
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn create_cliprdr_context( pub fn create_cliprdr_context(
enable_files: bool, _enable_files: bool,
_enable_others: bool, _enable_others: bool,
response_wait_timeout_secs: u32, _response_wait_timeout_secs: u32,
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> { ) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
use std::{fs::Permissions, os::unix::prelude::PermissionsExt}; #[cfg(feature = "unix-file-copy-paste")]
{
use std::{fs::Permissions, os::unix::prelude::PermissionsExt};
use hbb_common::{config::APP_NAME, log}; use hbb_common::{config::APP_NAME, log};
if !enable_files { if !_enable_files {
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); 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().unwrap())?;
log::debug!("start cliprdr FUSE");
unix_ctx.run().expect("failed to start cliprdr FUSE");
Ok(Box::new(unix_ctx) as Box<_>)
} }
let timeout = std::time::Duration::from_secs(response_wait_timeout_secs as u64); #[cfg(not(feature = "unix-file-copy-paste"))]
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
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().unwrap())?;
log::debug!("start cliprdr FUSE");
unix_ctx.run().expect("failed to start cliprdr FUSE");
Ok(Box::new(unix_ctx) as Box<_>)
} }
struct DummyCliprdrContext {} struct DummyCliprdrContext {}
@ -73,7 +81,8 @@ impl CliprdrServiceContext for DummyCliprdrContext {
} }
} }
#[cfg(feature = "unix-file-copy-paste")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
// begin of epoch used by microsoft // begin of epoch used by microsoft
// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 // 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00
#[cfg(any(target_os = "linux", target_os = "macos"))]
const LDAP_EPOCH_DELTA: u64 = 116444772610000000; const LDAP_EPOCH_DELTA: u64 = 116444772610000000;

View File

@ -24,7 +24,6 @@ use self::url::{encode_path_to_uri, parse_plain_uri_list};
use super::fuse::FuseServer; use super::fuse::FuseServer;
#[cfg(not(feature = "wayland"))]
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
/// clipboard implementation of x11 /// clipboard implementation of x11
pub mod x11; pub mod x11;
@ -34,6 +33,7 @@ pub mod x11;
pub mod ns_clipboard; pub mod ns_clipboard;
pub mod local_file; pub mod local_file;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod url; pub mod url;
@ -68,7 +68,6 @@ fn add_remote_format(local_name: &str, remote_id: i32) {
trait SysClipboard: Send + Sync { trait SysClipboard: Send + Sync {
fn start(&self); fn start(&self);
fn stop(&self);
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>;
fn get_file_list(&self) -> Vec<PathBuf>; fn get_file_list(&self) -> Vec<PathBuf>;
@ -531,7 +530,7 @@ impl CliprdrServiceContext for ClipboardContext {
if let Some(fuse_handle) = self.fuse_handle.lock().take() { if let Some(fuse_handle) = self.fuse_handle.lock().take() {
fuse_handle.join(); fuse_handle.join();
} }
self.clipboard.stop(); // we don't stop the clipboard, keep listening in case of restart
Ok(()) Ok(())
} }

View File

@ -1,8 +1,4 @@
use std::{ use std::{collections::BTreeSet, path::PathBuf};
collections::BTreeSet,
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
use cacao::pasteboard::{Pasteboard, PasteboardName}; use cacao::pasteboard::{Pasteboard, PasteboardName};
use hbb_common::log; use hbb_common::log;
@ -28,7 +24,6 @@ fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> {
} }
pub struct NsPasteboard { pub struct NsPasteboard {
stopped: AtomicBool,
ignore_path: PathBuf, ignore_path: PathBuf,
former_file_list: Mutex<Vec<PathBuf>>, former_file_list: Mutex<Vec<PathBuf>>,
@ -37,16 +32,10 @@ pub struct NsPasteboard {
impl NsPasteboard { impl NsPasteboard {
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> { pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
Ok(Self { Ok(Self {
stopped: AtomicBool::new(false),
ignore_path: ignore_path.to_owned(), ignore_path: ignore_path.to_owned(),
former_file_list: Mutex::new(vec![]), former_file_list: Mutex::new(vec![]),
}) })
} }
#[inline]
fn is_stopped(&self) -> bool {
self.stopped.load(Ordering::Relaxed)
}
} }
impl SysClipboard for NsPasteboard { impl SysClipboard for NsPasteboard {
@ -56,13 +45,11 @@ impl SysClipboard for NsPasteboard {
} }
fn start(&self) { fn start(&self) {
self.stopped.store(false, Ordering::Relaxed); {
*self.former_file_list.lock() = vec![];
}
loop { loop {
if self.is_stopped() {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
let file_list = match wait_file_list() { let file_list = match wait_file_list() {
Some(v) => v, Some(v) => v,
None => { None => {
@ -104,10 +91,6 @@ impl SysClipboard for NsPasteboard {
log::debug!("stop listening file related atoms on clipboard"); log::debug!("stop listening file related atoms on clipboard");
} }
fn stop(&self) {
self.stopped.store(true, Ordering::Relaxed);
}
fn get_file_list(&self) -> Vec<PathBuf> { fn get_file_list(&self) -> Vec<PathBuf> {
self.former_file_list.lock().clone() self.former_file_list.lock().clone()
} }

View File

@ -1,8 +1,4 @@
use std::{ use std::{collections::BTreeSet, path::PathBuf};
collections::BTreeSet,
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
use hbb_common::log; use hbb_common::log;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -16,15 +12,11 @@ use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard};
static X11_CLIPBOARD: OnceCell<Clipboard> = OnceCell::new(); static X11_CLIPBOARD: OnceCell<Clipboard> = OnceCell::new();
// this is tested on an Arch Linux with X11
const X11_CLIPBOARD_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(70);
fn get_clip() -> Result<&'static Clipboard, CliprdrError> { fn get_clip() -> Result<&'static Clipboard, CliprdrError> {
X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)) X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit))
} }
pub struct X11Clipboard { pub struct X11Clipboard {
stop: AtomicBool,
ignore_path: PathBuf, ignore_path: PathBuf,
text_uri_list: Atom, text_uri_list: Atom,
gnome_copied_files: Atom, gnome_copied_files: Atom,
@ -50,7 +42,6 @@ impl X11Clipboard {
.map_err(|_| CliprdrError::CliprdrInit)?; .map_err(|_| CliprdrError::CliprdrInit)?;
Ok(Self { Ok(Self {
ignore_path: ignore_path.to_owned(), ignore_path: ignore_path.to_owned(),
stop: AtomicBool::new(false),
text_uri_list, text_uri_list,
gnome_copied_files, gnome_copied_files,
nautilus_clipboard, nautilus_clipboard,
@ -64,11 +55,18 @@ impl X11Clipboard {
// NOTE: // NOTE:
// # why not use `load_wait` // # why not use `load_wait`
// load_wait is likely to wait forever, which is not what we want // load_wait is likely to wait forever, which is not what we want
let res = get_clip()?.load(clip, target, prop, X11_CLIPBOARD_TIMEOUT); let res = get_clip()?.load_wait(clip, target, prop);
match res { match res {
Ok(res) => Ok(res), Ok(res) => Ok(res),
Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]), Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]),
Err(_) => Err(CliprdrError::ClipboardInternalError), 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)
}
} }
} }
@ -81,22 +79,12 @@ impl X11Clipboard {
} }
fn wait_file_list(&self) -> Result<Option<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)?;
let p = parse_plain_uri_list(v)?; let p = parse_plain_uri_list(v)?;
Ok(Some(p)) Ok(Some(p))
} }
} }
impl X11Clipboard {
#[inline]
fn is_stopped(&self) -> bool {
self.stop.load(Ordering::Relaxed)
}
}
impl SysClipboard for X11Clipboard { impl SysClipboard for X11Clipboard {
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
*self.former_file_list.lock() = paths.to_vec(); *self.former_file_list.lock() = paths.to_vec();
@ -114,19 +102,12 @@ impl SysClipboard for X11Clipboard {
.map_err(|_| CliprdrError::ClipboardInternalError) .map_err(|_| CliprdrError::ClipboardInternalError)
} }
fn stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
fn start(&self) { fn start(&self) {
self.stop.store(false, Ordering::Relaxed); {
// clear cached file list
*self.former_file_list.lock() = vec![];
}
loop { loop {
if self.is_stopped() {
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
let sth = match self.wait_file_list() { let sth = match self.wait_file_list() {
Ok(sth) => sth, Ok(sth) => sth,
Err(e) => { Err(e) => {

View File

@ -1,4 +1,3 @@
from ast import parse
import os import os
import optparse import optparse
from hashlib import md5 from hashlib import md5
@ -47,7 +46,7 @@ def write_metadata(md5_table: dict, output_folder: str, exe: str):
f.write((len(path)).to_bytes(length=length_count, byteorder='big')) f.write((len(path)).to_bytes(length=length_count, byteorder='big'))
f.write(path) f.write(path)
# data length & compressed data # data length & compressed data
f.write((data_length).to_bytes( f.write(data_length.to_bytes(
length=length_count, byteorder='big')) length=length_count, byteorder='big'))
f.write(compressed_data) f.write(compressed_data)
# md5 code # md5 code
@ -65,6 +64,8 @@ def build_portable(output_folder: str):
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py # Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe # Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
if __name__ == '__main__': if __name__ == '__main__':
parser = optparse.OptionParser() parser = optparse.OptionParser()
parser.add_option("-f", "--folder", dest="folder", parser.add_option("-f", "--folder", dest="folder",

View File

@ -5,20 +5,22 @@ import glob
import sys import sys
import csv import csv
def get_lang(lang): def get_lang(lang):
out = {} out = {}
for ln in open('./src/lang/%s.rs'%lang, encoding='utf8'): for ln in open('./src/lang/%s.rs' % lang, encoding='utf8'):
ln = ln.strip() ln = ln.strip()
if ln.startswith('("'): if ln.startswith('("'):
k, v = line_split(ln) k, v = line_split(ln)
out[k] = v out[k] = v
return out return out
def line_split(line): def line_split(line):
toks = line.split('", "') toks = line.split('", "')
if len(toks) != 2: if len(toks) != 2:
print(line) print(line)
assert(0) assert 0
# Replace fixed position. # Replace fixed position.
# Because toks[1] may be v") or v"), # Because toks[1] may be v") or v"),
k = toks[0][toks[0].find('"') + 1:] k = toks[0][toks[0].find('"') + 1:]
@ -27,62 +29,62 @@ def line_split(line):
def main(): def main():
if len(sys.argv) == 1: if len(sys.argv) == 1:
expand() expand()
elif sys.argv[1] == '1': elif sys.argv[1] == '1':
to_csv() to_csv()
else: else:
to_rs(sys.argv[1]) to_rs(sys.argv[1])
def expand(): def expand():
for fn in glob.glob('./src/lang/*.rs'): for fn in glob.glob('./src/lang/*.rs'):
lang = os.path.basename(fn)[:-3] lang = os.path.basename(fn)[:-3]
if lang in ['en','template']: continue if lang in ['en', 'template']: continue
print(lang) print(lang)
dict = get_lang(lang) dict = get_lang(lang)
fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8')
for line in open('./src/lang/template.rs', encoding='utf8'): for line in open('./src/lang/template.rs', encoding='utf8'):
line_strip = line.strip() line_strip = line.strip()
if line_strip.startswith('("'): if line_strip.startswith('("'):
k, v = line_split(line_strip) k, v = line_split(line_strip)
if k in dict: if k in dict:
# embraced with " to avoid empty v # embraced with " to avoid empty v
line = line.replace('"%s"'%v, '"%s"'%dict[k]) line = line.replace('"%s"' % v, '"%s"' % dict[k])
else: else:
line = line.replace(v, "") line = line.replace(v, "")
fw.write(line) fw.write(line)
else: else:
fw.write(line) fw.write(line)
fw.close() fw.close()
def to_csv(): def to_csv():
for fn in glob.glob('./src/lang/*.rs'): for fn in glob.glob('./src/lang/*.rs'):
lang = os.path.basename(fn)[:-3] lang = os.path.basename(fn)[:-3]
csvfile = open('./src/lang/%s.csv'%lang, "wt", encoding='utf8') csvfile = open('./src/lang/%s.csv' % lang, "wt", encoding='utf8')
csvwriter = csv.writer(csvfile) csvwriter = csv.writer(csvfile)
for line in open(fn, encoding='utf8'): for line in open(fn, encoding='utf8'):
line_strip = line.strip() line_strip = line.strip()
if line_strip.startswith('("'): if line_strip.startswith('("'):
k, v = line_split(line_strip) k, v = line_split(line_strip)
csvwriter.writerow([k, v]) csvwriter.writerow([k, v])
csvfile.close() csvfile.close()
def to_rs(lang): def to_rs(lang):
csvfile = open('%s.csv'%lang, "rt", encoding='utf8') csvfile = open('%s.csv' % lang, "rt", encoding='utf8')
fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8')
fw.write('''lazy_static::lazy_static! { fw.write('''lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> = pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[ [
''') ''')
for row in csv.reader(csvfile): for row in csv.reader(csvfile):
fw.write(' ("%s", "%s"),\n'%(row[0].replace('"', '\"'), row[1].replace('"', '\"'))) fw.write(' ("%s", "%s"),\n' % (row[0].replace('"', '\"'), row[1].replace('"', '\"')))
fw.write(''' ].iter().cloned().collect(); fw.write(''' ].iter().cloned().collect();
} }
''') ''')
fw.close() fw.close()
main() main()

View File

@ -317,6 +317,9 @@ impl<T: InvokeUiSession> Remote<T> {
if stop { if stop {
ContextSend::set_is_stopped(); ContextSend::set_is_stopped();
} else { } else {
if let Err(e) = ContextSend::make_sure_enabled() {
log::error!("failed to restart clipboard context: {}", e);
};
log::debug!("Send system clipboard message to remote"); log::debug!("Send system clipboard message to remote");
let msg = crate::clipboard_file::clip_2_msg(clip); let msg = crate::clipboard_file::clip_2_msg(clip);
allow_err!(peer.send(&msg).await); allow_err!(peer.send(&msg).await);
@ -1704,7 +1707,13 @@ impl<T: InvokeUiSession> Remote<T> {
} }
fn check_clipboard_file_context(&self) { fn check_clipboard_file_context(&self) {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] #[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() let enabled = *self.handler.server_file_transfer_enabled.read().unwrap()
&& self.handler.lc.read().unwrap().enable_file_transfer.v; && self.handler.lc.read().unwrap().enable_file_transfer.v;
@ -1736,6 +1745,9 @@ impl<T: InvokeUiSession> Remote<T> {
"Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}",
stop, is_stopping_allowed, file_transfer_enabled); stop, is_stopping_allowed, file_transfer_enabled);
if !stop { if !stop {
if let Err(e) = ContextSend::make_sure_enabled() {
log::error!("failed to restart clipboard context: {}", e);
};
let _ = ContextSend::proc(|context| -> ResultType<()> { let _ = ContextSend::proc(|context| -> ResultType<()> {
context context
.server_clip_file(self.client_conn_id, clip) .server_clip_file(self.client_conn_id, clip)

View File

@ -14,22 +14,22 @@ pub enum GrabState {
#[cfg(not(any( #[cfg(not(any(
target_os = "android", target_os = "android",
target_os = "ios", target_os = "ios",
all(target_os = "linux", not(feature = "wayland")) all(target_os = "linux", feature = "unix-file-copy-paste")
)))] )))]
pub use arboard::Clipboard as ClipboardContext; pub use arboard::Clipboard as ClipboardContext;
#[cfg(all(target_os = "linux", not(feature = "wayland")))] #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
static X11_CLIPBOARD: once_cell::sync::OnceCell<x11_clipboard::Clipboard> = static X11_CLIPBOARD: once_cell::sync::OnceCell<x11_clipboard::Clipboard> =
once_cell::sync::OnceCell::new(); once_cell::sync::OnceCell::new();
#[cfg(all(target_os = "linux", not(feature = "wayland")))] #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> {
X11_CLIPBOARD X11_CLIPBOARD
.get_or_try_init(|| x11_clipboard::Clipboard::new()) .get_or_try_init(|| x11_clipboard::Clipboard::new())
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
#[cfg(all(target_os = "linux", not(feature = "wayland")))] #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
pub struct ClipboardContext { pub struct ClipboardContext {
string_setter: x11rb::protocol::xproto::Atom, string_setter: x11rb::protocol::xproto::Atom,
string_getter: x11rb::protocol::xproto::Atom, string_getter: x11rb::protocol::xproto::Atom,
@ -39,7 +39,7 @@ pub struct ClipboardContext {
prop: x11rb::protocol::xproto::Atom, prop: x11rb::protocol::xproto::Atom,
} }
#[cfg(all(target_os = "linux", not(feature = "wayland")))] #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
fn parse_plain_uri_list(v: Vec<u8>) -> Result<String, String> { fn parse_plain_uri_list(v: Vec<u8>) -> Result<String, String> {
let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?;
let mut list = String::new(); let mut list = String::new();
@ -56,7 +56,7 @@ fn parse_plain_uri_list(v: Vec<u8>) -> Result<String, String> {
Ok(list) Ok(list)
} }
#[cfg(all(target_os = "linux", not(feature = "wayland")))] #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
impl ClipboardContext { impl ClipboardContext {
pub fn new() -> Result<Self, String> { pub fn new() -> Result<Self, String> {
let clipboard = get_clipboard()?; let clipboard = get_clipboard()?;
@ -87,7 +87,7 @@ impl ClipboardContext {
let clip = self.clip; let clip = self.clip;
let prop = self.prop; let prop = self.prop;
const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(100); const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120);
let text_content = get_clipboard()? let text_content = get_clipboard()?
.load(clip, self.string_getter, prop, TIMEOUT) .load(clip, self.string_getter, prop, TIMEOUT)

View File

@ -1725,6 +1725,17 @@ pub fn main_use_texture_render() -> SyncReturn<bool> {
} }
} }
pub fn main_has_file_clipboard() -> SyncReturn<bool> {
let ret = cfg!(any(
target_os = "windows",
all(
feature = "unix-file-copy-paste",
any(target_os = "linux", target_os = "macos")
)
));
SyncReturn(ret)
}
pub fn cm_init() { pub fn cm_init() {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::flutter::connection_manager::cm_init(); crate::flutter::connection_manager::cm_init();

View File

@ -570,7 +570,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Big tiles", "Große Kacheln"), ("Big tiles", "Große Kacheln"),
("Small tiles", "Kleine Kacheln"), ("Small tiles", "Kleine Kacheln"),
("List", "Liste"), ("List", "Liste"),
("Virtual display", ""), ("Virtual display", "Virtueller Bildschirm"),
("Plug out all", ""), ("Plug out all", "Alle ausschalten"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -570,7 +570,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Big tiles", "Icone grandi"), ("Big tiles", "Icone grandi"),
("Small tiles", "Icone piccole"), ("Small tiles", "Icone piccole"),
("List", "Elenco"), ("List", "Elenco"),
("Virtual display", ""), ("Virtual display", "Scehrmo virtuale"),
("Plug out all", ""), ("Plug out all", "Scollega tutto"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Chat", "Czat"), ("Chat", "Czat"),
("Total", "Łącznie"), ("Total", "Łącznie"),
("items", "elementów"), ("items", "elementów"),
("Selected", "Zaznaczonych"), ("Selected", "zaznaczonych"),
("Screen Capture", "Przechwytywanie ekranu"), ("Screen Capture", "Przechwytywanie ekranu"),
("Input Control", "Kontrola wejścia"), ("Input Control", "Kontrola wejścia"),
("Audio Capture", "Przechwytywanie dźwięku"), ("Audio Capture", "Przechwytywanie dźwięku"),
@ -564,13 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("elevated_switch_display_msg", "Przełącz się na ekran główny, ponieważ wyświetlanie kilku ekranów nie jest obsługiwane przy podniesionych uprawnieniach."), ("elevated_switch_display_msg", "Przełącz się na ekran główny, ponieważ wyświetlanie kilku ekranów nie jest obsługiwane przy podniesionych uprawnieniach."),
("Open in new window", "Otwórz w nowym oknie"), ("Open in new window", "Otwórz w nowym oknie"),
("Show displays as individual windows", "Pokaż ekrany w osobnych oknach"), ("Show displays as individual windows", "Pokaż ekrany w osobnych oknach"),
("Use all my displays for the remote session", ""), ("Use all my displays for the remote session", "Użyj wszystkich moich ekranów do zdalnej sesji"),
("selinux_tip", ""), ("selinux_tip", "SELinux jest włączony na Twoim urządzeniu, co może przeszkodzić w uruchomieniu RustDesk po stronie kontrolowanej."),
("Change view", ""), ("Change view", "Zmień widok"),
("Big tiles", ""), ("Big tiles", "Duże kafelki"),
("Small tiles", ""), ("Small tiles", "Małe kafelki"),
("List", ""), ("List", "Lista"),
("Virtual display", ""), ("Virtual display", "Witualne ekrany"),
("Plug out all", ""), ("Plug out all", "Odłącz wszystko"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -570,7 +570,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Big tiles", "Большие значки"), ("Big tiles", "Большие значки"),
("Small tiles", "Маленькие значки"), ("Small tiles", "Маленькие значки"),
("List", "Список"), ("List", "Список"),
("Virtual display", ""), ("Virtual display", "Виртуальный дисплей"),
("Plug out all", ""), ("Plug out all", "Отключить все"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -1032,7 +1032,7 @@ impl Connection {
pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.hostname = DEVICE_NAME.lock().unwrap().clone();
pi.platform = "Android".into(); pi.platform = "Android".into();
} }
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
let mut platform_additions = serde_json::Map::new(); let mut platform_additions = serde_json::Map::new();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -1062,7 +1062,18 @@ impl Connection {
} }
} }
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(
target_os = "windows",
all(
any(target_os = "linux", target_os = "macos"),
feature = "unix-file-copy-paste"
)
))]
{
platform_additions.insert("has_file_clipboard".into(), json!(true));
}
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
if !platform_additions.is_empty() { if !platform_additions.is_empty() {
pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into());
} }

View File

@ -196,7 +196,7 @@ class Header: Reactor.Component {
{!cursor_embedded && <li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>{translate('Show remote cursor')}</li>} {!cursor_embedded && <li #show-remote-cursor .toggle-option><span>{svg_checkmark}</span>{translate('Show remote cursor')}</li>}
<li #show-quality-monitor .toggle-option><span>{svg_checkmark}</span>{translate('Show quality monitor')}</li> <li #show-quality-monitor .toggle-option><span>{svg_checkmark}</span>{translate('Show quality monitor')}</li>
{audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>{translate('Mute')}</li> : ""} {audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>{translate('Mute')}</li> : ""}
{((is_win && pi.platform == "Windows")||(is_linux && pi.platform == "Linux"))||(is_osx && pi.platform == "Mac OS") && file_enabled ? <li #enable-file-transfer .toggle-option><span>{svg_checkmark}</span>{translate('Allow file copy and paste')}</li> : ""} {(is_win && pi.platform == "Windows") && file_enabled ? <li #enable-file-transfer .toggle-option><span>{svg_checkmark}</span>{translate('Allow file copy and paste')}</li> : ""}
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""} {keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""} {keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
{keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""} {keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}

View File

@ -575,12 +575,13 @@ pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
} }
}); });
log::debug!( #[cfg(any(
"start_ipc enable context_send: {}", target_os = "windows",
Config::get_option("enable-file-transfer").is_empty() all(
); any(target_os = "linux", target_os = "macos"),
feature = "unix-file-copy-paste"
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] ),
))]
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

@ -1008,6 +1008,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc:
#[cfg(not(feature = "flutter"))] #[cfg(not(feature = "flutter"))]
let mut id = "".to_owned(); let mut id = "".to_owned();
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
#[allow(unused_mut, dead_code)]
let mut enable_file_transfer = "".to_owned(); let mut enable_file_transfer = "".to_owned();
loop { loop {
@ -1030,7 +1031,13 @@ 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(any(target_os="windows", target_os="linux", target_os = "macos"))] #[cfg(any(
target_os = "windows",
all(
any(target_os="linux", target_os = "macos"),
feature = "unix-file-copy-paste"
)
))]
{ {
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

@ -1420,7 +1420,13 @@ 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>, round: u32) { pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>, round: u32) {
// It is ok to call this function multiple times. // It is ok to call this function multiple times.
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] #[cfg(any(
target_os = "windows",
all(
any(target_os = "linux", target_os = "macos"),
feature = "unix-file-copy-paste"
)
))]
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);
} }