Skip to content

Commit

Permalink
refact: clipboard listener
Browse files Browse the repository at this point in the history
Signed-off-by: fufesou <[email protected]>
  • Loading branch information
fufesou committed Dec 27, 2024
1 parent f5e8828 commit cfddcd5
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 198 deletions.
118 changes: 53 additions & 65 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[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")))]
Expand Down Expand Up @@ -174,6 +176,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,
Expand Down Expand Up @@ -685,6 +689,8 @@ impl Client {
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
return;
}
#[cfg(not(target_os = "android"))]
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
CLIPBOARD_STATE.lock().unwrap().running = false;
}

Expand All @@ -703,40 +709,33 @@ impl Client {
}

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 || {
tx_started.send(()).ok();
let mut handler = ClientClipboardHandler {
ctx: None,
#[cfg(not(feature = "flutter"))]
client_clip_ctx: _client_clip_ctx,
};

tx_started.send(()).ok();
loop {
if !CLIPBOARD_STATE.lock().unwrap().running {
break;
}
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;
Expand All @@ -746,12 +745,13 @@ 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();
log::info!("Stop client clipboard loop");
CLIPBOARD_STATE.lock().unwrap().running = false;
});

Expand All @@ -766,7 +766,7 @@ impl Client {
}
clipboard_lock.running = true;

log::info!("Start text clipboard loop");
log::info!("Start client clipboard loop");
std::thread::spawn(move || {
loop {
if !CLIPBOARD_STATE.lock().unwrap().running {
Expand All @@ -783,7 +783,7 @@ impl Client {

std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
}
log::info!("Stop text clipboard loop");
log::info!("Stop client clipboard loop");
CLIPBOARD_STATE.lock().unwrap().running = false;
});

Expand All @@ -807,7 +807,6 @@ impl ClipboardState {
#[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>,
}
Expand Down Expand Up @@ -843,6 +842,30 @@ impl ClientClipboardHandler {
}
}

fn check_clipboard(&mut self) {
if CLIPBOARD_STATE.lock().unwrap().running {
#[cfg(feature = "unix-file-copy-paste")]
if self.is_file_required() {
if let Some(msg) =
check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false)
{
if !msg.is_empty() {
let msg =
crate::clipboard_file::clip_2_msg(unix_file_clip::get_format_list());
self.send_msg(msg, true);
return;
}
}
}

if self.is_text_required() {
if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) {
self.send_msg(msg, false);
}
}
}
}

#[inline]
#[cfg(feature = "flutter")]
fn send_msg(&self, msg: Message, _is_file: bool) {
Expand Down Expand Up @@ -875,41 +898,6 @@ impl ClientClipboardHandler {
}
}

#[cfg(not(any(target_os = "android", target_os = "ios")))]
impl ClipboardHandler for ClientClipboardHandler {
fn on_clipboard_change(&mut self) -> CallbackResult {
if CLIPBOARD_STATE.lock().unwrap().running {
#[cfg(feature = "unix-file-copy-paste")]
if self.is_file_required() {
if let Some(msg) =
check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false)
{
if !msg.is_empty() {
let msg =
crate::clipboard_file::clip_2_msg(unix_file_clip::get_format_list());
self.send_msg(msg, true);
return CallbackResult::Next;
}
}
}

if self.is_text_required() {
if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) {
self.send_msg(msg, false);
}
}
}
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 {
Expand Down
165 changes: 133 additions & 32 deletions src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#[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},
Expand Down Expand Up @@ -441,36 +439,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"))]
Expand Down Expand Up @@ -685,3 +653,136 @@ 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, 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
}
}
Loading

0 comments on commit cfddcd5

Please sign in to comment.