From 15404ecab4b5cca0d02f5d8e357123e5bb3d26d6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:35:39 +0800 Subject: [PATCH] fix: clipboard, windows, controlled side, formats (#8885) * fix: clipboard, windows, controlled side, formats Signed-off-by: fufesou * Clipboard, reuse ipc conn and send_raw() Signed-off-by: fufesou * Clipboard, merge content buffer Signed-off-by: fufesou * refact: clipboard service, ipc stream Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/clipboard.rs | 29 +++++++- src/ipc.rs | 36 +++++++--- src/server/clipboard_service.rs | 116 +++++++++++++++++++++++++++++++- src/ui_cm_interface.rs | 62 +++++++++++++---- 4 files changed, 216 insertions(+), 27 deletions(-) diff --git a/src/clipboard.rs b/src/clipboard.rs index 44a3c09d3..0510eca6a 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -16,10 +16,11 @@ lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); // cache the clipboard msg static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); - // Clipboard on Linux is "server--clients" mode. + // 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_UPDATE_CTX: Arc>> = Arc::new(Mutex::new(None)); + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); } const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ @@ -159,12 +160,34 @@ pub fn check_clipboard( None } +#[cfg(target_os = "windows")] +pub fn check_clipboard_cm() -> ResultType { + 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"); + } +} + fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { return; } - let mut ctx = CLIPBOARD_UPDATE_CTX.lock().unwrap(); + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if ctx.is_none() { match ClipboardContext::new() { Ok(x) => { diff --git a/src/ipc.rs b/src/ipc.rs index 489db38e7..fba0000fb 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,10 +1,3 @@ -use std::{ - collections::HashMap, - sync::atomic::{AtomicBool, Ordering}, -}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; - use crate::{ privacy_mode::PrivacyModeState, ui_interface::{get_local_option, set_local_option}, @@ -14,6 +7,12 @@ use parity_tokio_ipc::{ Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, }; use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -26,8 +25,11 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, tokio, - tokio::io::{AsyncRead, AsyncWrite}, + log, password_security as password, timeout, + tokio::{ + self, + io::{AsyncRead, AsyncWrite}, + }, tokio_util::codec::Framed, ResultType, }; @@ -100,6 +102,20 @@ pub enum FS { }, } +#[cfg(target_os = "windows")] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t")] +pub struct ClipboardNonFile { + pub compress: bool, + pub content: bytes::Bytes, + pub content_len: usize, + pub next_raw: bool, + pub width: i32, + pub height: i32, + // message.proto: ClipboardFormat + pub format: i32, +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] @@ -207,6 +223,8 @@ pub enum Data { #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), + #[cfg(target_os = "windows")] + ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index c0d081eef..d07fc74b4 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -3,6 +3,8 @@ pub use crate::clipboard::{ check_clipboard, ClipboardContext, ClipboardSide, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, }; +#[cfg(windows)] +use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; use clipboard_master::{CallbackResult, ClipboardHandler}; use std::{ io, @@ -14,6 +16,8 @@ struct Handler { sp: EmptyExtraFieldService, ctx: Option, tx_cb_result: Sender, + #[cfg(target_os = "windows")] + stream: Option>, } pub fn new() -> GenericService { @@ -28,6 +32,8 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { sp: sp.clone(), ctx: Some(ClipboardContext::new()?), tx_cb_result, + #[cfg(target_os = "windows")] + stream: None, }; let (tx_start_res, rx_start_res) = channel(); @@ -64,8 +70,10 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { impl ClipboardHandler for Handler { fn on_clipboard_change(&mut self) -> CallbackResult { self.sp.snapshot(|_sps| Ok(())).ok(); - if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Host, false) { - self.sp.send(msg); + if self.sp.ok() { + if let Some(msg) = self.get_clipboard_msg() { + self.sp.send(msg); + } } CallbackResult::Next } @@ -77,3 +85,107 @@ impl ClipboardHandler for Handler { CallbackResult::Next } } + +impl Handler { + fn get_clipboard_msg(&mut self) -> Option { + #[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) => { + 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(), + ..Default::default() + }) + .collect(), + ..Default::default() + }; + msg.set_multi_clipboards(multi_clipboards); + return Some(msg); + } + } + } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) + } + + // It's ok to do async operation in the clipboard service because: + // 1. the clipboard is not used frequently. + // 2. the clipboard handle is sync and will not block the main thread. + #[cfg(windows)] + #[tokio::main(flavor = "current_thread")] + async fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + 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 = stream.send(&Data::ClipboardNonFile(None)).await.is_ok(); + } + if !is_sent { + let mut stream = crate::ipc::connect(100, "_cm").await?; + stream.send(&Data::ClipboardNonFile(None)).await?; + self.stream = Some(stream); + } + + if let Some(stream) = &mut self.stream { + loop { + match stream.next_timeout(800).await? { + Some(Data::ClipboardNonFile(Some((err, mut contents)))) => { + if !err.is_empty() { + bail!("{}", err); + } else { + if contents.iter().any(|c| c.next_raw) { + match timeout(1000, stream.next_raw()).await { + Ok(Ok(mut data)) => { + for c in &mut contents { + if c.next_raw { + if c.content_len <= data.len() { + c.content = + data.split_off(c.content_len).into(); + } else { + // Reconnect the next time to avoid the next raw data mismatch. + self.stream = None; + bail!("failed to get raw clipboard data: invalid size"); + } + } + } + } + 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"); + } +} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 7dae13ebc..8374e65a1 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -1,16 +1,5 @@ -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::iter::FromIterator; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -use std::sync::Arc; -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicI64, Ordering}, - RwLock, - }, -}; - +#[cfg(target_os = "windows")] +use crate::ipc::ClipboardNonFile; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] @@ -36,6 +25,18 @@ use hbb_common::{ #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use serde_derive::Serialize; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +use std::iter::FromIterator; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use std::sync::Arc; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, +}; #[derive(Serialize, Clone)] pub struct Client { @@ -486,6 +487,41 @@ impl IpcTaskRunner { Data::CloseVoiceCall(reason) => { self.cm.voice_call_closed(self.conn_id, reason.as_str()); } + #[cfg(target_os = "windows")] + Data::ClipboardNonFile(_) => { + match crate::clipboard::check_clipboard_cm() { + Ok(multi_clipoards) => { + let mut raw_contents = bytes::BytesMut::new(); + let mut main_data = vec![]; + for c in multi_clipoards.clipboards.into_iter() { + let (content, content_len, next_raw) = { + // TODO: find out a better threshold + let content_len = c.content.len(); + if content_len > 1024 * 3 { + (c.content, content_len, false) + } else { + raw_contents.extend(c.content); + (bytes::Bytes::new(), content_len, true) + } + }; + main_data.push(ClipboardNonFile { + compress: c.compress, + content, + content_len, + next_raw, + width: c.width, + height: c.height, + format: c.format.value(), + }); + } + allow_err!(self.stream.send(&Data::ClipboardNonFile(Some(("".to_owned(), main_data)))).await); + allow_err!(self.stream.send_raw(raw_contents.into()).await); + } + Err(e) => { + allow_err!(self.stream.send(&Data::ClipboardNonFile(Some((format!("{}", e), vec![])))).await); + } + } + } _ => { }