mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-25 17:54:55 +03:00
* fix(keyboard): wayland clipboard input prompt Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): clipboard input, remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): dialog, better enableAndContinue Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog consent Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): prompt text Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): text input 1. Use `keysym` for the installed version if possible. 2. Use the clipboard if the string cannot be fully handled by `keysym`. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): dialog, title type Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): better decode_utf8_prefix() Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): better process_chr() Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): unit tests Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog, no icon Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog, Toast show the result Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog, showToast() on persist failed Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt, better dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog, translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): better wayland clipboard input prompt Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): wayland clipboard, link external app Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): trivial changes Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): wayland clipboard input, dialog content Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): tranlsations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
386 lines
15 KiB
Rust
386 lines
15 KiB
Rust
use super::*;
|
||
#[cfg(not(target_os = "android"))]
|
||
use crate::clipboard::clipboard_listener;
|
||
#[cfg(not(target_os = "android"))]
|
||
pub use crate::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;
|
||
#[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},
|
||
time::Duration,
|
||
};
|
||
#[cfg(windows)]
|
||
use tokio::runtime::Runtime;
|
||
|
||
#[cfg(target_os = "android")]
|
||
static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false);
|
||
|
||
#[cfg(not(target_os = "android"))]
|
||
struct Handler {
|
||
ctx: Option<ClipboardContext>,
|
||
#[cfg(target_os = "windows")]
|
||
stream: Option<ipc::ConnectionTmpl<parity_tokio_ipc::ConnectionClient>>,
|
||
#[cfg(target_os = "windows")]
|
||
rt: Option<Runtime>,
|
||
}
|
||
|
||
#[cfg(target_os = "android")]
|
||
pub fn is_clipboard_service_ok() -> bool {
|
||
CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst)
|
||
}
|
||
|
||
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 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,
|
||
};
|
||
|
||
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;
|
||
}
|
||
Ok(CallbackResult::StopWithError(err)) => {
|
||
bail!("Clipboard listener stopped with error: {}", err);
|
||
}
|
||
Err(RecvTimeoutError::Timeout) => {}
|
||
Err(RecvTimeoutError::Disconnected) => {
|
||
log::error!("Clipboard listener disconnected");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
clipboard_listener::unsubscribe(&sp.name());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
const WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES: usize =
|
||
super::input_service::WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS * 4;
|
||
|
||
#[cfg(target_os = "linux")]
|
||
fn decode_utf8_prefix(bytes: &[u8]) -> Option<String> {
|
||
let end = bytes.len().min(WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
|
||
let slice = &bytes[..end];
|
||
match std::str::from_utf8(slice) {
|
||
Ok(text) => Some(text.to_owned()),
|
||
Err(e) => {
|
||
if e.error_len().is_some() {
|
||
return None;
|
||
}
|
||
let valid_up_to = e.valid_up_to();
|
||
std::str::from_utf8(&slice[..valid_up_to])
|
||
.ok()
|
||
.map(ToOwned::to_owned)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
fn decode_text_clipboard(clipboard: &Clipboard) -> Option<String> {
|
||
if clipboard.format.enum_value() != Ok(ClipboardFormat::Text) {
|
||
return None;
|
||
}
|
||
if clipboard.compress {
|
||
let bytes = hbb_common::compress::decompress(&clipboard.content);
|
||
return decode_utf8_prefix(&bytes);
|
||
}
|
||
decode_utf8_prefix(&clipboard.content)
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
fn should_skip_wayland_clipboard_sync(msg: &Message) -> bool {
|
||
if crate::platform::linux::is_x11() {
|
||
return false;
|
||
}
|
||
let is_recent_wayland_input = |clipboard: &Clipboard| -> bool {
|
||
let Some(text) = decode_text_clipboard(clipboard) else {
|
||
return false;
|
||
};
|
||
super::input_service::is_recent_wayland_clipboard_input(&text)
|
||
};
|
||
|
||
match &msg.union {
|
||
Some(message::Union::Clipboard(clipboard)) => is_recent_wayland_input(clipboard),
|
||
Some(message::Union::MultiClipboards(multi_clipboards)) => multi_clipboards
|
||
.clipboards
|
||
.iter()
|
||
.any(is_recent_wayland_input),
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
#[cfg(not(target_os = "android"))]
|
||
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() {
|
||
#[cfg(target_os = "macos")]
|
||
if crate::clipboard::is_file_url_set_by_rustdesk(&urls) {
|
||
return;
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn get_clipboard_msg(&mut self) -> Option<Message> {
|
||
#[cfg(target_os = "windows")]
|
||
if crate::common::is_server() && crate::platform::is_root() {
|
||
match self.read_clipboard_from_cm_ipc() {
|
||
Err(e) => {
|
||
log::error!("Failed to read clipboard from cm: {}", e);
|
||
}
|
||
Ok(data) => {
|
||
// Skip sending empty clipboard data.
|
||
// Maybe there's something wrong reading the clipboard data in cm, but no error msg is returned.
|
||
// The clipboard data should not be empty, the last line will try again to get the clipboard data.
|
||
if !data.is_empty() {
|
||
let mut msg = Message::new();
|
||
let multi_clipboards = MultiClipboards {
|
||
clipboards: data
|
||
.into_iter()
|
||
.map(|c| Clipboard {
|
||
compress: c.compress,
|
||
content: c.content,
|
||
width: c.width,
|
||
height: c.height,
|
||
format: ClipboardFormat::from_i32(c.format)
|
||
.unwrap_or(ClipboardFormat::Text)
|
||
.into(),
|
||
special_name: c.special_name,
|
||
..Default::default()
|
||
})
|
||
.collect(),
|
||
..Default::default()
|
||
};
|
||
msg.set_multi_clipboards(multi_clipboards);
|
||
return Some(msg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(target_os = "linux")]
|
||
{
|
||
let msg = crate::clipboard::peek_clipboard(&mut self.ctx, ClipboardSide::Host, false)?;
|
||
if should_skip_wayland_clipboard_sync(&msg) {
|
||
log::debug!("Skip clipboard sync for recent Wayland keyboard injection");
|
||
return None;
|
||
}
|
||
return Some(msg);
|
||
}
|
||
#[cfg(not(target_os = "linux"))]
|
||
{
|
||
crate::clipboard::check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
|
||
}
|
||
}
|
||
|
||
// Read clipboard data from cm using ipc.
|
||
//
|
||
// We cannot use `#[tokio::main(flavor = "current_thread")]` here,
|
||
// because the auto-managed tokio runtime (async context) will be dropped after the call.
|
||
// The next call will create a new runtime, which will cause the previous stream to be unusable.
|
||
// So we need to manage the tokio runtime manually.
|
||
#[cfg(windows)]
|
||
fn read_clipboard_from_cm_ipc(&mut self) -> ResultType<Vec<ClipboardNonFile>> {
|
||
if self.rt.is_none() {
|
||
self.rt = Some(Runtime::new()?);
|
||
}
|
||
let Some(rt) = &self.rt else {
|
||
// unreachable!
|
||
bail!("failed to get tokio runtime");
|
||
};
|
||
let mut is_sent = false;
|
||
if let Some(stream) = &mut self.stream {
|
||
// If previous stream is still alive, reuse it.
|
||
// If the previous stream is dead, `is_sent` will trigger reconnect.
|
||
is_sent = match rt.block_on(stream.send(&Data::ClipboardNonFile(None))) {
|
||
Ok(_) => true,
|
||
Err(e) => {
|
||
log::debug!("Failed to send to cm: {}", e);
|
||
false
|
||
}
|
||
};
|
||
}
|
||
if !is_sent {
|
||
let mut stream = rt.block_on(crate::ipc::connect(100, "_cm"))?;
|
||
rt.block_on(stream.send(&Data::ClipboardNonFile(None)))?;
|
||
self.stream = Some(stream);
|
||
}
|
||
|
||
if let Some(stream) = &mut self.stream {
|
||
loop {
|
||
match rt.block_on(stream.next_timeout(800))? {
|
||
Some(Data::ClipboardNonFile(Some((err, mut contents)))) => {
|
||
if !err.is_empty() {
|
||
bail!("{}", err);
|
||
} else {
|
||
if contents.iter().any(|c| c.next_raw) {
|
||
// Wrap the future with a `Timeout` in an async block to avoid panic.
|
||
// We cannot use `rt.block_on(timeout(1000, stream.next_raw()))` here, because it causes panic:
|
||
// thread '<unnamed>' panicked at D:\Projects\rust\rustdesk\libs\hbb_common\src\lib.rs:98:5:
|
||
// there is no reactor running, must be called from the context of a Tokio 1.x runtime
|
||
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||
match rt.block_on(async { timeout(1000, stream.next_raw()).await })
|
||
{
|
||
Ok(Ok(mut data)) => {
|
||
for c in &mut contents {
|
||
if c.next_raw {
|
||
// No need to check the length because sum(content_len) == data.len().
|
||
c.content = data.split_to(c.content_len).into();
|
||
}
|
||
}
|
||
}
|
||
Ok(Err(e)) => {
|
||
// reset by peer
|
||
self.stream = None;
|
||
bail!("failed to get raw clipboard data: {}", e);
|
||
}
|
||
Err(e) => {
|
||
// Reconnect to avoid the next raw data remaining in the buffer.
|
||
self.stream = None;
|
||
log::debug!("Failed to get raw clipboard data: {}", e);
|
||
}
|
||
}
|
||
}
|
||
return Ok(contents);
|
||
}
|
||
}
|
||
Some(Data::ClipboardFile(ClipboardFile::MonitorReady)) => {
|
||
// ClipboardFile::MonitorReady is the first message sent by cm.
|
||
}
|
||
_ => {
|
||
bail!("failed to get clipboard data from cm");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// unreachable!
|
||
bail!("failed to get clipboard data from cm");
|
||
}
|
||
}
|
||
|
||
#[cfg(target_os = "android")]
|
||
fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
||
CLIPBOARD_SERVICE_OK.store(sp.ok(), Ordering::SeqCst);
|
||
while sp.ok() {
|
||
if let Some(msg) = crate::clipboard::get_clipboards_msg(false) {
|
||
sp.send(msg);
|
||
}
|
||
std::thread::sleep(Duration::from_millis(INTERVAL));
|
||
}
|
||
CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst);
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[cfg(target_os = "linux")]
|
||
mod tests {
|
||
use super::{decode_utf8_prefix, WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES};
|
||
|
||
#[test]
|
||
fn decode_utf8_prefix_returns_text_for_valid_utf8() {
|
||
let text = "hello-مرحبا";
|
||
assert_eq!(decode_utf8_prefix(text.as_bytes()), Some(text.to_owned()));
|
||
}
|
||
|
||
#[test]
|
||
fn decode_utf8_prefix_returns_none_for_invalid_utf8_sequence() {
|
||
let bytes = b"ab\xffcd";
|
||
assert_eq!(decode_utf8_prefix(bytes), None);
|
||
}
|
||
|
||
#[test]
|
||
fn decode_utf8_prefix_trims_incomplete_utf8_suffix() {
|
||
let bytes = vec![b'a', 0xE4, 0xB8];
|
||
assert_eq!(decode_utf8_prefix(&bytes), Some("a".to_owned()));
|
||
}
|
||
|
||
#[test]
|
||
fn decode_utf8_prefix_applies_max_bytes_limit() {
|
||
let bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES + 8];
|
||
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
|
||
assert_eq!(result.len(), WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
|
||
}
|
||
|
||
#[test]
|
||
fn decode_utf8_prefix_keeps_utf8_boundary_when_limited() {
|
||
let mut bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1];
|
||
bytes.extend_from_slice("ا".as_bytes());
|
||
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
|
||
assert_eq!(
|
||
result.len(),
|
||
WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1
|
||
);
|
||
assert!(result.chars().all(|c| c == 'a'));
|
||
}
|
||
}
|