feat: add x11 clipboard support

Signed-off-by: 蔡略 <cailue@bupt.edu.cn>
This commit is contained in:
蔡略 2023-09-08 19:39:00 +08:00
parent 4f7036a405
commit 25cf36a948
12 changed files with 1233 additions and 512 deletions

4
Cargo.lock generated
View File

@ -946,6 +946,7 @@ dependencies = [
"thiserror",
"utf16string",
"x11-clipboard",
"x11rb 0.12.0",
]
[[package]]
@ -7269,8 +7270,7 @@ dependencies = [
[[package]]
name = "x11-clipboard"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b41aca1115b1f195f21c541c5efb423470848d48143127d0f07f8b90c27440df"
source = "git+https://github.com/clslaid/x11-clipboard?branch=feat/store-batch#5fc2e73bc01ada3681159b34cf3ea8f0d14cd904"
dependencies = [
"x11rb 0.12.0",
]

View File

@ -18,6 +18,7 @@ hbb_common = { path = "../hbb_common" }
parking_lot = {version = "0.12"}
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
x11rb = {version = "0.12", features = ["all-extensions"]}
rand = {version = "0.8"}
fuser = {version = "0.13"}
libc = {version = "0.2"}
@ -25,4 +26,4 @@ rayon = {version = "1.7"}
dashmap = "5.5"
percent-encoding = "2.3"
utf16string = "0.2"
x11-clipboard = "0.8"
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch"}

View File

@ -1,39 +1,37 @@
use cc;
fn build_c_impl() {
#[cfg(not(target_os = "linux"))]
let mut build = cc::Build::new();
#[cfg(target_os = "windows")]
build.file("src/windows/wf_cliprdr.c");
#[cfg(target_os = "linux")]
build.file("src/X11/xf_cliprdr.c");
#[cfg(target_os = "macos")]
build.file("src/OSX/Clipboard.m");
build.flag_if_supported("-Wno-c++0x-extensions");
build.flag_if_supported("-Wno-return-type-c-linkage");
build.flag_if_supported("-Wno-invalid-offsetof");
build.flag_if_supported("-Wno-unused-parameter");
#[cfg(not(target_os = "linux"))]
{
build.flag_if_supported("-Wno-c++0x-extensions");
build.flag_if_supported("-Wno-return-type-c-linkage");
build.flag_if_supported("-Wno-invalid-offsetof");
build.flag_if_supported("-Wno-unused-parameter");
if build.get_compiler().is_like_msvc() {
build.define("WIN32", "");
// build.define("_AMD64_", "");
build.flag("-Z7");
build.flag("-GR-");
// build.flag("-std:c++11");
} else {
build.flag("-fPIC");
// build.flag("-std=c++11");
// build.flag("-include");
// build.flag(&confdefs_path.to_string_lossy());
if build.get_compiler().is_like_msvc() {
build.define("WIN32", "");
// build.define("_AMD64_", "");
build.flag("-Z7");
build.flag("-GR-");
// build.flag("-std:c++11");
} else {
build.flag("-fPIC");
// build.flag("-std=c++11");
// build.flag("-include");
// build.flag(&confdefs_path.to_string_lossy());
}
build.compile("mycliprdr");
}
build.compile("mycliprdr");
#[cfg(target_os = "windows")]
println!("cargo:rerun-if-changed=src/windows/wf_cliprdr.c");
#[cfg(target_os = "linux")]
println!("cargo:rerun-if-changed=src/X11/xf_cliprdr.c");
#[cfg(target_os = "macos")]
println!("cargo:rerun-if-changed=src/OSX/Clipboard.m");
}

View File

@ -1,5 +1,6 @@
#[allow(dead_code)]
use std::{
ffi::{CStr, CString},
path::PathBuf,
sync::{Arc, Mutex, RwLock},
};
@ -18,7 +19,9 @@ pub mod context_send;
pub mod platform;
pub use context_send::*;
#[cfg(target_os = "windows")]
const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001;
#[cfg(target_os = "windows")]
const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
pub(crate) use platform::create_cliprdr_context;
@ -33,7 +36,7 @@ pub trait CliprdrServiceContext: Send + Sync {
/// set to be stopped
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
/// clear the content on clipboard
fn empty_clipboard(&mut self, conn_id: i32) -> bool;
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError>;
/// run as a server for clipboard RPC
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>;
@ -51,12 +54,16 @@ pub enum CliprdrError {
ClipboardInternalError,
#[error("cliprdr occupied")]
ClipboardOccupied,
#[error("content not available")]
ContentNotAvailable,
#[error("conversion failure")]
ConversionFailure,
#[error("failure to read clipboard")]
OpenClipboard,
#[error("failure to read file metadata or content")]
FileError { path: PathBuf, err: std::io::Error },
#[error("invalid request")]
InvalidRequest { description: String },
#[error("unknown cliprdr error")]
Unknown { description: String },
Unknown(u32),
}
#[derive(Debug, Serialize, Deserialize, Clone)]

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,54 @@
use std::{
collections::HashSet,
fs::File,
os::unix::prelude::FileExt,
path::{Path, PathBuf},
time::Duration,
sync::{atomic::AtomicBool, Arc},
time::{Duration, SystemTime},
};
use crate::CliprdrError;
use dashmap::DashMap;
use fuser::MountOption;
use hbb_common::{
bytes::{BufMut, BytesMut},
log,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use utf16string::WString;
use super::fuse::{self, FuseServer};
use crate::{send_data, ClipboardFile, CliprdrError, CliprdrServiceContext};
use super::{fuse::FuseServer, LDAP_EPOCH_DELTA};
#[cfg(not(feature = "wayland"))]
pub mod x11;
trait SysClipboard {
// not actual format id, just a placeholder
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
// not actual format id, just a placeholder
const FILECONTENTS_FORMAT_ID: i32 = 49267;
const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
lazy_static! {
static ref REMOTE_FORMAT_MAP: DashMap<i32, String> = DashMap::new();
}
fn get_local_format(remote_id: i32) -> Option<String> {
REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone())
}
fn add_remote_format(local_name: &str, remote_id: i32) {
REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string());
}
trait SysClipboard: Send + Sync {
fn wait_file_list(&self) -> Result<Vec<PathBuf>, CliprdrError>;
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>;
}
fn get_sys_clipboard() -> Box<dyn SysClipboard> {
fn get_sys_clipboard() -> Result<Box<dyn SysClipboard>, CliprdrError> {
#[cfg(feature = "wayland")]
{
unimplemented!()
@ -23,7 +56,8 @@ fn get_sys_clipboard() -> Box<dyn SysClipboard> {
#[cfg(not(feature = "wayland"))]
{
pub use x11::*;
X11Clipboard::new()
let x11_clip = X11Clipboard::new()?;
Ok(Box::new(x11_clip) as Box<_>)
}
}
@ -72,18 +106,6 @@ fn parse_plain_uri_list(v: Vec<u8>) -> Result<Vec<PathBuf>, CliprdrError> {
parse_uri_list(&text)
}
// helper parse function
// convert "x-special/gnome-copied-files", "x-special/x-kde-cutselection" and "x-special/nautilus-clipboard" data to a list of valid Paths
// # Note
// - none utf8 data will lead to error
fn parse_de_uri_list(v: Vec<u8>) -> Result<Vec<PathBuf>, CliprdrError> {
let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?;
let plain_list = text
.trim_start_matches("copy\n")
.trim_start_matches("cut\n");
parse_uri_list(plain_list)
}
// helper parse function
// convert 'text/uri-list' data to a list of valid Paths
// # Note
@ -92,6 +114,9 @@ fn parse_uri_list(text: &str) -> Result<Vec<PathBuf>, CliprdrError> {
let mut list = Vec::new();
for line in text.lines() {
if !line.starts_with("file://") {
continue;
}
let decoded = parse_uri_to_path(line)?;
list.push(decoded)
}
@ -99,37 +124,590 @@ fn parse_uri_list(text: &str) -> Result<Vec<PathBuf>, CliprdrError> {
}
#[derive(Debug)]
pub struct ClipboardContext {
pub stop: bool,
pub fuse_mount_point: PathBuf,
pub fuse_server: FuseServer,
pub file_list: HashSet<PathBuf>,
pub clipboard: Clipboard,
struct LocalFile {
pub path: PathBuf,
pub handle: Option<File>,
pub bkg_session: fuser::BackgroundSession,
pub name: String,
pub size: u64,
pub last_write_time: SystemTime,
pub is_dir: bool,
pub read_only: bool,
pub hidden: bool,
pub system: bool,
pub archive: bool,
pub normal: bool,
}
impl LocalFile {
pub fn try_open(path: &PathBuf) -> Result<Self, CliprdrError> {
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
path: path.clone(),
err: e,
})?;
let size = mt.len() as u64;
let is_dir = mt.is_dir();
let read_only = mt.permissions().readonly();
let system = false;
let hidden = false;
let archive = false;
let normal = !is_dir;
let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let name = path
.display()
.to_string()
.trim_start_matches('/')
.replace('/', "\\");
let handle = if is_dir {
None
} else {
let file = std::fs::File::open(path).map_err(|e| CliprdrError::FileError {
path: path.clone(),
err: e,
})?;
let reader = file;
Some(reader)
};
Ok(Self {
name,
path: path.clone(),
handle,
size,
last_write_time,
is_dir,
read_only,
system,
hidden,
archive,
normal,
})
}
pub fn as_bin(&self) -> Vec<u8> {
let mut buf = BytesMut::with_capacity(592);
let read_only_flag = if self.read_only { 0x1 } else { 0 };
let hidden_flag = if self.hidden { 0x2 } else { 0 };
let system_flag = if self.system { 0x4 } else { 0 };
let directory_flag = if self.is_dir { 0x10 } else { 0 };
let archive_flag = if self.archive { 0x20 } else { 0 };
let normal_flag = if self.normal { 0x80 } else { 0 };
let file_attributes: u32 = read_only_flag
| hidden_flag
| system_flag
| directory_flag
| archive_flag
| normal_flag;
let win32_time = self
.last_write_time
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
/ 100
+ LDAP_EPOCH_DELTA;
let size_high = (self.size >> 32) as u32;
let size_low = (self.size & (u32::MAX as u64)) as u32;
let wstr: WString<utf16string::LE> = WString::from(&self.name);
let name = wstr.as_bytes();
let flags = 0x4064;
// flags, 4 bytes
buf.put_u32_le(flags);
// 32 bytes reserved
buf.put(&[0u8; 32][..]);
// file attributes, 4 bytes
buf.put_u32_le(file_attributes);
// 16 bytes reserved
buf.put(&[0u8; 16][..]);
// last write time, 8 bytes
buf.put_u64_le(win32_time);
// file size (high)
buf.put_u32_le(size_high);
// file size (low)
buf.put_u32_le(size_low);
// put name and padding to 520 bytes
let name_len = name.len();
buf.put(name);
buf.put(&vec![0u8; 520 - name_len][..]);
buf.to_vec()
}
}
fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
fn constr_file_lst(
path: &PathBuf,
file_list: &mut Vec<LocalFile>,
visited: &mut HashSet<PathBuf>,
) -> Result<(), CliprdrError> {
// prevent fs loop
if visited.contains(path) {
return Ok(());
}
visited.insert(path.clone());
let local_file = LocalFile::try_open(path)?;
file_list.push(local_file);
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
path: path.clone(),
err: e,
})?;
if mt.is_dir() {
let dir = std::fs::read_dir(path).unwrap();
for entry in dir {
let entry = entry.unwrap();
let path = entry.path();
constr_file_lst(&path, file_list, visited)?;
}
}
Ok(())
}
let mut file_list = Vec::new();
let mut visited = HashSet::new();
for path in paths {
constr_file_lst(path, &mut file_list, &mut visited)?;
}
Ok(file_list)
}
#[derive(Debug)]
enum FileContentsRequest {
Size {
stream_id: i32,
file_idx: usize,
},
Range {
stream_id: i32,
file_idx: usize,
offset: u64,
length: u64,
},
}
/// this is a proxy type for the clipboard context
pub struct CliprdrClient {
pub context: Arc<ClipboardContext>,
}
impl CliprdrServiceContext for CliprdrClient {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
self.context.set_is_stopped()
}
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError> {
self.context.empty_clipboard(conn_id)
}
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
self.context.serve(conn_id, msg)
}
}
pub struct ClipboardContext {
pub stop: AtomicBool,
pub fuse_mount_point: PathBuf,
fuse_server: Arc<FuseServer>,
file_list: RwLock<Vec<LocalFile>>,
clipboard: Arc<dyn SysClipboard>,
}
impl ClipboardContext {
fn new(timeout: Duration, mount_path: PathBuf) -> Result<Self, CliprdrError> {
pub fn new(timeout: Duration, mount_path: PathBuf) -> Result<Self, CliprdrError> {
// assert mount path exists
let mountpoint = mount_path
.canonicalize()
.map_err(|e| CliprdrError::Unknown {
description: format!("invalid mount point: {:?}", e),
})?;
let fuse_server = FuseServer::new(timeout);
let mnt_opts = [
fuser::MountOption::FSName("clipboard".to_string()),
fuser::MountOption::NoAtime,
fuser::MountOption::RO,
fuser::MountOption::NoExec,
];
let bkg_session = fuser::spawn_mount2(fuse_server, mountpoint, &mnt_opts).map_err(|e| {
CliprdrError::Unknown {
description: format!("failed to mount fuse: {:?}", e),
}
let fuse_mount_point = mount_path.canonicalize().map_err(|e| {
log::error!("failed to canonicalize mount path: {:?}", e);
CliprdrError::CliprdrInit
})?;
log::debug!("mounting clipboard fuse to {}", mount_path.display());
let fuse_server = Arc::new(FuseServer::new(timeout));
let clipboard = get_sys_clipboard()?;
let clipboard = Arc::from(clipboard);
let file_list = RwLock::new(vec![]);
Ok(Self {
stop: AtomicBool::new(false),
fuse_mount_point,
fuse_server,
file_list,
clipboard,
})
}
pub fn client(self: Arc<Self>) -> CliprdrClient {
CliprdrClient { context: self }
}
// mount and run fuse server, blocking
pub fn mount(&self) -> Result<(), CliprdrError> {
let mount_opts = [
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
MountOption::RO,
MountOption::NoAtime,
];
let fuse_client = self.fuse_server.client();
fuser::mount2(fuse_client, self.fuse_mount_point.clone(), &mount_opts).map_err(|e| {
log::error!("failed to mount fuse: {:?}", e);
CliprdrError::CliprdrInit
})
}
pub fn listen_clipboard(&self) -> Result<(), CliprdrError> {
while let Ok(v) = self.clipboard.wait_file_list() {
let filtered: Vec<_> = v
.into_iter()
.filter(|pb| !pb.starts_with(&self.fuse_mount_point))
.collect();
if filtered.is_empty() {
continue;
}
// construct format list update and send
let data = ClipboardFile::FormatList {
format_list: vec![
(
FILEDESCRIPTOR_FORMAT_ID,
FILEDESCRIPTORW_FORMAT_NAME.to_string(),
),
(FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string()),
],
};
send_data(0, data)
}
Ok(())
}
fn send_format_list(&self, conn_id: i32) -> Result<(), CliprdrError> {
let data = self.clipboard.wait_file_list()?;
let filtered: Vec<_> = data
.into_iter()
.filter(|pb| !pb.starts_with(&self.fuse_mount_point))
.collect();
if filtered.is_empty() {
return Ok(());
}
let format_list = ClipboardFile::FormatList {
format_list: vec![
(
FILEDESCRIPTOR_FORMAT_ID,
FILEDESCRIPTORW_FORMAT_NAME.to_string(),
),
(FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string()),
],
};
send_data(conn_id, format_list);
Ok(())
}
fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> {
let data = self.clipboard.wait_file_list()?;
let filtered: Vec<_> = data
.into_iter()
.filter(|pb| !pb.starts_with(&self.fuse_mount_point))
.collect();
let files = construct_file_list(filtered.as_slice())?;
let mut data = BytesMut::with_capacity(4 + 592 * files.len());
data.put_u32_le(filtered.len() as u32);
for file in files.iter() {
data.put(file.as_bin().as_slice());
}
{
let mut w_list = self.file_list.write();
*w_list = files;
}
let format_data = data.to_vec();
send_data(
conn_id,
ClipboardFile::FormatDataResponse {
msg_flags: 1,
format_data,
},
);
Ok(())
}
fn serve_file_contents(
&self,
conn_id: i32,
request: FileContentsRequest,
) -> Result<(), CliprdrError> {
log::debug!("file contents (range) requested from conn: {}", conn_id);
let file_contents_req = match request {
FileContentsRequest::Size {
stream_id,
file_idx,
} => {
let file_list = self.file_list.read();
let Some(file) = file_list.get(file_idx) else {
log::error!(
"invalid file index {} requested from conn: {}",
file_idx,
conn_id
);
resp_file_contents_fail(conn_id, stream_id);
return Err(CliprdrError::InvalidRequest {
description: format!(
"invalid file index {} requested from conn: {}",
file_idx, conn_id
),
});
};
log::debug!("conn {} requested file {}", conn_id, file.name);
let size = file.size;
ClipboardFile::FileContentsResponse {
msg_flags: 0x1,
stream_id,
requested_data: size.to_le_bytes().to_vec(),
}
}
FileContentsRequest::Range {
stream_id,
file_idx,
offset,
length,
} => {
let file_list = self.file_list.read();
let Some(file) = file_list.get(file_idx) else {
log::error!(
"invalid file index {} requested from conn: {}",
file_idx,
conn_id
);
resp_file_contents_fail(conn_id, stream_id);
return Err(CliprdrError::InvalidRequest {
description: format!(
"invalid file index {} requested from conn: {}",
file_idx, conn_id
),
});
};
log::debug!("conn {} requested file {}", conn_id, file.name);
let Some(handle) = &file.handle else {
log::error!(
"invalid file index {} requested from conn: {}",
file_idx,
conn_id
);
resp_file_contents_fail(conn_id, stream_id);
return Err(CliprdrError::InvalidRequest {
description: format!(
"request to read directory on index {} as file from conn: {}",
file_idx, conn_id
),
});
};
if offset > file.size {
log::error!("invalid reading offset requested from conn: {}", conn_id);
resp_file_contents_fail(conn_id, stream_id);
return Err(CliprdrError::InvalidRequest {
description: format!(
"invalid reading offset requested from conn: {}",
conn_id
),
});
}
let read_size = if offset + length > file.size {
file.size - offset
} else {
length
};
let mut buf = vec![0u8; read_size as usize];
handle
.read_exact_at(&mut buf, offset)
.map_err(|e| CliprdrError::FileError {
path: file.path.clone(),
err: e,
})?;
ClipboardFile::FileContentsResponse {
msg_flags: 0x1,
stream_id,
requested_data: buf,
}
}
};
send_data(conn_id, file_contents_req);
log::debug!("file contents sent to conn: {}", conn_id);
Ok(())
}
}
fn resp_file_contents_fail(conn_id: i32, stream_id: i32) {
let resp = ClipboardFile::FileContentsResponse {
msg_flags: 0x2,
stream_id,
requested_data: vec![],
};
send_data(conn_id, resp)
}
impl ClipboardContext {
pub fn set_is_stopped(&self) -> Result<(), CliprdrError> {
// do nothing
Ok(())
}
pub fn empty_clipboard(&self, conn_id: i32) -> Result<bool, CliprdrError> {
// gc all files, the clipboard is going to shutdown
self.fuse_server
.update_files(conn_id, FILEDESCRIPTOR_FORMAT_ID, FILECONTENTS_FORMAT_ID)
}
pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
match msg {
ClipboardFile::NotifyCallback { .. } => {
unreachable!()
}
ClipboardFile::MonitorReady => {
log::debug!("server_monitor_ready called");
// ignore capabilities for now
self.send_file_list(0)?;
Ok(())
}
ClipboardFile::FormatList { format_list } => {
// filter out "FileGroupDescriptorW" and "FileContents"
let fmt_lst: Vec<(i32, String)> = format_list
.into_iter()
.filter(|(_, name)| {
name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME
})
.collect();
if fmt_lst.len() != 2 {
log::debug!("no supported formats");
return Ok(());
}
log::debug!("supported formats: {:?}", fmt_lst);
let file_contents_id = fmt_lst
.iter()
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
.map(|(id, _)| *id)
.unwrap();
let file_descriptor_id = fmt_lst
.iter()
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
.map(|(id, _)| *id)
.unwrap();
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
self.fuse_server
.update_files(conn_id, file_descriptor_id, file_contents_id)?;
Ok(())
}
ClipboardFile::FormatListResponse { msg_flags } => {
if msg_flags != 0x1 {
self.send_format_list(conn_id)
} else {
Ok(())
}
}
ClipboardFile::FormatDataRequest {
requested_format_id,
} => {
let Some(format) = get_local_format(requested_format_id) else {
log::error!(
"got unsupported format data request: id={} from conn={}",
requested_format_id,
conn_id
);
resp_format_data_failure(conn_id);
return Ok(());
};
if format == FILEDESCRIPTORW_FORMAT_NAME {
self.send_file_list(requested_format_id)?;
} else if format == FILECONTENTS_FORMAT_NAME {
log::error!(
"try to read file contents with FormatDataRequest from conn={}",
conn_id
);
resp_format_data_failure(conn_id);
} else {
log::error!(
"got unsupported format data request: id={} from conn={}",
requested_format_id,
conn_id
);
resp_format_data_failure(conn_id);
}
Ok(())
}
ClipboardFile::FormatDataResponse { .. }
| ClipboardFile::FileContentsResponse { .. } => {
self.fuse_server.recv(conn_id, msg);
Ok(())
}
ClipboardFile::FileContentsRequest {
stream_id,
list_index,
dw_flags,
n_position_low,
n_position_high,
cb_requested,
..
} => {
let fcr = if dw_flags == 0x1 {
FileContentsRequest::Size {
stream_id,
file_idx: list_index as usize,
}
} else if dw_flags == 0x2 {
let offset = (n_position_high as u64) << 32 | n_position_low as u64;
let length = cb_requested as u64;
FileContentsRequest::Range {
stream_id,
file_idx: list_index as usize,
offset,
length,
}
} else {
log::error!("got invalid FileContentsRequest from conn={}", conn_id);
resp_file_contents_fail(conn_id, stream_id);
return Ok(());
};
self.serve_file_contents(conn_id, fcr)
}
}
}
}
fn resp_format_data_failure(conn_id: i32) {
let data = ClipboardFile::FormatDataResponse {
msg_flags: 0x2,
format_data: vec![],
};
send_data(conn_id, data)
}

View File

@ -1,7 +1,69 @@
use super::SysClipboard;
use std::path::PathBuf;
pub struct X11Clipboard {}
use x11_clipboard::Clipboard;
use x11rb::protocol::xproto::Atom;
use crate::CliprdrError;
use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard};
pub struct X11Clipboard {
text_uri_list: Atom,
gnome_copied_files: Atom,
clipboard: Clipboard,
}
impl X11Clipboard {
pub fn new() -> Result<Self, CliprdrError> {
let clipboard = Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)?;
let text_uri_list = clipboard
.setter
.get_atom("text/uri-list")
.map_err(|_| CliprdrError::CliprdrInit)?;
let gnome_copied_files = clipboard
.setter
.get_atom("x-special/gnome-copied-files")
.map_err(|_| CliprdrError::CliprdrInit)?;
Ok(Self {
text_uri_list,
gnome_copied_files,
clipboard,
})
}
fn load(&self, target: Atom) -> Result<Vec<u8>, CliprdrError> {
let clip = self.clipboard.setter.atoms.clipboard;
let prop = self.clipboard.setter.atoms.property;
self.clipboard
.load_wait(clip, target, prop)
.map_err(|_| CliprdrError::ConversionFailure)
}
fn store_batch(&self, batch: Vec<(Atom, Vec<u8>)>) -> Result<(), CliprdrError> {
let clip = self.clipboard.setter.atoms.clipboard;
self.clipboard
.store_batch(clip, batch)
.map_err(|_| CliprdrError::ClipboardInternalError)
}
}
impl SysClipboard for X11Clipboard {
todo!()
fn wait_file_list(&self) -> Result<Vec<PathBuf>, CliprdrError> {
let v = self.load(self.text_uri_list)?;
// loading 'text/uri-list' should be enough?
parse_plain_uri_list(v)
}
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
let uri_list: Vec<String> = paths.iter().map(|pb| encode_path_to_uri(pb)).collect();
let uri_list = uri_list.join("\n");
let text_uri_list_data = uri_list.as_bytes().to_vec();
let gnome_copied_files_data = vec!["copy\n".as_bytes(), uri_list.as_bytes()].concat();
let batch = vec![
(self.text_uri_list, text_uri_list_data),
(self.gnome_copied_files, gnome_copied_files_data),
];
self.store_batch(batch)
.map_err(|_| CliprdrError::ClipboardInternalError)
}
}

View File

@ -1,4 +1,4 @@
use parking_lot::{Condvar, Mutex};
use crate::{CliprdrError, CliprdrServiceContext};
#[cfg(target_os = "windows")]
pub mod windows;
@ -19,8 +19,48 @@ pub mod linux;
#[cfg(target_os = "linux")]
pub fn create_cliprdr_context(
enable_files: bool,
enable_others: bool,
_enable_others: bool,
response_wait_timeout_secs: u32,
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
unimplemented!()
use std::sync::Arc;
if !enable_files {
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
}
let timeout = std::time::Duration::from_secs(response_wait_timeout_secs as u64);
let mut tmp_path = std::env::temp_dir();
tmp_path.push("rustdesk-cliprdr");
let rd_mnt = tmp_path;
std::fs::create_dir(rd_mnt.clone())?;
let linux_ctx = Arc::new(linux::ClipboardContext::new(timeout, rd_mnt)?);
let fuse_ctx = linux_ctx.clone();
std::thread::spawn(move || fuse_ctx.mount());
let clipboard_listen_ctx = linux_ctx.clone();
std::thread::spawn(move || clipboard_listen_ctx.listen_clipboard());
Ok(Box::new(linux_ctx.client()) as Box<_>)
}
struct DummyCliprdrContext {}
impl CliprdrServiceContext for DummyCliprdrContext {
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
Ok(())
}
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
Ok(true)
}
fn server_clip_file(
&mut self,
_conn_id: i32,
_msg: crate::ClipboardFile,
) -> Result<(), crate::CliprdrError> {
Ok(())
}
}
// begin of epoch used by microsoft
// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00
const LDAP_EPOCH_DELTA: u64 = 116444772610000000;

View File

@ -5,7 +5,7 @@ use std::sync::{
Arc,
};
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
use clipboard::ContextSend;
use crossbeam_queue::ArrayQueue;
use hbb_common::config::{PeerConfig, TransferSerde};
@ -20,7 +20,7 @@ use hbb_common::rendezvous_proto::ConnType;
use hbb_common::sleep;
#[cfg(not(target_os = "ios"))]
use hbb_common::tokio::sync::mpsc::error::TryRecvError;
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
use hbb_common::tokio::sync::Mutex as TokioMutex;
use hbb_common::tokio::{
self,
@ -57,7 +57,7 @@ pub struct Remote<T: InvokeUiSession> {
timer: Interval,
last_update_jobs_status: (Instant, HashMap<i32, u64>),
first_frame: bool,
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
client_conn_id: i32, // used for file clipboard
data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>,
@ -91,7 +91,7 @@ impl<T: InvokeUiSession> Remote<T> {
timer: time::interval(SEC30),
last_update_jobs_status: (Instant::now(), Default::default()),
first_frame: false,
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
client_conn_id: 0,
data_count: Arc::new(AtomicUsize::new(0)),
frame_count,
@ -131,14 +131,14 @@ impl<T: InvokeUiSession> Remote<T> {
}
// just build for now
#[cfg(not(windows))]
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>();
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
let (_tx_holder, rx) = mpsc::unbounded_channel();
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx));
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let is_conn_not_default = self.handler.is_file_transfer()
|| self.handler.is_port_forward()
@ -148,7 +148,7 @@ impl<T: InvokeUiSession> Remote<T> {
clipboard::get_rx_cliprdr_client(&self.handler.session_id);
};
}
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
let mut rx_clip_client = rx_clip_client_lock.lock().await;
let mut status_timer = time::interval(Duration::new(1, 0));
@ -204,7 +204,7 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
_msg = rx_clip_client.recv() => {
#[cfg(windows)]
#[cfg(any(target_os="windows", target_os="linux"))]
match _msg {
Some(clip) => match clip {
clipboard::ClipboardFile::NotifyCallback{r#type, title, text} => {
@ -278,11 +278,11 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Client::try_stop_clipboard(&self.handler.session_id);
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let conn_id = self.client_conn_id;
let _ = ContextSend::proc(|context| -> ResultType<()> {
context.empty_clipboard(conn_id);
context.empty_clipboard(conn_id)?;
Ok(())
});
}
@ -1031,7 +1031,7 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
}
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
Some(message::Union::Cliprdr(clip)) => {
self.handle_cliprdr_msg(clip);
}
@ -1551,7 +1551,7 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(not(feature = "flutter"))]
fn check_clipboard_file_context(&self) {
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let enabled = *self.handler.server_file_transfer_enabled.read().unwrap()
&& self.handler.lc.read().unwrap().enable_file_transfer.v;
@ -1559,7 +1559,7 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) {
#[cfg(feature = "flutter")]
if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union {

View File

@ -59,7 +59,7 @@ mod ui_session_interface;
mod hbbs_http;
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
pub mod clipboard_file;
#[cfg(windows)]

View File

@ -63,8 +63,8 @@ pub fn make_tray() -> hbb_common::ResultType<()> {
let open_func = move || {
#[cfg(not(feature = "flutter"))]
{
crate::run_me::<&str>(vec![]).ok();
return;
crate::run_me::<&str>(vec![]).ok();
return;
}
#[cfg(target_os = "macos")]
crate::platform::macos::handle_application_should_open_untitled_file();

View File

@ -1,6 +1,6 @@
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
use std::iter::FromIterator;
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
use std::sync::Arc;
use std::{
collections::HashMap,
@ -15,11 +15,11 @@ use std::{
use crate::ipc::Connection;
#[cfg(not(any(target_os = "ios")))]
use crate::ipc::{self, Data};
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
use clipboard::ContextSend;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::tokio::sync::mpsc::unbounded_channel;
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
use hbb_common::tokio::sync::Mutex as TokioMutex;
use hbb_common::{
allow_err,
@ -71,9 +71,9 @@ struct IpcTaskRunner<T: InvokeUiCM> {
running: bool,
authorized: bool,
conn_id: i32,
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
file_transfer_enabled: bool,
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
file_transfer_enabled_peer: bool,
}
@ -174,10 +174,10 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
.map(|c| c.disconnected = true);
}
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let _ = ContextSend::proc(|context| -> ResultType<()> {
context.empty_clipboard(id);
context.empty_clipboard(id)?;
Ok(())
});
}
@ -318,11 +318,11 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
// for tmp use, without real conn id
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
let rx_clip1;
let mut rx_clip;
let _tx_clip;
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
if self.conn_id > 0 && self.authorized {
rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id);
rx_clip = rx_clip1.lock().await;
@ -332,12 +332,12 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
rx_clip1 = Arc::new(TokioMutex::new(rx_clip2));
rx_clip = rx_clip1.lock().await;
}
#[cfg(not(windows))]
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
{
(_tx_clip, rx_clip) = unbounded_channel::<i32>();
}
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
if ContextSend::is_enabled() {
allow_err!(
@ -402,7 +402,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::ClipboardFile(_clip) => {
#[cfg(windows)]
#[cfg(any(windows, linux))]
{
let is_stopping_allowed = _clip.is_stopping_allowed_from_peer();
let is_clipboard_enabled = ContextSend::is_enabled();
@ -423,7 +423,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
}
}
Data::ClipboardFileEnabled(_enabled) => {
#[cfg(windows)]
#[cfg(any(target_os= "windows",target_os ="linux"))]
{
self.file_transfer_enabled_peer = _enabled;
}
@ -468,7 +468,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
}
clip_file = rx_clip.recv() => match clip_file {
Some(_clip) => {
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os ="linux"))]
{
let is_stopping_allowed = _clip.is_stopping_allowed();
let is_clipboard_enabled = ContextSend::is_enabled();
@ -505,9 +505,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
running: true,
authorized: false,
conn_id: 0,
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
file_transfer_enabled: false,
#[cfg(windows)]
#[cfg(any(target_os = "windows", target_os = "linux"))]
file_transfer_enabled_peer: false,
};