refact: file copy&paste, cross platform (no macOS) (#10671)

* feat: unix, file copy&paste

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

* refact: unix file c&p, check peer version

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

* Update pubspec.yaml

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
fufesou
2025-02-04 20:33:02 +08:00
committed by GitHub
parent a27fa43081
commit fbba8f0b34
42 changed files with 2026 additions and 1778 deletions

View File

@@ -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<Data>,
#[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<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
static ref CLIPBOARD_STATE: Arc<Mutex<ClipboardState>> = 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<ClientClipboardContext>,
) -> Option<UnboundedReceiver<()>> {
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<UnboundedReceiver<()>> {
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<crate::clipboard::ClipboardContext>,
tx_cb_result: Sender<CallbackResult>,
#[cfg(not(feature = "flutter"))]
client_clip_ctx: Option<ClientClipboardContext>,
}
#[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)),

View File

@@ -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<T: InvokeUiSession> {
handler: Session<T>,
@@ -63,7 +64,7 @@ pub struct Remote<T: InvokeUiSession> {
last_update_jobs_status: (Instant, HashMap<i32, u64>),
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<AtomicUsize>,
video_format: CodecFormat,
@@ -107,7 +108,7 @@ impl<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
}
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<T: InvokeUiSession> Remote<T> {
}
// 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::<i32>();
#[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<T: InvokeUiSession> Remote<T> {
}
}
_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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
.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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
#[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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
#[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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
};
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);
}
}
}
}

View File

@@ -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<x11_clipboard::Clipboard> =
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<u8>) -> Result<String, String> {
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<Self, String> {
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<String, String> {
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<ClipboardContext>,
@@ -179,6 +75,73 @@ pub fn check_clipboard(
None
}
#[cfg(feature = "unix-file-copy-paste")]
pub fn check_clipboard_files(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<Vec<String>> {
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<String>, 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<MultiClipboards> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
@@ -203,10 +166,15 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
#[cfg(not(target_os = "android"))]
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, 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<ClipboardData>, 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<Clipboard>, 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<ClipboardContext> {
@@ -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<Vec<ClipboardData>> {
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<Vec<ClipboardData>> {
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<Option<Vec<String>>> {
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<String> = 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::<Vec<_>>(),
),
_ => None,
})
.flatten()
.collect::<Vec<_>>();
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<Shutdown>, 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<Message> {
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<Mutex<ClipboardListener>> = Default::default();
}
struct Handler {
subscribers: Arc<Mutex<HashMap<String, Sender<CallbackResult>>>>,
}
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<Mutex<HashMap<String, Sender<CallbackResult>>>>,
handle: Option<(Shutdown, JoinHandle<()>)>,
}
pub fn subscribe(name: String, tx: Sender<CallbackResult>) -> 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<Shutdown>, 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
}
}

View File

@@ -189,3 +189,206 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option<ClipboardFile> {
_ => 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<Mutex<Option<crate::clipboard::ClipboardContext>>> = 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<Message> {
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
}
}

View File

@@ -89,7 +89,7 @@ lazy_static::lazy_static! {
pub struct SimpleCallOnReturn {
pub b: bool,
pub f: Box<dyn Fn() + 'static>,
pub f: Box<dyn Fn() + Send + 'static>,
}
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<FileEntry>) -> Strin
}
pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Map<String, Value> {
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));

View File

@@ -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 {

View File

@@ -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<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")
)
));
let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",));
SyncReturn(ret)
}

View File

@@ -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<Box<(Config, Config2)>>),
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(target_os = "windows")]
ClipboardFile(ClipboardFile),
ClipboardFileEnabled(bool),
#[cfg(target_os = "windows")]

View File

@@ -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")))]
{

View File

@@ -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<ClipboardContext>,
tx_cb_result: Sender<CallbackResult>,
#[cfg(target_os = "windows")]
stream: Option<ipc::ConnectionTmpl<parity_tokio_ipc::ConnectionClient>>,
#[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<Message> {
#[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)
}

View File

@@ -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<i32>;
#[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::<i32>();
}
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) {

View File

@@ -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);

View File

@@ -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 <popup>
<menu.context #display-options>
<li #adjust-window style="display:none">{translate('Adjust Window')}</li>
@@ -201,7 +208,7 @@ class Header: Reactor.Component {
{<li #follow-remote-window .toggle-option><span>{svg_checkmark}</span>{translate('Follow remote window focus')}</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> : ""}
{(is_win && pi.platform == "Windows") && file_enabled ? <li #enable-file-copy-paste .toggle-option><span>{svg_checkmark}</span>{translate('Enable file copy and paste')}</li> : ""}
{is_file_copy_paste_supported && file_enabled ? <li #enable-file-copy-paste .toggle-option><span>{svg_checkmark}</span>{translate('Enable file copy and paste')}</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 && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}

View File

@@ -66,6 +66,39 @@ impl SciterHandler {
}
displays_value
}
fn make_platform_additions(data: &str) -> Option<Value> {
if let Ok(v2) = serde_json::from_str::<HashMap<String, serde_json::Value>>(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);

View File

@@ -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<T: InvokeUiCM> {
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<T: InvokeUiCM> ConnectionManager<T> {
}
#[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<T: InvokeUiCM> ConnectionManager<T> {
.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<T: InvokeUiCM> IpcTaskRunner<T> {
// for tmp use, without real conn id
let mut write_jobs: Vec<fs::TransferJob> = 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::<clipboard::ClipboardFile>();
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::<i32>();
}
#[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<T: InvokeUiCM> IpcTaskRunner<T> {
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<T: InvokeUiCM> IpcTaskRunner<T> {
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<T: InvokeUiCM> IpcTaskRunner<T> {
}
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<T: InvokeUiCM> IpcTaskRunner<T> {
},
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<T: InvokeUiCM> IpcTaskRunner<T> {
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<T: InvokeUiCM> IpcTaskRunner<T> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tokio::main(flavor = "current_thread")]
pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
#[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),

View File

@@ -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<ipc:
let mut video_conn_count = 0;
#[cfg(not(feature = "flutter"))]
let mut id = "".to_owned();
#[cfg(any(
target_os = "windows",
all(
any(target_os = "linux", target_os = "macos"),
feature = "unix-file-copy-paste"
)
))]
#[cfg(target_os = "windows")]
let mut enable_file_transfer = "".to_owned();
let is_cm = crate::common::is_cm();
@@ -1183,13 +1178,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc:
*OPTIONS.lock().unwrap() = v;
*OPTION_SYNCED.lock().unwrap() = true;
#[cfg(any(
target_os = "windows",
all(
any(target_os="linux", target_os = "macos"),
feature = "unix-file-copy-paste"
)
))]
#[cfg(target_os = "windows")]
{
let b = OPTIONS.lock().unwrap().get(OPTION_ENABLE_FILE_TRANSFER).map(|x| x.to_string()).unwrap_or_default();
if b != enable_file_transfer {

View File

@@ -162,6 +162,13 @@ impl SessionPermissionConfig {
&& *self.server_keyboard_enabled.read().unwrap()
&& !self.lc.read().unwrap().disable_clipboard.v
}
#[cfg(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
}
}
impl<T: InvokeUiSession> Session<T> {
@@ -324,7 +331,7 @@ impl<T: InvokeUiSession> Session<T> {
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<T: InvokeUiSession> Session<T> {
&& !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) {