895 lines
31 KiB
Rust
895 lines
31 KiB
Rust
#[cfg(not(target_os = "android"))]
|
|
use arboard::{ClipboardData, ClipboardFormat};
|
|
use hbb_common::{bail, log, message_proto::*, ResultType};
|
|
use std::{
|
|
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.
|
|
const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner";
|
|
|
|
// Add special format for Excel XML Spreadsheet
|
|
const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet";
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
lazy_static::lazy_static! {
|
|
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
|
|
// cache the clipboard msg
|
|
static ref LAST_MULTI_CLIPBOARDS: Arc<Mutex<MultiClipboards>> = Arc::new(Mutex::new(MultiClipboards::new()));
|
|
// For updating in server and getting content in cm.
|
|
// Clipboard on Linux is "server--clients" mode.
|
|
// The clipboard content is owned by the server and passed to the clients when requested.
|
|
// Plain text is the only exception, it does not require the server to be present.
|
|
static ref CLIPBOARD_CTX: Arc<Mutex<Option<ClipboardContext>>> = Arc::new(Mutex::new(None));
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
const CLIPBOARD_GET_MAX_RETRY: usize = 3;
|
|
#[cfg(not(target_os = "android"))]
|
|
const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33);
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
|
|
ClipboardFormat::Text,
|
|
ClipboardFormat::Html,
|
|
ClipboardFormat::Rtf,
|
|
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(not(target_os = "android"))]
|
|
pub fn check_clipboard(
|
|
ctx: &mut Option<ClipboardContext>,
|
|
side: ClipboardSide,
|
|
force: bool,
|
|
) -> Option<Message> {
|
|
if ctx.is_none() {
|
|
*ctx = ClipboardContext::new().ok();
|
|
}
|
|
let ctx2 = ctx.as_mut()?;
|
|
match ctx2.get(side, force) {
|
|
Ok(content) => {
|
|
if !content.is_empty() {
|
|
let mut msg = Message::new();
|
|
let clipboards = proto::create_multi_clipboards(content);
|
|
msg.set_multi_clipboards(clipboards.clone());
|
|
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
|
|
return Some(msg);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to get clipboard content. {}", e);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
|
pub fn is_file_url_set_by_rustdesk(url: &Vec<String>) -> bool {
|
|
if url.len() != 1 {
|
|
return false;
|
|
}
|
|
url.iter()
|
|
.next()
|
|
.map(|s| {
|
|
for prefix in &["file:///tmp/.rustdesk_", "//tmp/.rustdesk_"] {
|
|
if s.starts_with(prefix) {
|
|
return s[prefix.len()..].parse::<uuid::Uuid>().is_ok();
|
|
}
|
|
}
|
|
false
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[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(all(target_os = "linux", 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) {
|
|
std::thread::spawn(move || {
|
|
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
|
if ctx.is_none() {
|
|
match ClipboardContext::new() {
|
|
Ok(x) => {
|
|
*ctx = Some(x);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to create clipboard context: {}", e);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
#[allow(unused_mut)]
|
|
if let Some(mut ctx) = ctx.as_mut() {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use clipboard::platform::unix;
|
|
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
|
ctx.try_empty_clipboard_files(_side);
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
ctx.try_empty_clipboard_files(_side);
|
|
// No need to make sure the context is enabled.
|
|
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
|
context.empty_clipboard(_conn_id).ok();
|
|
Ok(())
|
|
})
|
|
.ok();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[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();
|
|
if ctx.is_none() {
|
|
match ClipboardContext::new() {
|
|
Ok(x) => {
|
|
*ctx = Some(x);
|
|
}
|
|
Err(e) => {
|
|
hbb_common::bail!("Failed to create clipboard context: {}", e);
|
|
}
|
|
}
|
|
}
|
|
if let Some(ctx) = ctx.as_mut() {
|
|
let content = ctx.get(ClipboardSide::Host, false)?;
|
|
let clipboards = proto::create_multi_clipboards(content);
|
|
Ok(clipboards)
|
|
} else {
|
|
hbb_common::bail!("Failed to create clipboard context");
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
|
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() {
|
|
Ok(x) => {
|
|
*ctx = Some(x);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to create clipboard context: {}", e);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if let Some(ctx) = ctx.as_mut() {
|
|
to_update_data.push(ClipboardData::Special((
|
|
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
|
|
side.get_owner_data(),
|
|
)));
|
|
if let Err(e) = ctx.set(&to_update_data) {
|
|
log::debug!("Failed to set clipboard: {}", e);
|
|
} else {
|
|
log::debug!("{} updated on {}", CLIPBOARD_NAME, side);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
|
std::thread::spawn(move || {
|
|
update_clipboard_(multi_clipboards, side);
|
|
});
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
pub struct ClipboardContext {
|
|
inner: arboard::Clipboard,
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
#[allow(unreachable_code)]
|
|
impl ClipboardContext {
|
|
pub fn new() -> ResultType<ClipboardContext> {
|
|
let board;
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
board = arboard::Clipboard::new()?;
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
let mut i = 1;
|
|
loop {
|
|
// Try 5 times to create clipboard
|
|
// Arboard::new() connect to X server or Wayland compositor, which shoud be ok at most time
|
|
// But sometimes, the connection may fail, so we retry here.
|
|
match arboard::Clipboard::new() {
|
|
Ok(x) => {
|
|
board = x;
|
|
break;
|
|
}
|
|
Err(e) => {
|
|
if i == 5 {
|
|
return Err(e.into());
|
|
} else {
|
|
std::thread::sleep(std::time::Duration::from_millis(30 * i));
|
|
}
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
Ok(ClipboardContext { inner: board })
|
|
}
|
|
|
|
fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType<Vec<ClipboardData>> {
|
|
// If there're multiple threads or processes trying to access the clipboard at the same time,
|
|
// the previous clipboard owner will fail to access the clipboard.
|
|
// `GetLastError()` will return `ERROR_CLIPBOARD_NOT_OPEN` (OSError(1418): Thread does not have a clipboard open) at this time.
|
|
// See https://github.com/rustdesk-org/arboard/blob/747ab2d9b40a5c9c5102051cf3b0bb38b4845e60/src/platform/windows.rs#L34
|
|
//
|
|
// This is a common case on Windows, so we retry here.
|
|
// Related issues:
|
|
// 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(formats) {
|
|
Ok(data) => {
|
|
return Ok(data
|
|
.into_iter()
|
|
.filter(|c| !matches!(c, arboard::ClipboardData::None))
|
|
.collect())
|
|
}
|
|
Err(e) => match e {
|
|
arboard::Error::ClipboardOccupied => {
|
|
log::debug!("Failed to get clipboard formats, clipboard is occupied, retrying... {}", i + 1);
|
|
std::thread::sleep(CLIPBOARD_GET_RETRY_INTERVAL_DUR);
|
|
}
|
|
_ => {
|
|
log::error!("Failed to get clipboard formats, {}", e);
|
|
return Err(e.into());
|
|
}
|
|
},
|
|
}
|
|
}
|
|
bail!("Failed to get clipboard formats, clipboard is occupied, {CLIPBOARD_GET_MAX_RETRY} retries failed");
|
|
}
|
|
|
|
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(formats)?;
|
|
if data.is_empty() {
|
|
return Ok(data);
|
|
}
|
|
if !force {
|
|
for c in data.iter() {
|
|
if let ClipboardData::Special((s, d)) = c {
|
|
if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) {
|
|
return Ok(vec![]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(data
|
|
.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(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
|
fn get_file_urls_set_by_rustdesk(
|
|
data: Vec<ClipboardData>,
|
|
_side: ClipboardSide,
|
|
) -> Vec<String> {
|
|
for item in data.into_iter() {
|
|
if let ClipboardData::FileUrl(urls) = item {
|
|
if is_file_url_set_by_rustdesk(&urls) {
|
|
return urls;
|
|
}
|
|
}
|
|
}
|
|
vec![]
|
|
}
|
|
|
|
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
|
fn get_file_urls_set_by_rustdesk(data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<String> {
|
|
let exclude_path =
|
|
clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client);
|
|
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<_>>()
|
|
}
|
|
|
|
#[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]) {
|
|
let urls = Self::get_file_urls_set_by_rustdesk(data, side);
|
|
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 = {
|
|
use hbb_common::platform::linux::CMD_SH;
|
|
let is_kde = std::process::Command::new(CMD_SH.as_str())
|
|
.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 {
|
|
use hbb_common::get_version_number;
|
|
if get_version_number(peer_version) < get_version_number("1.3.0") {
|
|
return false;
|
|
}
|
|
if ["", &hbb_common::whoami::Platform::Ios.to_string()].contains(&peer_platform) {
|
|
return false;
|
|
}
|
|
if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3")
|
|
{
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
pub fn get_current_clipboard_msg(
|
|
peer_version: &str,
|
|
peer_platform: &str,
|
|
side: ClipboardSide,
|
|
) -> Option<Message> {
|
|
let mut multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap();
|
|
if multi_clipboards.clipboards.is_empty() {
|
|
let mut ctx = ClipboardContext::new().ok()?;
|
|
*multi_clipboards = proto::create_multi_clipboards(ctx.get(side, true).ok()?);
|
|
}
|
|
if multi_clipboards.clipboards.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
if is_support_multi_clipboard(peer_version, peer_platform) {
|
|
let mut msg = Message::new();
|
|
msg.set_multi_clipboards(multi_clipboards.clone());
|
|
Some(msg)
|
|
} else {
|
|
// Find the first text clipboard and send it.
|
|
multi_clipboards
|
|
.clipboards
|
|
.iter()
|
|
.find(|c| c.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text))
|
|
.map(|c| {
|
|
let mut msg = Message::new();
|
|
msg.set_clipboard(c.clone());
|
|
msg
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
|
pub enum ClipboardSide {
|
|
Host,
|
|
Client,
|
|
}
|
|
|
|
impl ClipboardSide {
|
|
// 01: the clipboard is owned by the host
|
|
// 10: the clipboard is owned by the client
|
|
fn get_owner_data(&self) -> Vec<u8> {
|
|
match self {
|
|
ClipboardSide::Host => vec![0b01],
|
|
ClipboardSide::Client => vec![0b10],
|
|
}
|
|
}
|
|
|
|
fn is_owner(&self, data: &[u8]) -> bool {
|
|
if data.len() == 0 {
|
|
return false;
|
|
}
|
|
data[0] & 0b11 != 0
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for ClipboardSide {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ClipboardSide::Host => write!(f, "host"),
|
|
ClipboardSide::Client => write!(f, "client"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub use proto::get_msg_if_not_support_multi_clip;
|
|
mod proto {
|
|
#[cfg(not(target_os = "android"))]
|
|
use arboard::ClipboardData;
|
|
use hbb_common::{
|
|
compress::{compress as compress_func, decompress},
|
|
message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards},
|
|
};
|
|
|
|
fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard {
|
|
let compressed = compress_func(s.as_bytes());
|
|
let compress = compressed.len() < s.as_bytes().len();
|
|
let content = if compress {
|
|
compressed
|
|
} else {
|
|
s.bytes().collect::<Vec<u8>>()
|
|
};
|
|
Clipboard {
|
|
compress,
|
|
content: content.into(),
|
|
format: format.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn image_to_proto(a: arboard::ImageData) -> Clipboard {
|
|
match &a {
|
|
arboard::ImageData::Rgba(rgba) => {
|
|
let compressed = compress_func(&a.bytes());
|
|
let compress = compressed.len() < a.bytes().len();
|
|
let content = if compress {
|
|
compressed
|
|
} else {
|
|
a.bytes().to_vec()
|
|
};
|
|
Clipboard {
|
|
compress,
|
|
content: content.into(),
|
|
width: rgba.width as _,
|
|
height: rgba.height as _,
|
|
format: ClipboardFormat::ImageRgba.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
arboard::ImageData::Png(png) => Clipboard {
|
|
compress: false,
|
|
content: png.to_owned().to_vec().into(),
|
|
format: ClipboardFormat::ImagePng.into(),
|
|
..Default::default()
|
|
},
|
|
arboard::ImageData::Svg(_) => {
|
|
let compressed = compress_func(&a.bytes());
|
|
let compress = compressed.len() < a.bytes().len();
|
|
let content = if compress {
|
|
compressed
|
|
} else {
|
|
a.bytes().to_vec()
|
|
};
|
|
Clipboard {
|
|
compress,
|
|
content: content.into(),
|
|
format: ClipboardFormat::ImageSvg.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn special_to_proto(d: Vec<u8>, s: String) -> Clipboard {
|
|
let compressed = compress_func(&d);
|
|
let compress = compressed.len() < d.len();
|
|
let content = if compress {
|
|
compressed
|
|
} else {
|
|
s.bytes().collect::<Vec<u8>>()
|
|
};
|
|
Clipboard {
|
|
compress,
|
|
content: content.into(),
|
|
format: ClipboardFormat::Special.into(),
|
|
special_name: s,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn clipboard_data_to_proto(data: ClipboardData) -> Option<Clipboard> {
|
|
let d = match data {
|
|
ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text),
|
|
ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf),
|
|
ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html),
|
|
ClipboardData::Image(a) => image_to_proto(a),
|
|
ClipboardData::Special((s, d)) => special_to_proto(d, s),
|
|
_ => return None,
|
|
};
|
|
Some(d)
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
pub fn create_multi_clipboards(vec_data: Vec<ClipboardData>) -> MultiClipboards {
|
|
MultiClipboards {
|
|
clipboards: vec_data
|
|
.into_iter()
|
|
.filter_map(clipboard_data_to_proto)
|
|
.collect(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn from_clipboard(clipboard: Clipboard) -> Option<ClipboardData> {
|
|
let data = if clipboard.compress {
|
|
decompress(&clipboard.content)
|
|
} else {
|
|
clipboard.content.into()
|
|
};
|
|
match clipboard.format.enum_value() {
|
|
Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text),
|
|
Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf),
|
|
Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html),
|
|
Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba(
|
|
clipboard.width as _,
|
|
clipboard.height as _,
|
|
data.into(),
|
|
))),
|
|
Ok(ClipboardFormat::ImagePng) => {
|
|
Some(ClipboardData::Image(arboard::ImageData::png(data.into())))
|
|
}
|
|
Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg(
|
|
std::str::from_utf8(&data).unwrap_or_default(),
|
|
))),
|
|
Ok(ClipboardFormat::Special) => {
|
|
Some(ClipboardData::Special((clipboard.special_name, data)))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
|
|
multi_clipboards
|
|
.into_iter()
|
|
.filter_map(from_clipboard)
|
|
.collect()
|
|
}
|
|
|
|
pub fn get_msg_if_not_support_multi_clip(
|
|
version: &str,
|
|
platform: &str,
|
|
multi_clipboards: &MultiClipboards,
|
|
) -> Option<Message> {
|
|
if crate::clipboard::is_support_multi_clipboard(version, platform) {
|
|
return None;
|
|
}
|
|
|
|
// Find the first text clipboard and send it.
|
|
multi_clipboards
|
|
.clipboards
|
|
.iter()
|
|
.find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text))
|
|
.map(|c| {
|
|
let mut msg = Message::new();
|
|
msg.set_clipboard(c.clone());
|
|
msg
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub fn handle_msg_clipboard(mut cb: Clipboard) {
|
|
use hbb_common::protobuf::Message;
|
|
|
|
if cb.compress {
|
|
cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content));
|
|
}
|
|
let multi_clips = MultiClipboards {
|
|
clipboards: vec![cb],
|
|
..Default::default()
|
|
};
|
|
if let Ok(bytes) = multi_clips.write_to_bytes() {
|
|
let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) {
|
|
use hbb_common::protobuf::Message;
|
|
|
|
for cb in mcb.clipboards.iter_mut() {
|
|
if cb.compress {
|
|
cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content));
|
|
}
|
|
}
|
|
if let Ok(bytes) = mcb.write_to_bytes() {
|
|
let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub fn get_clipboards_msg(client: bool) -> Option<Message> {
|
|
let mut clipboards = scrap::android::ffi::get_clipboards(client)?;
|
|
let mut msg = Message::new();
|
|
for c in &mut clipboards.clipboards {
|
|
let compressed = hbb_common::compress::compress(&c.content);
|
|
let compress = compressed.len() < c.content.len();
|
|
if compress {
|
|
c.content = compressed.into();
|
|
}
|
|
c.compress = compress;
|
|
}
|
|
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
|
|
}
|
|
}
|