refact: file copy&paste, cross platform (no macOS) (#10671)
* feat: unix, file copy&paste Signed-off-by: fufesou <linlong1266@gmail.com> * refact: unix file c&p, check peer version Signed-off-by: fufesou <linlong1266@gmail.com> * Update pubspec.yaml --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
12
.github/workflows/flutter-build.yml
vendored
12
.github/workflows/flutter-build.yml
vendored
@@ -33,7 +33,7 @@ env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2025.01.13
|
||||
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
|
||||
VERSION: "1.3.7"
|
||||
VERSION: "1.3.8"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -415,7 +415,7 @@ jobs:
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
./build.py --flutter --hwcodec
|
||||
./build.py --flutter --hwcodec --unix-file-copy-paste
|
||||
|
||||
- name: create unsigned dmg
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -796,7 +796,7 @@ jobs:
|
||||
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
|
||||
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
|
||||
fi
|
||||
./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }}
|
||||
./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }}
|
||||
|
||||
- name: create unsigned dmg
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -1554,7 +1554,7 @@ jobs:
|
||||
export JOBS=""
|
||||
fi
|
||||
echo $JOBS
|
||||
cargo build --lib $JOBS --features hwcodec,flutter --release
|
||||
cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
|
||||
rm -rf target/release/deps target/release/build
|
||||
rm -rf ~/.cargo
|
||||
|
||||
@@ -1706,7 +1706,7 @@ jobs:
|
||||
deb_arch: amd64,
|
||||
sciter_arch: x64,
|
||||
vcpkg-triplet: x64-linux,
|
||||
extra_features: ",hwcodec",
|
||||
extra_features: ",hwcodec,unix-file-copy-paste",
|
||||
}
|
||||
- {
|
||||
arch: armv7,
|
||||
@@ -1716,7 +1716,7 @@ jobs:
|
||||
deb_arch: armhf,
|
||||
sciter_arch: arm32,
|
||||
vcpkg-triplet: arm-linux,
|
||||
extra_features: "",
|
||||
extra_features: ",unix-file-copy-paste",
|
||||
}
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.11.16
|
||||
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||
VERSION: "1.3.7"
|
||||
VERSION: "1.3.8"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.4.0"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#4e16bad260ea05dd7dcdb68cc7549dad3920b940"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics 0.23.2",
|
||||
@@ -234,6 +234,7 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde 1.0.203",
|
||||
"serde_derive",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -1707,7 +1708,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2231,17 +2232,17 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "fuser"
|
||||
version = "0.13.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b"
|
||||
checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix 0.29.0",
|
||||
"page_size",
|
||||
"pkg-config",
|
||||
"smallvec",
|
||||
"zerocopy 0.6.6",
|
||||
"zerocopy 0.8.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4175,7 +4176,7 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate 2.0.2",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
"syn 2.0.68",
|
||||
@@ -4508,9 +4509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
@@ -5506,7 +5507,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.3.7"
|
||||
version = "1.3.8"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5606,7 +5607,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.7"
|
||||
version = "1.3.8"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -8074,16 +8075,6 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.34"
|
||||
@@ -8094,10 +8085,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.6.6"
|
||||
name = "zerocopy"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
|
||||
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
@@ -8106,9 +8106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.34"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.7"
|
||||
version = "1.3.8"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.7
|
||||
version: 1.3.8
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.7
|
||||
version: 1.3.8
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.3.7+56
|
||||
version: 1.3.8+57
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -34,7 +34,6 @@ parking_lot = {version = "0.12"}
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
rand = {version = "0.8", optional = true}
|
||||
fuser = {version = "0.13", optional = true}
|
||||
libc = {version = "0.2", optional = true}
|
||||
dashmap = {version ="5.5", optional = true}
|
||||
utf16string = {version = "0.2", optional = true}
|
||||
@@ -44,6 +43,7 @@ once_cell = {version = "1.18", optional = true}
|
||||
percent-encoding = {version ="2.3", optional = true}
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
fuser = {version = "0.15", default-features = false, optional = true}
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
#[allow(dead_code)]
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
use hbb_common::{allow_err, bail};
|
||||
#[cfg(target_os = "windows")]
|
||||
use hbb_common::ResultType;
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
use hbb_common::{allow_err, log};
|
||||
use hbb_common::{
|
||||
lazy_static,
|
||||
tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex as TokioMutex,
|
||||
},
|
||||
ResultType,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod context_send;
|
||||
pub mod platform;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use context_send::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -28,8 +27,10 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use platform::create_cliprdr_context;
|
||||
|
||||
// to-do: This trait may be removed, because unix file copy paste does not need it.
|
||||
/// Ability to handle Clipboard File from remote rustdesk client
|
||||
///
|
||||
/// # Note
|
||||
@@ -41,7 +42,6 @@ pub trait CliprdrServiceContext: Send + Sync {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
|
||||
/// clear the content on clipboard
|
||||
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>;
|
||||
}
|
||||
@@ -63,9 +63,11 @@ pub enum CliprdrError {
|
||||
#[error("failure to read clipboard")]
|
||||
OpenClipboard,
|
||||
#[error("failure to read file metadata or content")]
|
||||
FileError { path: PathBuf, err: std::io::Error },
|
||||
FileError { path: String, err: std::io::Error },
|
||||
#[error("invalid request")]
|
||||
InvalidRequest { description: String },
|
||||
#[error("common request")]
|
||||
CommonError { description: String },
|
||||
#[error("unknown cliprdr error")]
|
||||
Unknown(u32),
|
||||
}
|
||||
@@ -199,37 +201,53 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
pub fn remove_channel_by_conn_id(conn_id: i32) {
|
||||
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
|
||||
if let Some(index) = lock.iter().position(|x| x.conn_id == conn_id) {
|
||||
lock.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
#[inline]
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return send_data_to_channel(conn_id, data);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if conn_id == 0 {
|
||||
send_data_to_all(data);
|
||||
let _ = send_data_to_all(data);
|
||||
Ok(())
|
||||
} else {
|
||||
send_data_to_channel(conn_id, data);
|
||||
send_data_to_channel(conn_id, data)
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
|
||||
#[inline]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.conn_id == conn_id)
|
||||
{
|
||||
msg_channel.sender.send(data)?;
|
||||
Ok(())
|
||||
msg_channel
|
||||
.sender
|
||||
.send(data)
|
||||
.map_err(|e| CliprdrError::CommonError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
} else {
|
||||
bail!("conn_id not found");
|
||||
Err(CliprdrError::InvalidRequest {
|
||||
description: "conn_id not found".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) {
|
||||
use hbb_common::log;
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
if msg_channel.conn_id != conn_id {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
@@ -237,14 +255,13 @@ pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[inline]
|
||||
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn send_data_to_all(data: ClipboardFile) {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use crate::{CliprdrError, CliprdrServiceContext};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -16,76 +13,4 @@ pub fn create_cliprdr_context(
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
/// use FUSE for file pasting on these platforms
|
||||
pub mod fuse;
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub mod unix;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn create_cliprdr_context(
|
||||
_enable_files: bool,
|
||||
_enable_others: bool,
|
||||
_response_wait_timeout_secs: u32,
|
||||
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
{
|
||||
use std::{fs::Permissions, os::unix::prelude::PermissionsExt};
|
||||
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
|
||||
if !_enable_files {
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64);
|
||||
|
||||
let app_name = APP_NAME.read().unwrap().clone();
|
||||
|
||||
let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr");
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
std::fs::create_dir(&mnt_path).ok();
|
||||
std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
log::info!("clear previously mounted cliprdr FUSE");
|
||||
if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() {
|
||||
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
|
||||
}
|
||||
|
||||
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?;
|
||||
log::debug!("start cliprdr FUSE");
|
||||
unix_ctx.run()?;
|
||||
|
||||
Ok(Box::new(unix_ctx) as Box<_>)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unix-file-copy-paste"))]
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
struct DummyCliprdrContext {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
// 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;
|
||||
|
||||
188
libs/clipboard/src/platform/unix/filetype.rs
Normal file
188
libs/clipboard/src/platform/unix/filetype.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA};
|
||||
use crate::CliprdrError;
|
||||
use hbb_common::{
|
||||
bytes::{Buf, Bytes},
|
||||
log,
|
||||
};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use utf16string::WStr;
|
||||
|
||||
pub type Inode = u64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
// todo: support symlink
|
||||
Symlink,
|
||||
}
|
||||
|
||||
/// read only permission
|
||||
pub const PERM_READ: u16 = 0o444;
|
||||
/// read and write permission
|
||||
pub const PERM_RW: u16 = 0o644;
|
||||
/// only self can read and readonly
|
||||
pub const PERM_SELF_RO: u16 = 0o400;
|
||||
/// rwx
|
||||
pub const PERM_RWX: u16 = 0o755;
|
||||
/// max length of file name
|
||||
pub const MAX_NAME_LEN: usize = 255;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FileDescription {
|
||||
pub conn_id: i32,
|
||||
pub name: PathBuf,
|
||||
pub kind: FileType,
|
||||
pub atime: SystemTime,
|
||||
pub last_modified: SystemTime,
|
||||
pub last_metadata_changed: SystemTime,
|
||||
pub creation_time: SystemTime,
|
||||
|
||||
pub size: u64,
|
||||
|
||||
pub perm: u16,
|
||||
}
|
||||
|
||||
impl FileDescription {
|
||||
fn parse_file_descriptor(
|
||||
bytes: &mut Bytes,
|
||||
conn_id: i32,
|
||||
) -> Result<FileDescription, CliprdrError> {
|
||||
let flags = bytes.get_u32_le();
|
||||
// skip reserved 32 bytes
|
||||
bytes.advance(32);
|
||||
let attributes = bytes.get_u32_le();
|
||||
|
||||
// in original specification, this is 16 bytes reserved
|
||||
// we use the last 4 bytes to store the file mode
|
||||
// skip reserved 12 bytes
|
||||
bytes.advance(12);
|
||||
let perm = bytes.get_u32_le() as u16;
|
||||
|
||||
// last write time from 1601-01-01 00:00:00, in 100ns
|
||||
let last_write_time = bytes.get_u64_le();
|
||||
// file size
|
||||
let file_size_high = bytes.get_u32_le();
|
||||
let file_size_low = bytes.get_u32_le();
|
||||
// utf16 file name, double \0 terminated, in 520 bytes block
|
||||
// read with another pointer, and advance the main pointer
|
||||
let block = bytes.clone();
|
||||
bytes.advance(520);
|
||||
|
||||
let block = &block[..520];
|
||||
let wstr = WStr::from_utf16le(block).map_err(|e| {
|
||||
log::error!("cannot convert file descriptor path: {:?}", e);
|
||||
CliprdrError::ConversionFailure
|
||||
})?;
|
||||
|
||||
let from_unix = flags & FLAGS_FD_UNIX_MODE != 0;
|
||||
|
||||
let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0;
|
||||
if !valid_attributes {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file description must have valid attributes".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// todo: check normal, hidden, system, readonly, archive...
|
||||
let directory = attributes & 0x10 != 0;
|
||||
let normal = attributes == 0x80;
|
||||
let hidden = attributes & 0x02 != 0;
|
||||
let readonly = attributes & 0x01 != 0;
|
||||
|
||||
let perm = if from_unix {
|
||||
// as is
|
||||
perm
|
||||
// cannot set as is...
|
||||
} else if normal {
|
||||
PERM_RWX
|
||||
} else if readonly {
|
||||
PERM_READ
|
||||
} else if hidden {
|
||||
PERM_SELF_RO
|
||||
} else if directory {
|
||||
PERM_RWX
|
||||
} else {
|
||||
PERM_RW
|
||||
};
|
||||
|
||||
let kind = if directory {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
|
||||
// to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;`
|
||||
// We use `true` to for compatibility with Windows.
|
||||
// let valid_size = flags & FLAGS_FD_SIZE != 0;
|
||||
let valid_size = true;
|
||||
let size = if valid_size {
|
||||
((file_size_high as u64) << 32) + file_size_low as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0;
|
||||
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
|
||||
let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100;
|
||||
let last_write_time = Duration::from_nanos(last_write_time);
|
||||
SystemTime::UNIX_EPOCH + last_write_time
|
||||
} else {
|
||||
SystemTime::UNIX_EPOCH
|
||||
};
|
||||
|
||||
let name = wstr.to_utf8().replace('\\', "/");
|
||||
let name = PathBuf::from(name.trim_end_matches('\0'));
|
||||
|
||||
let desc = FileDescription {
|
||||
conn_id,
|
||||
name,
|
||||
kind,
|
||||
atime: last_modified,
|
||||
last_modified,
|
||||
last_metadata_changed: last_modified,
|
||||
|
||||
creation_time: last_modified,
|
||||
size,
|
||||
perm,
|
||||
};
|
||||
|
||||
Ok(desc)
|
||||
}
|
||||
|
||||
/// parse file descriptions from a format data response PDU
|
||||
/// which containing a CSPTR_FILEDESCRIPTORW indicated format data
|
||||
pub fn parse_file_descriptors(
|
||||
file_descriptor_pdu: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<Self>, CliprdrError> {
|
||||
let mut data = Bytes::from(file_descriptor_pdu);
|
||||
if data.remaining() < 4 {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with infficient length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let count = data.get_u32_le() as usize;
|
||||
if data.remaining() == 0 && count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if data.remaining() != 592 * count {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let desc = Self::parse_file_descriptor(&mut data, conn_id)?;
|
||||
files.push(desc);
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
@@ -31,33 +31,29 @@ use std::{
|
||||
};
|
||||
|
||||
use fuser::{ReplyDirectory, FUSE_ROOT_ID};
|
||||
use hbb_common::{
|
||||
bytes::{Buf, Bytes},
|
||||
log,
|
||||
};
|
||||
use hbb_common::log;
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
use utf16string::WStr;
|
||||
|
||||
use crate::{send_data, ClipboardFile, CliprdrError};
|
||||
|
||||
use super::LDAP_EPOCH_DELTA;
|
||||
use crate::{
|
||||
platform::unix::{
|
||||
filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX},
|
||||
BLOCK_SIZE,
|
||||
},
|
||||
send_data, ClipboardFile, CliprdrError,
|
||||
};
|
||||
|
||||
/// fuse server ready retry max times
|
||||
const READ_RETRY: i32 = 3;
|
||||
|
||||
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
|
||||
pub const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
|
||||
|
||||
/// read only permission
|
||||
const PERM_READ: u16 = 0o444;
|
||||
/// read and write permission
|
||||
const PERM_RW: u16 = 0o644;
|
||||
/// only self can read and readonly
|
||||
const PERM_SELF_RO: u16 = 0o400;
|
||||
/// rwx
|
||||
const PERM_RWX: u16 = 0o755;
|
||||
/// max length of file name
|
||||
const MAX_NAME_LEN: usize = 255;
|
||||
impl From<FileType> for fuser::FileType {
|
||||
fn from(value: FileType) -> Self {
|
||||
match value {
|
||||
FileType::File => Self::RegularFile,
|
||||
FileType::Directory => Self::Directory,
|
||||
FileType::Symlink => Self::Symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// fuse client
|
||||
/// this is a proxy to the fuse server
|
||||
@@ -150,9 +146,15 @@ impl fuser::Filesystem for FuseClient {
|
||||
server.release(req, ino, fh, _flags, _lock_owner, _flush, reply)
|
||||
}
|
||||
|
||||
fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
|
||||
fn getattr(
|
||||
&mut self,
|
||||
req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: Option<u64>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
let mut server = self.server.lock();
|
||||
server.getattr(req, ino, reply)
|
||||
server.getattr(req, ino, fh, reply)
|
||||
}
|
||||
|
||||
fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) {
|
||||
@@ -247,7 +249,6 @@ impl fuser::Filesystem for FuseServer {
|
||||
|
||||
if parent_entry.attributes.kind != FileType::Directory {
|
||||
log::error!("fuse: parent is not a directory");
|
||||
|
||||
reply.error(libc::ENOTDIR);
|
||||
return;
|
||||
}
|
||||
@@ -480,7 +481,13 @@ impl fuser::Filesystem for FuseServer {
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
|
||||
fn getattr(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
_fh: Option<u64>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
let files = &self.files;
|
||||
let Some(entry) = files.get(ino as usize - 1) else {
|
||||
reply.error(libc::ENOENT);
|
||||
@@ -527,14 +534,6 @@ impl FuseServer {
|
||||
size: u32,
|
||||
) -> Result<Vec<u8>, std::io::Error> {
|
||||
// todo: async and concurrent read, generate stream_id per request
|
||||
log::debug!(
|
||||
"reading {:?} offset {} size {} on stream: {}",
|
||||
node.name,
|
||||
offset,
|
||||
size,
|
||||
node.stream_id
|
||||
);
|
||||
|
||||
let cb_requested = unsafe {
|
||||
// convert `size` from u32 to i32
|
||||
// yet with same bit representation
|
||||
@@ -554,16 +553,14 @@ impl FuseServer {
|
||||
clip_data_id: 0,
|
||||
};
|
||||
|
||||
send_data(node.conn_id, request.clone());
|
||||
|
||||
log::debug!(
|
||||
"waiting for read reply for {:?} on stream: {}",
|
||||
node.name,
|
||||
node.stream_id
|
||||
);
|
||||
send_data(node.conn_id, request.clone()).map_err(|e| {
|
||||
log::error!("failed to send file list to channel: {:?}", e);
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e)
|
||||
})?;
|
||||
|
||||
let mut retry_times = 0;
|
||||
|
||||
// to-do: more tests needed
|
||||
loop {
|
||||
let reply = self.rx.recv_timeout(self.timeout).map_err(|e| {
|
||||
log::error!("failed to receive file list from channel: {:?}", e);
|
||||
@@ -590,7 +587,10 @@ impl FuseServer {
|
||||
));
|
||||
}
|
||||
|
||||
send_data(node.conn_id, request.clone());
|
||||
send_data(node.conn_id, request.clone()).map_err(|e| {
|
||||
log::error!("failed to send file list to channel: {:?}", e);
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e)
|
||||
})?;
|
||||
continue;
|
||||
}
|
||||
return Ok(requested_data);
|
||||
@@ -605,160 +605,6 @@ impl FuseServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FileDescription {
|
||||
pub conn_id: i32,
|
||||
pub name: PathBuf,
|
||||
pub kind: FileType,
|
||||
pub atime: SystemTime,
|
||||
pub last_modified: SystemTime,
|
||||
pub last_metadata_changed: SystemTime,
|
||||
pub creation_time: SystemTime,
|
||||
|
||||
pub size: u64,
|
||||
|
||||
pub perm: u16,
|
||||
}
|
||||
|
||||
impl FileDescription {
|
||||
fn parse_file_descriptor(
|
||||
bytes: &mut Bytes,
|
||||
conn_id: i32,
|
||||
) -> Result<FileDescription, CliprdrError> {
|
||||
let flags = bytes.get_u32_le();
|
||||
// skip reserved 32 bytes
|
||||
bytes.advance(32);
|
||||
let attributes = bytes.get_u32_le();
|
||||
|
||||
// in original specification, this is 16 bytes reserved
|
||||
// we use the last 4 bytes to store the file mode
|
||||
// skip reserved 12 bytes
|
||||
bytes.advance(12);
|
||||
let perm = bytes.get_u32_le() as u16;
|
||||
|
||||
// last write time from 1601-01-01 00:00:00, in 100ns
|
||||
let last_write_time = bytes.get_u64_le();
|
||||
// file size
|
||||
let file_size_high = bytes.get_u32_le();
|
||||
let file_size_low = bytes.get_u32_le();
|
||||
// utf16 file name, double \0 terminated, in 520 bytes block
|
||||
// read with another pointer, and advance the main pointer
|
||||
let block = bytes.clone();
|
||||
bytes.advance(520);
|
||||
|
||||
let block = &block[..520];
|
||||
let wstr = WStr::from_utf16le(block).map_err(|e| {
|
||||
log::error!("cannot convert file descriptor path: {:?}", e);
|
||||
CliprdrError::ConversionFailure
|
||||
})?;
|
||||
|
||||
let from_unix = flags & 0x08 != 0;
|
||||
|
||||
let valid_attributes = flags & 0x04 != 0;
|
||||
if !valid_attributes {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file description must have valid attributes".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// todo: check normal, hidden, system, readonly, archive...
|
||||
let directory = attributes & 0x10 != 0;
|
||||
let normal = attributes == 0x80;
|
||||
let hidden = attributes & 0x02 != 0;
|
||||
let readonly = attributes & 0x01 != 0;
|
||||
|
||||
let perm = if from_unix {
|
||||
// as is
|
||||
perm
|
||||
// cannot set as is...
|
||||
} else if normal {
|
||||
PERM_RWX
|
||||
} else if readonly {
|
||||
PERM_READ
|
||||
} else if hidden {
|
||||
PERM_SELF_RO
|
||||
} else if directory {
|
||||
PERM_RWX
|
||||
} else {
|
||||
PERM_RW
|
||||
};
|
||||
|
||||
let kind = if directory {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
|
||||
let valid_size = flags & 0x40 != 0;
|
||||
let size = if valid_size {
|
||||
((file_size_high as u64) << 32) + file_size_low as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let valid_write_time = flags & 0x20 != 0;
|
||||
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
|
||||
let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100;
|
||||
let last_write_time = Duration::from_nanos(last_write_time);
|
||||
SystemTime::UNIX_EPOCH + last_write_time
|
||||
} else {
|
||||
SystemTime::UNIX_EPOCH
|
||||
};
|
||||
|
||||
let name = wstr.to_utf8().replace('\\', "/");
|
||||
let name = PathBuf::from(name.trim_end_matches('\0'));
|
||||
|
||||
let desc = FileDescription {
|
||||
conn_id,
|
||||
name,
|
||||
kind,
|
||||
atime: last_modified,
|
||||
last_modified,
|
||||
last_metadata_changed: last_modified,
|
||||
|
||||
creation_time: last_modified,
|
||||
size,
|
||||
perm,
|
||||
};
|
||||
|
||||
Ok(desc)
|
||||
}
|
||||
|
||||
/// parse file descriptions from a format data response PDU
|
||||
/// which containing a CSPTR_FILEDESCRIPTORW indicated format data
|
||||
pub fn parse_file_descriptors(
|
||||
file_descriptor_pdu: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<Self>, CliprdrError> {
|
||||
let mut data = Bytes::from(file_descriptor_pdu);
|
||||
if data.remaining() < 4 {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with infficient length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let count = data.get_u32_le() as usize;
|
||||
if data.remaining() == 0 && count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if data.remaining() != 592 * count {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let desc = Self::parse_file_descriptor(&mut data, conn_id)?;
|
||||
files.push(desc);
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
/// a node in the FUSE file tree
|
||||
#[derive(Debug)]
|
||||
struct FuseNode {
|
||||
@@ -881,7 +727,7 @@ impl FuseNode {
|
||||
format!("invalid file name {}", file.name.display()),
|
||||
);
|
||||
CliprdrError::FileError {
|
||||
path: file.name.clone(),
|
||||
path: file.name.to_string_lossy().to_string(),
|
||||
err,
|
||||
}
|
||||
})?;
|
||||
@@ -902,26 +748,6 @@ impl FuseNode {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Inode = u64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
// todo: support symlink
|
||||
Symlink,
|
||||
}
|
||||
|
||||
impl From<FileType> for fuser::FileType {
|
||||
fn from(value: FileType) -> Self {
|
||||
match value {
|
||||
FileType::File => Self::RegularFile,
|
||||
FileType::Directory => Self::Directory,
|
||||
FileType::Symlink => Self::Symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InodeAttributes {
|
||||
inode: Inode,
|
||||
@@ -1064,8 +890,6 @@ impl FileHandles {
|
||||
|
||||
#[cfg(test)]
|
||||
mod fuse_test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
// todo: more tests needed!
|
||||
225
libs/clipboard/src/platform/unix/fuse/mod.rs
Normal file
225
libs/clipboard/src/platform/unix/fuse/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
mod cs;
|
||||
|
||||
use super::filetype::FileDescription;
|
||||
use crate::{ClipboardFile, CliprdrError};
|
||||
use cs::FuseServer;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_CONTEXT_CLIENT: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
|
||||
static ref FUSE_CONTEXT_SERVER: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
||||
if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
} else {
|
||||
FUSE_MOUNT_POINT_SERVER.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_fuse_context_inited(is_client: bool) -> bool {
|
||||
if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock().is_some()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
let mut fuse_context_lock = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
if fuse_context_lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mount_point = if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
} else {
|
||||
FUSE_MOUNT_POINT_SERVER.clone()
|
||||
};
|
||||
|
||||
let mount_point = std::path::PathBuf::from(&*mount_point);
|
||||
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
prepare_fuse_mount_point(&mount_point);
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
MountOption::RO,
|
||||
];
|
||||
log::info!("mounting clipboard FUSE to {}", mount_point.display());
|
||||
// to-do: ignore the error if the mount point is already mounted
|
||||
// Because the sciter version uses separate processes as the controlling side.
|
||||
let session = fuser::spawn_mount2(
|
||||
FuseServer::client(server.clone()),
|
||||
mount_point.clone(),
|
||||
&mnt_opts,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to mount cliprdr fuse: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
let session = Mutex::new(Some(session));
|
||||
|
||||
let ctx = FuseContext {
|
||||
server,
|
||||
tx,
|
||||
mount_point,
|
||||
session,
|
||||
conn_id: 0,
|
||||
};
|
||||
*fuse_context_lock = Some(ctx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninit_fuse_context(is_client: bool) {
|
||||
uninit_fuse_context_(is_client)
|
||||
}
|
||||
|
||||
pub fn format_data_response_to_urls(
|
||||
is_client: bool,
|
||||
format_data: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<String>, CliprdrError> {
|
||||
let mut ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_mut()
|
||||
.ok_or(CliprdrError::CliprdrInit)?
|
||||
.format_data_response_to_urls(format_data, conn_id)
|
||||
}
|
||||
|
||||
pub fn handle_file_content_response(
|
||||
is_client: bool,
|
||||
clip: ClipboardFile,
|
||||
) -> Result<(), CliprdrError> {
|
||||
// we don't know its corresponding request, no resend can be performed
|
||||
let ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_ref()
|
||||
.ok_or(CliprdrError::CliprdrInit)?
|
||||
.tx
|
||||
.send(clip)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to send file contents response to fuse: {:?}", e);
|
||||
CliprdrError::ClipboardInternalError
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool {
|
||||
let ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_ref()
|
||||
.map(|c| c.empty_local_files(conn_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
struct FuseContext {
|
||||
server: Arc<Mutex<FuseServer>>,
|
||||
tx: Sender<ClipboardFile>,
|
||||
mount_point: PathBuf,
|
||||
// stores fuse background session handle
|
||||
session: Mutex<Option<fuser::BackgroundSession>>,
|
||||
// Indicates the connection ID of that set the clipboard content
|
||||
conn_id: i32,
|
||||
}
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
};
|
||||
|
||||
fs::create_dir(mount_point).ok();
|
||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
if let Err(e) = std::process::Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
||||
}
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
if is_client {
|
||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
||||
} else {
|
||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FuseContext {
|
||||
fn drop(&mut self) {
|
||||
self.session.lock().take().map(|s| s.join());
|
||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
||||
}
|
||||
}
|
||||
|
||||
impl FuseContext {
|
||||
pub fn empty_local_files(&self, conn_id: i32) -> bool {
|
||||
if conn_id != 0 && self.conn_id != conn_id {
|
||||
return false;
|
||||
}
|
||||
let mut fuse_guard = self.server.lock();
|
||||
let _ = fuse_guard.load_file_list(vec![]);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn format_data_response_to_urls(
|
||||
&mut self,
|
||||
format_data: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<String>, CliprdrError> {
|
||||
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
|
||||
|
||||
let paths = {
|
||||
let mut fuse_guard = self.server.lock();
|
||||
fuse_guard.load_file_list(files)?;
|
||||
self.conn_id = conn_id;
|
||||
|
||||
fuse_guard.list_root()
|
||||
};
|
||||
|
||||
let prefix = self.mount_point.clone();
|
||||
Ok(paths
|
||||
.into_iter()
|
||||
.map(|p| prefix.join(p).to_string_lossy().to_string())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA};
|
||||
use crate::{
|
||||
platform::unix::{
|
||||
FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE,
|
||||
FLAGS_FD_UNIX_MODE,
|
||||
},
|
||||
CliprdrError,
|
||||
};
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
@@ -7,32 +19,11 @@ use std::{
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use utf16string::WString;
|
||||
|
||||
use crate::{
|
||||
platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA},
|
||||
CliprdrError,
|
||||
};
|
||||
|
||||
/// has valid file attributes
|
||||
const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
|
||||
/// has valid file size
|
||||
const FLAGS_FD_SIZE: u32 = 0x40;
|
||||
/// has valid last write time
|
||||
const FLAGS_FD_LAST_WRITE: u32 = 0x20;
|
||||
/// show progress
|
||||
const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
|
||||
/// transferred from unix, contains file mode
|
||||
/// P.S. this flag is not used in windows
|
||||
const FLAGS_FD_UNIX_MODE: u32 = 0x08;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LocalFile {
|
||||
pub relative_root: PathBuf,
|
||||
pub path: PathBuf,
|
||||
|
||||
pub handle: Option<BufReader<File>>,
|
||||
@@ -51,9 +42,9 @@ pub(super) struct LocalFile {
|
||||
}
|
||||
|
||||
impl LocalFile {
|
||||
pub fn try_open(path: &Path) -> Result<Self, CliprdrError> {
|
||||
pub fn try_open(relative_root: &Path, path: &Path) -> Result<Self, CliprdrError> {
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let size = mt.len() as u64;
|
||||
@@ -79,7 +70,8 @@ impl LocalFile {
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
path: path.clone(),
|
||||
relative_root: relative_root.to_path_buf(),
|
||||
path: path.to_path_buf(),
|
||||
handle,
|
||||
offset,
|
||||
size,
|
||||
@@ -121,7 +113,12 @@ impl LocalFile {
|
||||
let size_high = (self.size >> 32) as u32;
|
||||
let size_low = (self.size & (u32::MAX as u64)) as u32;
|
||||
|
||||
let path = self.path.to_string_lossy().to_string();
|
||||
let path = self
|
||||
.path
|
||||
.strip_prefix(&self.relative_root)
|
||||
.unwrap_or(&self.path)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
let wstr: WString<utf16string::LE> = WString::from(&path);
|
||||
let name = wstr.as_bytes();
|
||||
@@ -172,12 +169,12 @@ impl LocalFile {
|
||||
pub fn load_handle(&mut self) -> Result<(), CliprdrError> {
|
||||
if !self.is_dir && self.handle.is_none() {
|
||||
let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle);
|
||||
reader.fill_buf().map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
self.handle = Some(reader);
|
||||
@@ -188,20 +185,25 @@ impl LocalFile {
|
||||
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
||||
self.load_handle()?;
|
||||
|
||||
let handle = self.handle.as_mut()?;
|
||||
let Some(handle) = self.handle.as_mut() else {
|
||||
return Err(CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"),
|
||||
});
|
||||
};
|
||||
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
}
|
||||
handle
|
||||
.read_exact(buf)
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let new_offset = offset + (buf.len() as u64);
|
||||
@@ -219,6 +221,7 @@ impl LocalFile {
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
fn constr_file_lst(
|
||||
relative_root: &Path,
|
||||
path: &Path,
|
||||
file_list: &mut Vec<LocalFile>,
|
||||
visited: &mut HashSet<PathBuf>,
|
||||
@@ -227,22 +230,28 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
if visited.contains(path) {
|
||||
return Ok(());
|
||||
}
|
||||
visited.insert(path.clone());
|
||||
visited.insert(path.to_path_buf());
|
||||
|
||||
let local_file = LocalFile::try_open(path)?;
|
||||
let local_file = LocalFile::try_open(relative_root, path)?;
|
||||
file_list.push(local_file);
|
||||
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
if mt.is_dir() {
|
||||
let dir = std::fs::read_dir(path)?;
|
||||
let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
for entry in dir {
|
||||
let entry = entry?;
|
||||
let entry = entry.map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let path = entry.path();
|
||||
constr_file_lst(&path, file_list, visited)?;
|
||||
constr_file_lst(relative_root, &path, file_list, visited)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -251,8 +260,18 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
let mut file_list = Vec::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
let relative_root = paths
|
||||
.first()
|
||||
.ok_or(CliprdrError::InvalidRequest {
|
||||
description: "empty file list".to_string(),
|
||||
})?
|
||||
.parent()
|
||||
.ok_or(CliprdrError::InvalidRequest {
|
||||
description: "empty parent".to_string(),
|
||||
})?
|
||||
.to_path_buf();
|
||||
for path in paths {
|
||||
constr_file_lst(path, &mut file_list, &mut visited)?;
|
||||
constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?;
|
||||
}
|
||||
Ok(file_list)
|
||||
}
|
||||
@@ -263,7 +282,7 @@ mod file_list_test {
|
||||
|
||||
use hbb_common::bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::{platform::fuse::FileDescription, CliprdrError};
|
||||
use crate::{platform::unix::filetype::FileDescription, CliprdrError};
|
||||
|
||||
use super::LocalFile;
|
||||
|
||||
@@ -277,6 +296,7 @@ mod file_list_test {
|
||||
#[inline]
|
||||
fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile {
|
||||
LocalFile {
|
||||
relative_root: PathBuf::from("."),
|
||||
path: PathBuf::from(path),
|
||||
handle: None,
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -1,48 +1,38 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
platform::{fuse::FileDescription, unix::local_file::construct_file_list},
|
||||
send_data, ClipboardFile, CliprdrError, CliprdrServiceContext,
|
||||
};
|
||||
|
||||
use self::local_file::LocalFile;
|
||||
mod filetype;
|
||||
/// use FUSE for file pasting on these platforms
|
||||
#[cfg(target_os = "linux")]
|
||||
use self::url::{encode_path_to_uri, parse_plain_uri_list};
|
||||
|
||||
use super::fuse::FuseServer;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// clipboard implementation of x11
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// clipboard implementation of macos
|
||||
pub mod ns_clipboard;
|
||||
|
||||
pub mod fuse;
|
||||
pub mod local_file;
|
||||
pub mod serv_files;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod url;
|
||||
/// has valid file attributes
|
||||
pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
|
||||
/// has valid file size
|
||||
pub const FLAGS_FD_SIZE: u32 = 0x40;
|
||||
/// has valid last write time
|
||||
pub const FLAGS_FD_LAST_WRITE: u32 = 0x20;
|
||||
/// show progress
|
||||
pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
|
||||
/// transferred from unix, contains file mode
|
||||
/// P.S. this flag is not used in windows
|
||||
pub const FLAGS_FD_UNIX_MODE: u32 = 0x08;
|
||||
|
||||
// not actual format id, just a placeholder
|
||||
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
||||
pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||
pub 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";
|
||||
pub const FILECONTENTS_FORMAT_ID: i32 = 49267;
|
||||
pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
|
||||
|
||||
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
|
||||
pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
|
||||
|
||||
// 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;
|
||||
|
||||
lazy_static! {
|
||||
static ref REMOTE_FORMAT_MAP: DashMap<i32, String> = DashMap::from_iter(
|
||||
@@ -58,541 +48,7 @@ lazy_static! {
|
||||
);
|
||||
}
|
||||
|
||||
fn get_local_format(remote_id: i32) -> Option<String> {
|
||||
#[inline]
|
||||
pub 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 start(&self);
|
||||
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>;
|
||||
fn get_file_list(&self) -> Vec<PathBuf>;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_sys_clipboard(ignore_path: &Path) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
{
|
||||
use x11::*;
|
||||
let x11_clip = X11Clipboard::new(ignore_path)?;
|
||||
Ok(Box::new(x11_clip) as Box<_>)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_sys_clipboard(ignore_path: &Path) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
use ns_clipboard::*;
|
||||
let ns_pb = NsPasteboard::new(ignore_path)?;
|
||||
Ok(Box::new(ns_pb) as Box<_>)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FileContentsRequest {
|
||||
Size {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
},
|
||||
|
||||
Range {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ClipboardContext {
|
||||
pub fuse_mount_point: PathBuf,
|
||||
/// stores fuse background session handle
|
||||
fuse_handle: Mutex<Option<fuser::BackgroundSession>>,
|
||||
|
||||
/// a sender of clipboard file contents pdu to fuse server
|
||||
fuse_tx: Sender<ClipboardFile>,
|
||||
fuse_server: Arc<Mutex<FuseServer>>,
|
||||
|
||||
clipboard: Arc<dyn SysClipboard>,
|
||||
local_files: Mutex<Vec<LocalFile>>,
|
||||
}
|
||||
|
||||
impl ClipboardContext {
|
||||
pub fn new(timeout: Duration, mount_path: PathBuf) -> Result<Self, CliprdrError> {
|
||||
// assert mount path exists
|
||||
let fuse_mount_point = mount_path.canonicalize().map_err(|e| {
|
||||
log::error!("failed to canonicalize mount path: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
|
||||
let (fuse_server, fuse_tx) = FuseServer::new(timeout);
|
||||
|
||||
let fuse_server = Arc::new(Mutex::new(fuse_server));
|
||||
|
||||
let clipboard = get_sys_clipboard(&fuse_mount_point)?;
|
||||
let clipboard = Arc::from(clipboard) as Arc<_>;
|
||||
let local_files = Mutex::new(vec![]);
|
||||
|
||||
Ok(Self {
|
||||
fuse_mount_point,
|
||||
fuse_server,
|
||||
fuse_tx,
|
||||
fuse_handle: Mutex::new(None),
|
||||
clipboard,
|
||||
local_files,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), CliprdrError> {
|
||||
if !self.is_stopped() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut fuse_handle = self.fuse_handle.lock();
|
||||
|
||||
let mount_path = &self.fuse_mount_point;
|
||||
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
MountOption::RO,
|
||||
];
|
||||
log::info!(
|
||||
"mounting clipboard FUSE to {}",
|
||||
self.fuse_mount_point.display()
|
||||
);
|
||||
|
||||
let new_handle = fuser::spawn_mount2(
|
||||
FuseServer::client(self.fuse_server.clone()),
|
||||
mount_path,
|
||||
&mnt_opts,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to mount cliprdr fuse: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
*fuse_handle = Some(new_handle);
|
||||
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
log::debug!("start listening clipboard");
|
||||
clipboard.start();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set clipboard data from file list
|
||||
pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
let prefix = self.fuse_mount_point.clone();
|
||||
let paths: Vec<PathBuf> = paths.iter().cloned().map(|p| prefix.join(p)).collect();
|
||||
log::debug!("setting clipboard with paths: {:?}", paths);
|
||||
self.clipboard.set_file_list(&paths)?;
|
||||
log::debug!("clipboard set, paths: {:?}", paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serve_file_contents(
|
||||
&self,
|
||||
conn_id: i32,
|
||||
request: FileContentsRequest,
|
||||
) -> Result<(), CliprdrError> {
|
||||
let mut file_list = self.local_files.lock();
|
||||
|
||||
let (file_idx, file_contents_resp) = match request {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx,
|
||||
} => {
|
||||
log::debug!("file contents (size) requested from conn: {}", conn_id);
|
||||
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_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
let size = file.size;
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: size.to_le_bytes().to_vec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents (range from {} length {}) request from conn: {}",
|
||||
offset,
|
||||
length,
|
||||
conn_id
|
||||
);
|
||||
let Some(file) = file_list.get_mut(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_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
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];
|
||||
|
||||
file.read_exact_at(&mut buf, offset)?;
|
||||
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: buf,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
send_data(conn_id, file_contents_resp);
|
||||
log::debug!("file contents sent to conn: {}", conn_id);
|
||||
// hot reload next file
|
||||
for next_file in file_list.iter_mut().skip(file_idx + 1) {
|
||||
if !next_file.is_dir {
|
||||
next_file.load_handle()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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 is_stopped(&self) -> bool {
|
||||
self.fuse_handle.lock().is_none()
|
||||
}
|
||||
|
||||
pub fn sync_local_files(&self) -> Result<(), CliprdrError> {
|
||||
let mut local_files = self.local_files.lock();
|
||||
let clipboard_files = self.clipboard.get_file_list();
|
||||
let local_file_list: Vec<PathBuf> = local_files.iter().map(|f| f.path.clone()).collect();
|
||||
if local_file_list == clipboard_files {
|
||||
return Ok(());
|
||||
}
|
||||
let new_files = construct_file_list(&clipboard_files)?;
|
||||
*local_files = new_files;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
log::debug!("serve clipboard file from conn: {}", conn_id);
|
||||
if self.is_stopped() {
|
||||
log::debug!("cliprdr stopped, restart it");
|
||||
self.run()?;
|
||||
}
|
||||
match msg {
|
||||
ClipboardFile::NotifyCallback { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
ClipboardFile::MonitorReady => {
|
||||
log::debug!("server_monitor_ready called");
|
||||
|
||||
self.send_file_list(conn_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
ClipboardFile::FormatList { format_list } => {
|
||||
log::debug!("server_format_list called");
|
||||
// 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)?;
|
||||
let file_descriptor_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)?;
|
||||
|
||||
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
|
||||
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
|
||||
|
||||
// sync file system from peer
|
||||
let data = ClipboardFile::FormatDataRequest {
|
||||
requested_format_id: file_descriptor_id,
|
||||
};
|
||||
send_data(conn_id, data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FormatListResponse { msg_flags } => {
|
||||
log::debug!("server_format_list_response called");
|
||||
if msg_flags != 0x1 {
|
||||
send_format_list(conn_id)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ClipboardFile::FormatDataRequest {
|
||||
requested_format_id,
|
||||
} => {
|
||||
log::debug!("server_format_data_request called");
|
||||
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(conn_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 {
|
||||
msg_flags,
|
||||
format_data,
|
||||
} => {
|
||||
log::debug!(
|
||||
"server_format_data_response called, msg_flags={}",
|
||||
msg_flags
|
||||
);
|
||||
|
||||
if msg_flags != 0x1 {
|
||||
resp_format_data_failure(conn_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("parsing file descriptors");
|
||||
// this must be a file descriptor format data
|
||||
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
|
||||
|
||||
let paths = {
|
||||
let mut fuse_guard = self.fuse_server.lock();
|
||||
fuse_guard.load_file_list(files)?;
|
||||
|
||||
fuse_guard.list_root()
|
||||
};
|
||||
|
||||
log::debug!("load file list: {:?}", paths);
|
||||
self.set_clipboard(&paths)?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FileContentsResponse { .. } => {
|
||||
log::debug!("server_file_contents_response called");
|
||||
// we don't know its corresponding request, no resend can be performed
|
||||
self.fuse_tx.send(msg).map_err(|e| {
|
||||
log::error!("failed to send file contents response to fuse: {:?}", e);
|
||||
CliprdrError::ClipboardInternalError
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
..
|
||||
} => {
|
||||
log::debug!("server_file_contents_request called");
|
||||
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 send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> {
|
||||
self.sync_local_files()?;
|
||||
|
||||
let file_list = self.local_files.lock();
|
||||
send_file_list(&*file_list, conn_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl CliprdrServiceContext for ClipboardContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
// unmount the fuse
|
||||
if let Some(fuse_handle) = self.fuse_handle.lock().take() {
|
||||
fuse_handle.join();
|
||||
}
|
||||
// we don't stop the clipboard, keep listening in case of restart
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
|
||||
self.clipboard.set_file_list(&[])?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
self.serve(conn_id, msg)
|
||||
}
|
||||
}
|
||||
|
||||
fn resp_format_data_failure(conn_id: i32) {
|
||||
let data = ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 0x2,
|
||||
format_data: vec![],
|
||||
};
|
||||
send_data(conn_id, data)
|
||||
}
|
||||
|
||||
fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> {
|
||||
log::debug!("send format list to remote, conn={}", conn_id);
|
||||
let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID)
|
||||
.unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string());
|
||||
let fc_format_name =
|
||||
get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string());
|
||||
let format_list = ClipboardFile::FormatList {
|
||||
format_list: vec![
|
||||
(FILEDESCRIPTOR_FORMAT_ID, fd_format_name),
|
||||
(FILECONTENTS_FORMAT_ID, fc_format_name),
|
||||
],
|
||||
};
|
||||
|
||||
send_data(conn_id, format_list);
|
||||
log::debug!("format list to remote dispatched, conn={}", conn_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_file_list_pdu(files: &[LocalFile]) -> Vec<u8> {
|
||||
let mut data = BytesMut::with_capacity(4 + 592 * files.len());
|
||||
data.put_u32_le(files.len() as u32);
|
||||
for file in files.iter() {
|
||||
data.put(file.as_bin().as_slice());
|
||||
}
|
||||
|
||||
data.to_vec()
|
||||
}
|
||||
|
||||
fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> {
|
||||
log::debug!(
|
||||
"send file list to remote, conn={}, list={:?}",
|
||||
conn_id,
|
||||
files.iter().map(|f| f.path.display()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let format_data = build_file_list_pdu(files);
|
||||
|
||||
send_data(
|
||||
conn_id,
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 1,
|
||||
format_data,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use cacao::pasteboard::{Pasteboard, PasteboardName};
|
||||
use hbb_common::log;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{platform::unix::send_format_list, CliprdrError};
|
||||
|
||||
use super::SysClipboard;
|
||||
|
||||
#[inline]
|
||||
fn wait_file_list() -> Option<Vec<PathBuf>> {
|
||||
let pb = Pasteboard::named(PasteboardName::General);
|
||||
pb.get_file_urls()
|
||||
.ok()
|
||||
.map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
let pb = Pasteboard::named(PasteboardName::General);
|
||||
pb.set_files(file_list.to_vec())
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
pub struct NsPasteboard {
|
||||
ignore_path: PathBuf,
|
||||
|
||||
former_file_list: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl NsPasteboard {
|
||||
pub fn new(ignore_path: &Path) -> Result<Self, CliprdrError> {
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SysClipboard for NsPasteboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
set_file_list(paths)
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
{
|
||||
*self.former_file_list.lock() = vec![];
|
||||
}
|
||||
|
||||
loop {
|
||||
let file_list = match wait_file_list() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let filtered = file_list
|
||||
.into_iter()
|
||||
.filter(|pb| !pb.starts_with(&self.ignore_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut former = self.former_file_list.lock();
|
||||
|
||||
let filtered_st: BTreeSet<_> = filtered.iter().collect();
|
||||
let former_st = former.iter().collect::<BTreeSet<_>>();
|
||||
if filtered_st == former_st {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
*former = filtered;
|
||||
}
|
||||
|
||||
if let Err(e) = send_format_list(0) {
|
||||
log::warn!("failed to send format list: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
log::debug!("stop listening file related atoms on clipboard");
|
||||
}
|
||||
|
||||
fn get_file_list(&self) -> Vec<PathBuf> {
|
||||
self.former_file_list.lock().clone()
|
||||
}
|
||||
}
|
||||
231
libs/clipboard/src/platform/unix/serv_files.rs
Normal file
231
libs/clipboard/src/platform/unix/serv_files.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use super::local_file::LocalFile;
|
||||
use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError};
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// local files are cached, this value should not be changed when copying files
|
||||
// Because `CliprdrFileContentsRequest` only contains the index of the file in the list.
|
||||
// We need to keep the file list in the same order as the remote side.
|
||||
// We may add a `FileId` field to `CliprdrFileContentsRequest` in the future.
|
||||
static ref CLIP_FILES: Arc<Mutex<ClipFiles>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FileContentsRequest {
|
||||
Size {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
},
|
||||
|
||||
Range {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClipFiles {
|
||||
files: Vec<String>,
|
||||
file_list: Vec<LocalFile>,
|
||||
files_pdu: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ClipFiles {
|
||||
fn clear(&mut self) {
|
||||
self.files.clear();
|
||||
self.file_list.clear();
|
||||
self.files_pdu.clear();
|
||||
}
|
||||
|
||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
||||
let clipboard_paths = clipboard_files
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(s))
|
||||
.collect::<Vec<_>>();
|
||||
self.file_list = construct_file_list(&clipboard_paths)?;
|
||||
self.files = clipboard_files.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_file_list_pdu(&mut self) {
|
||||
let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len());
|
||||
data.put_u32_le(self.file_list.len() as u32);
|
||||
for file in self.file_list.iter() {
|
||||
data.put(file.as_bin().as_slice());
|
||||
}
|
||||
self.files_pdu = data.to_vec()
|
||||
}
|
||||
|
||||
fn serve_file_contents(
|
||||
&mut self,
|
||||
conn_id: i32,
|
||||
request: FileContentsRequest,
|
||||
) -> Result<ClipboardFile, CliprdrError> {
|
||||
let (file_idx, file_contents_resp) = match request {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx,
|
||||
} => {
|
||||
log::debug!("file contents (size) requested from conn: {}", conn_id);
|
||||
let Some(file) = self.file_list.get(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_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_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
let size = file.size;
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: size.to_le_bytes().to_vec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents (range from {} length {}) request from conn: {}",
|
||||
offset,
|
||||
length,
|
||||
conn_id
|
||||
);
|
||||
let Some(file) = self.file_list.get_mut(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_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_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
if offset > file.size {
|
||||
log::error!("invalid reading offset requested from conn: {}", conn_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];
|
||||
|
||||
file.read_exact_at(&mut buf, offset)?;
|
||||
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: buf,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("file contents sent to conn: {}", conn_id);
|
||||
// hot reload next file
|
||||
for next_file in self.file_list.iter_mut().skip(file_idx + 1) {
|
||||
if !next_file.is_dir {
|
||||
next_file.load_handle()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(file_contents_resp)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear_files() {
|
||||
CLIP_FILES.lock().clear();
|
||||
}
|
||||
|
||||
pub fn read_file_contents(
|
||||
conn_id: i32,
|
||||
stream_id: i32,
|
||||
list_index: i32,
|
||||
dw_flags: i32,
|
||||
n_position_low: i32,
|
||||
n_position_high: i32,
|
||||
cb_requested: i32,
|
||||
) -> Result<ClipboardFile, CliprdrError> {
|
||||
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 {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"),
|
||||
});
|
||||
};
|
||||
|
||||
CLIP_FILES.lock().serve_file_contents(conn_id, fcr)
|
||||
}
|
||||
|
||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||
let mut files_lock = CLIP_FILES.lock();
|
||||
if files_lock.files == files {
|
||||
return Ok(());
|
||||
}
|
||||
files_lock.sync_files(files)?;
|
||||
Ok(files_lock.build_file_list_pdu())
|
||||
}
|
||||
|
||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||
CLIP_FILES.lock().files_pdu.clone()
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::CliprdrError;
|
||||
|
||||
// on x11, path will be encode as
|
||||
// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
// url encode and decode is needed
|
||||
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
||||
|
||||
pub(super) fn encode_path_to_uri(path: &Path) -> io::Result<String> {
|
||||
let encoded =
|
||||
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
|
||||
format!("file://{}", encoded)
|
||||
}
|
||||
|
||||
pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result<PathBuf, CliprdrError> {
|
||||
let encoded_path = encoded_uri.trim_start_matches("file://");
|
||||
let path_str = percent_encoding::percent_decode_str(encoded_path)
|
||||
.decode_utf8()
|
||||
.map_err(|_| CliprdrError::ConversionFailure)?;
|
||||
let path_str = path_str.to_string();
|
||||
|
||||
Ok(Path::new(&path_str).to_path_buf())
|
||||
}
|
||||
|
||||
// helper parse function
|
||||
// convert 'text/uri-list' data to a list of valid Paths
|
||||
// # Note
|
||||
// - none utf8 data will lead to error
|
||||
pub(super) fn parse_plain_uri_list(v: Vec<u8>) -> Result<Vec<PathBuf>, CliprdrError> {
|
||||
let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?;
|
||||
parse_uri_list(&text)
|
||||
}
|
||||
|
||||
// helper parse function
|
||||
// convert 'text/uri-list' data to a list of valid Paths
|
||||
// # Note
|
||||
// - none utf8 data will lead to error
|
||||
pub(super) 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)
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod uri_test {
|
||||
#[test]
|
||||
fn test_conversion() {
|
||||
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
||||
let uri = super::encode_path_to_uri(&path).unwrap();
|
||||
assert_eq!(
|
||||
uri,
|
||||
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
);
|
||||
let convert_back = super::parse_uri_to_path(&uri).unwrap();
|
||||
assert_eq!(path, convert_back);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_list() {
|
||||
let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png
|
||||
file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png
|
||||
"#;
|
||||
let list = super::parse_uri_list(uri_list.into()).unwrap();
|
||||
assert!(list.len() == 2);
|
||||
assert_eq!(list[0], list[1]);
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use hbb_common::log;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use x11_clipboard::Clipboard;
|
||||
use x11rb::protocol::xproto::Atom;
|
||||
|
||||
use crate::{platform::unix::send_format_list, CliprdrError};
|
||||
|
||||
use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard};
|
||||
|
||||
static X11_CLIPBOARD: OnceCell<Clipboard> = OnceCell::new();
|
||||
|
||||
fn get_clip() -> Result<&'static Clipboard, CliprdrError> {
|
||||
X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit))
|
||||
}
|
||||
|
||||
pub struct X11Clipboard {
|
||||
ignore_path: PathBuf,
|
||||
text_uri_list: Atom,
|
||||
gnome_copied_files: Atom,
|
||||
nautilus_clipboard: Atom,
|
||||
|
||||
former_file_list: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl X11Clipboard {
|
||||
pub fn new(ignore_path: &Path) -> Result<Self, CliprdrError> {
|
||||
let clipboard = get_clip()?;
|
||||
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)?;
|
||||
let nautilus_clipboard = clipboard
|
||||
.setter
|
||||
.get_atom("x-special/nautilus-clipboard")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
text_uri_list,
|
||||
gnome_copied_files,
|
||||
nautilus_clipboard,
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
})
|
||||
}
|
||||
|
||||
fn load(&self, target: Atom) -> Result<Vec<u8>, CliprdrError> {
|
||||
let clip = get_clip()?.setter.atoms.clipboard;
|
||||
let prop = get_clip()?.setter.atoms.property;
|
||||
// NOTE:
|
||||
// # why not use `load_wait`
|
||||
// load_wait is likely to wait forever, which is not what we want
|
||||
let res = get_clip()?.load_wait(clip, target, prop);
|
||||
match res {
|
||||
Ok(res) => Ok(res),
|
||||
Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]),
|
||||
Err(x11_clipboard::error::Error::Timeout) => {
|
||||
log::debug!("x11 clipboard get content timeout.");
|
||||
Err(CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("x11 clipboard get content fail: {:?}", e);
|
||||
Err(CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn store_batch(&self, batch: Vec<(Atom, Vec<u8>)>) -> Result<(), CliprdrError> {
|
||||
let clip = get_clip()?.setter.atoms.clipboard;
|
||||
log::debug!("try to store clipboard content");
|
||||
get_clip()?
|
||||
.store_batch(clip, batch)
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
fn wait_file_list(&self) -> Result<Option<Vec<PathBuf>>, CliprdrError> {
|
||||
let v = self.load(self.text_uri_list)?;
|
||||
let p = parse_plain_uri_list(v)?;
|
||||
Ok(Some(p))
|
||||
}
|
||||
}
|
||||
|
||||
impl SysClipboard for X11Clipboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
|
||||
let uri_list: Vec<String> = {
|
||||
let mut v = Vec::new();
|
||||
for path in paths {
|
||||
v.push(encode_path_to_uri(path)?);
|
||||
}
|
||||
v
|
||||
};
|
||||
let uri_list = uri_list.join("\n");
|
||||
let text_uri_list_data = uri_list.as_bytes().to_vec();
|
||||
let gnome_copied_files_data = ["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.clone()),
|
||||
(self.nautilus_clipboard, gnome_copied_files_data),
|
||||
];
|
||||
self.store_batch(batch)
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
{
|
||||
// clear cached file list
|
||||
*self.former_file_list.lock() = vec![];
|
||||
}
|
||||
loop {
|
||||
let sth = match self.wait_file_list() {
|
||||
Ok(sth) => sth,
|
||||
Err(e) => {
|
||||
log::warn!("failed to get file list from clipboard: {}", e);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(paths) = sth else {
|
||||
// just sleep
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
};
|
||||
|
||||
let filtered = paths
|
||||
.into_iter()
|
||||
.filter(|pb| !pb.starts_with(&self.ignore_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut former = self.former_file_list.lock();
|
||||
|
||||
let filtered_st: BTreeSet<_> = filtered.iter().collect();
|
||||
let former_st = former.iter().collect::<BTreeSet<_>>();
|
||||
if filtered_st == former_st {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
*former = filtered;
|
||||
}
|
||||
|
||||
if let Err(e) = send_format_list(0) {
|
||||
log::warn!("failed to send format list: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
log::debug!("stop listening file related atoms on clipboard");
|
||||
}
|
||||
|
||||
fn get_file_list(&self) -> Vec<PathBuf> {
|
||||
self.former_file_list.lock().clone()
|
||||
}
|
||||
}
|
||||
@@ -614,6 +614,7 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> {
|
||||
e => Err(CliprdrError::Unknown(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool {
|
||||
unsafe { TRUE == empty_cliprdr(context, conn_id as u32) }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.7"
|
||||
version = "1.3.8"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.3.7
|
||||
pkgver=1.3.8
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.7
|
||||
Version: 1.3.8
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.7
|
||||
Version: 1.3.8
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.7
|
||||
Version: 1.3.8
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
267
src/client.rs
267
src/client.rs
@@ -1,7 +1,9 @@
|
||||
#[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")))]
|
||||
use clipboard_master::{CallbackResult, ClipboardHandler};
|
||||
use clipboard_master::CallbackResult;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
@@ -15,17 +17,25 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
io,
|
||||
net::SocketAddr,
|
||||
ops::Deref,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
mpsc::{self, RecvTimeoutError, Sender},
|
||||
mpsc::{self, RecvTimeoutError},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
check_port,
|
||||
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp,
|
||||
ui_interface::{get_builtin_option, use_texture_render},
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::{clipboard::check_clipboard_files, clipboard_file::unix_file_clip};
|
||||
pub use file_trait::FileManager;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -62,14 +72,6 @@ use scrap::{
|
||||
CodecFormat, ImageFormat, ImageRgb, ImageTexture,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
check_port,
|
||||
common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
|
||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp,
|
||||
ui_interface::{get_builtin_option, use_texture_render},
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use crate::clipboard::CLIPBOARD_INTERVAL;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -128,14 +130,19 @@ pub(crate) struct ClientClipboardContext;
|
||||
pub(crate) struct ClientClipboardContext {
|
||||
pub cfg: SessionPermissionConfig,
|
||||
pub tx: UnboundedSender<Data>,
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub is_file_supported: bool,
|
||||
}
|
||||
|
||||
/// Client of the remote desktop.
|
||||
pub struct Client;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
struct TextClipboardState {
|
||||
is_required: bool,
|
||||
struct ClipboardState {
|
||||
#[cfg(feature = "flutter")]
|
||||
is_text_required: bool,
|
||||
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
|
||||
is_file_required: bool,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
@@ -151,7 +158,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref TEXT_CLIPBOARD_STATE: Arc<Mutex<TextClipboardState>> = Arc::new(Mutex::new(TextClipboardState::new()));
|
||||
static ref CLIPBOARD_STATE: Arc<Mutex<ClipboardState>> = Arc::new(Mutex::new(ClipboardState::new()));
|
||||
}
|
||||
|
||||
const PUBLIC_SERVER: &str = "public";
|
||||
@@ -167,6 +174,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,
|
||||
@@ -657,7 +666,13 @@ impl Client {
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn set_is_text_clipboard_required(b: bool) {
|
||||
TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b;
|
||||
CLIPBOARD_STATE.lock().unwrap().is_text_required = b;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
|
||||
pub fn set_is_file_clipboard_required(b: bool) {
|
||||
CLIPBOARD_STATE.lock().unwrap().is_file_required = b;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
@@ -673,68 +688,55 @@ impl Client {
|
||||
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
|
||||
return;
|
||||
}
|
||||
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
|
||||
CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||
clipboard::platform::unix::fuse::uninit_fuse_context(true);
|
||||
}
|
||||
|
||||
// `try_start_clipboard` is called by all session when connection is established. (When handling peer info).
|
||||
// This function only create one thread with a loop, the loop is shared by all sessions.
|
||||
// After all sessions are end, the loop exists.
|
||||
//
|
||||
// If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`.
|
||||
// If clipboard update is detected, the text will be sent to all sessions by `send_clipboard_msg`.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
fn try_start_clipboard(
|
||||
_client_clip_ctx: Option<ClientClipboardContext>,
|
||||
) -> Option<UnboundedReceiver<()>> {
|
||||
let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap();
|
||||
let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap();
|
||||
if clipboard_lock.running {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 || {
|
||||
let mut is_sent = false;
|
||||
let mut handler = ClientClipboardHandler {
|
||||
ctx: None,
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
client_clip_ctx: _client_clip_ctx,
|
||||
};
|
||||
|
||||
tx_started.send(()).ok();
|
||||
loop {
|
||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().running {
|
||||
if !CLIPBOARD_STATE.lock().unwrap().running {
|
||||
break;
|
||||
}
|
||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
|
||||
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
|
||||
continue;
|
||||
}
|
||||
|
||||
if !is_sent {
|
||||
is_sent = true;
|
||||
tx_started.send(()).ok();
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -744,13 +746,14 @@ 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();
|
||||
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
log::info!("Stop client clipboard loop");
|
||||
CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
});
|
||||
|
||||
Some(rx_started)
|
||||
@@ -758,31 +761,31 @@ impl Client {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn try_start_clipboard(_p: Option<()>) -> Option<UnboundedReceiver<()>> {
|
||||
let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap();
|
||||
let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap();
|
||||
if clipboard_lock.running {
|
||||
return None;
|
||||
}
|
||||
clipboard_lock.running = true;
|
||||
|
||||
log::info!("Start text clipboard loop");
|
||||
log::info!("Start client clipboard loop");
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().running {
|
||||
if !CLIPBOARD_STATE.lock().unwrap().running {
|
||||
break;
|
||||
}
|
||||
if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required {
|
||||
if !CLIPBOARD_STATE.lock().unwrap().is_text_required {
|
||||
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(msg) = crate::clipboard::get_clipboards_msg(true) {
|
||||
crate::flutter::send_text_clipboard_msg(msg);
|
||||
crate::flutter::send_clipboard_msg(msg, false);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL));
|
||||
}
|
||||
log::info!("Stop text clipboard loop");
|
||||
TEXT_CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
log::info!("Stop client clipboard loop");
|
||||
CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||
});
|
||||
|
||||
None
|
||||
@@ -790,10 +793,13 @@ impl Client {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
impl TextClipboardState {
|
||||
impl ClipboardState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
is_required: true,
|
||||
#[cfg(feature = "flutter")]
|
||||
is_text_required: true,
|
||||
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
|
||||
is_file_required: true,
|
||||
running: false,
|
||||
}
|
||||
}
|
||||
@@ -802,62 +808,105 @@ impl TextClipboardState {
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
impl ClientClipboardHandler {
|
||||
fn is_text_required(&self) -> bool {
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
CLIPBOARD_STATE.lock().unwrap().is_text_required
|
||||
}
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
{
|
||||
self.client_clip_ctx
|
||||
.as_ref()
|
||||
.map(|ctx| ctx.cfg.is_text_clipboard_required())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn is_file_required(&self) -> bool {
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
CLIPBOARD_STATE.lock().unwrap().is_file_required
|
||||
}
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
{
|
||||
self.client_clip_ctx
|
||||
.as_ref()
|
||||
.map(|ctx| ctx.cfg.is_file_clipboard_required())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_clipboard(&mut self) {
|
||||
if CLIPBOARD_STATE.lock().unwrap().running {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) {
|
||||
if !urls.is_empty() {
|
||||
if self.is_file_required() {
|
||||
match clipboard::platform::unix::serv_files::sync_files(&urls) {
|
||||
Ok(()) => {
|
||||
let msg = crate::clipboard_file::clip_2_msg(
|
||||
unix_file_clip::get_format_list(),
|
||||
);
|
||||
self.send_msg(msg, true);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to sync clipboard files: {}", e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) {
|
||||
if self.is_text_required() {
|
||||
self.send_msg(msg, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "flutter")]
|
||||
fn send_msg(&self, msg: Message) {
|
||||
crate::flutter::send_text_clipboard_msg(msg);
|
||||
fn send_msg(&self, msg: Message, _is_file: bool) {
|
||||
crate::flutter::send_clipboard_msg(msg, _is_file);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
fn send_msg(&self, msg: Message) {
|
||||
fn send_msg(&self, msg: Message, _is_file: bool) {
|
||||
if let Some(ctx) = &self.client_clip_ctx {
|
||||
if ctx.cfg.is_text_clipboard_required() {
|
||||
if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() {
|
||||
if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union {
|
||||
if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(
|
||||
&pi.version,
|
||||
&pi.platform,
|
||||
multi_clipboards,
|
||||
) {
|
||||
let _ = ctx.tx.send(Data::Message(msg_out));
|
||||
return;
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if _is_file {
|
||||
if ctx.is_file_supported {
|
||||
let _ = ctx.tx.send(Data::Message(msg));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() {
|
||||
if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union {
|
||||
if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(
|
||||
&pi.version,
|
||||
&pi.platform,
|
||||
multi_clipboards,
|
||||
) {
|
||||
let _ = ctx.tx.send(Data::Message(msg_out));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = ctx.tx.send(Data::Message(msg));
|
||||
}
|
||||
let _ = ctx.tx.send(Data::Message(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
impl ClipboardHandler for ClientClipboardHandler {
|
||||
fn on_clipboard_change(&mut self) -> CallbackResult {
|
||||
if TEXT_CLIPBOARD_STATE.lock().unwrap().running
|
||||
&& TEXT_CLIPBOARD_STATE.lock().unwrap().is_required
|
||||
{
|
||||
if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) {
|
||||
self.send_msg(msg);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -1813,6 +1862,12 @@ impl LoginConfigHandler {
|
||||
self.config.store(&self.id);
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if option.enable_file_transfer.enum_value() == Ok(BoolOption::No) {
|
||||
crate::clipboard::try_empty_clipboard_files(crate::clipboard::ClipboardSide::Client, 0);
|
||||
}
|
||||
|
||||
if !name.contains("block-input") {
|
||||
self.save_config(config);
|
||||
}
|
||||
@@ -2338,6 +2393,10 @@ impl LoginConfigHandler {
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
/// Media data.
|
||||
@@ -3240,7 +3299,7 @@ pub enum Data {
|
||||
CancelJob(i32),
|
||||
RemovePortForward(i32),
|
||||
AddPortForward((i32, String, i32)),
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
ToggleClipboardFile,
|
||||
NewRDP,
|
||||
SetConfirmOverrideFile((i32, i32, bool, bool, bool)),
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
num::NonZeroI64,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::{update_clipboard, ClipboardSide};
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
@@ -20,7 +10,9 @@ use crate::{
|
||||
common::get_default_sound_input,
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||
#[cfg(target_os = "windows")]
|
||||
use clipboard::ContextSend;
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
@@ -44,9 +36,18 @@ use hbb_common::{
|
||||
},
|
||||
Stream,
|
||||
};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType};
|
||||
use scrap::CodecFormat;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
num::NonZeroI64,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Remote<T: InvokeUiSession> {
|
||||
handler: Session<T>,
|
||||
@@ -63,7 +64,7 @@ pub struct Remote<T: InvokeUiSession> {
|
||||
last_update_jobs_status: (Instant, HashMap<i32, u64>),
|
||||
is_connected: bool,
|
||||
first_frame: bool,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
client_conn_id: i32, // used for file clipboard
|
||||
data_count: Arc<AtomicUsize>,
|
||||
video_format: CodecFormat,
|
||||
@@ -107,7 +108,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
last_update_jobs_status: (Instant::now(), Default::default()),
|
||||
is_connected: false,
|
||||
first_frame: false,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
client_conn_id: 0,
|
||||
data_count: Arc::new(AtomicUsize::new(0)),
|
||||
video_format: CodecFormat::Unknown,
|
||||
@@ -122,7 +123,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
let _file_clip_context_holder = {
|
||||
// `is_port_forward()` will not reach here, but we still check it for clarity.
|
||||
if !self.handler.is_file_transfer() && !self.handler.is_port_forward() {
|
||||
@@ -175,26 +176,33 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
// just build for now
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
#[cfg(not(any(target_os = "windows", feature = "unix-file-copy-paste")))]
|
||||
let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>();
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
let (_tx_holder, rx) = mpsc::unbounded_channel();
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx));
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None);
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
{
|
||||
let is_conn_not_default = self.handler.is_file_transfer()
|
||||
|| self.handler.is_port_forward()
|
||||
|| self.handler.is_rdp();
|
||||
if !is_conn_not_default {
|
||||
log::debug!("get cliprdr client for conn_id {}", self.client_conn_id);
|
||||
(self.client_conn_id, rx_clip_client_lock) =
|
||||
(self.client_conn_id, rx_clip_client_holder.0) =
|
||||
clipboard::get_rx_cliprdr_client(&self.handler.get_id());
|
||||
log::debug!("get cliprdr client for conn_id {}", self.client_conn_id);
|
||||
let client_conn_id = self.client_conn_id;
|
||||
rx_clip_client_holder.1 = Some(crate::SimpleCallOnReturn {
|
||||
b: true,
|
||||
f: Box::new(move || {
|
||||
clipboard::remove_channel_by_conn_id(client_conn_id);
|
||||
}),
|
||||
});
|
||||
};
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let mut rx_clip_client = rx_clip_client_lock.lock().await;
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
let mut rx_clip_client = rx_clip_client_holder.0.lock().await;
|
||||
|
||||
let mut status_timer =
|
||||
crate::rustdesk_interval(time::interval(Duration::new(1, 0)));
|
||||
@@ -242,8 +250,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
_msg = rx_clip_client.recv() => {
|
||||
#[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))]
|
||||
self.handle_local_clipboard_msg(&mut peer, _msg).await;
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
self.handle_local_clipboard_msg(&mut peer, _msg).await;
|
||||
}
|
||||
_ = self.timer.tick() => {
|
||||
if last_recv_time.elapsed() >= SEC30 {
|
||||
@@ -323,18 +331,13 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
Client::try_stop_clipboard();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
if _set_disconnected_ok {
|
||||
let conn_id = self.client_conn_id;
|
||||
log::debug!("try empty cliprdr for conn_id {}", conn_id);
|
||||
let _ = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.empty_clipboard(conn_id)?;
|
||||
Ok(())
|
||||
});
|
||||
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
async fn handle_local_clipboard_msg(
|
||||
&self,
|
||||
peer: &mut crate::client::FramedStream,
|
||||
@@ -365,8 +368,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
view_only, stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled
|
||||
);
|
||||
if stop {
|
||||
ContextSend::set_is_stopped();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
ContextSend::set_is_stopped();
|
||||
}
|
||||
} else {
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Err(e) = ContextSend::make_sure_enabled() {
|
||||
log::error!("failed to restart clipboard context: {}", e);
|
||||
// to-do: Show msgbox with "Don't show again" option
|
||||
@@ -509,7 +516,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
.handle_login_from_ui(os_username, os_password, password, remember, peer)
|
||||
.await;
|
||||
}
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
Data::ToggleClipboardFile => {
|
||||
self.check_clipboard_file_context();
|
||||
}
|
||||
@@ -1221,7 +1228,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let peer_platform = pi.platform.clone();
|
||||
self.set_peer_info(&pi);
|
||||
self.handler.handle_peer_info(pi);
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
self.check_clipboard_file_context();
|
||||
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) {
|
||||
#[cfg(feature = "flutter")]
|
||||
@@ -1233,6 +1240,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
crate::client::ClientClipboardContext {
|
||||
cfg: self.handler.get_permission_config(),
|
||||
tx: self.sender.clone(),
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
is_file_supported: crate::is_support_file_copy_paste(
|
||||
&peer_version,
|
||||
),
|
||||
},
|
||||
));
|
||||
// To make sure current text clipboard data is updated.
|
||||
@@ -1264,6 +1275,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
|
||||
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
|
||||
crate::flutter::update_file_clipboard_required();
|
||||
|
||||
// on connection established client
|
||||
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -1317,9 +1331,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
Some(message::Union::Cliprdr(clip)) => {
|
||||
self.handle_cliprdr_msg(clip);
|
||||
self.handle_cliprdr_msg(clip, peer).await;
|
||||
}
|
||||
Some(message::Union::FileResponse(fr)) => {
|
||||
match fr.union {
|
||||
@@ -1484,6 +1498,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
|
||||
crate::flutter::update_file_clipboard_required();
|
||||
self.handler.set_permission("keyboard", p.enabled);
|
||||
}
|
||||
Ok(Permission::Clipboard) => {
|
||||
@@ -1502,7 +1518,16 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
if !p.enabled && self.handler.is_file_transfer() {
|
||||
return true;
|
||||
}
|
||||
#[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))]
|
||||
crate::flutter::update_file_clipboard_required();
|
||||
self.handler.set_permission("file", p.enabled);
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if !p.enabled {
|
||||
try_empty_clipboard_files(
|
||||
ClipboardSide::Client,
|
||||
self.client_conn_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(Permission::Restart) => {
|
||||
self.handler.set_permission("restart", p.enabled);
|
||||
@@ -1922,24 +1947,19 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
fn check_clipboard_file_context(&self) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
feature = "unix-file-copy-paste",
|
||||
any(target_os = "linux", target_os = "macos")
|
||||
)
|
||||
))]
|
||||
{
|
||||
let enabled = *self.handler.server_file_transfer_enabled.read().unwrap()
|
||||
&& self.handler.lc.read().unwrap().enable_file_copy_paste.v;
|
||||
ContextSend::enable(enabled);
|
||||
}
|
||||
let enabled = *self.handler.server_file_transfer_enabled.read().unwrap()
|
||||
&& self.handler.lc.read().unwrap().enable_file_copy_paste.v;
|
||||
ContextSend::enable(enabled);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) {
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
async fn handle_cliprdr_msg(
|
||||
&self,
|
||||
clip: hbb_common::message_proto::Cliprdr,
|
||||
_peer: &mut Stream,
|
||||
) {
|
||||
log::debug!("handling cliprdr msg from server peer");
|
||||
#[cfg(feature = "flutter")]
|
||||
if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union {
|
||||
@@ -1956,20 +1976,34 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
};
|
||||
|
||||
let is_stopping_allowed = clip.is_beginning_message();
|
||||
let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v;
|
||||
let file_transfer_enabled = self.handler.is_file_clipboard_required();
|
||||
let stop = is_stopping_allowed && !file_transfer_enabled;
|
||||
log::debug!(
|
||||
"Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}",
|
||||
stop, is_stopping_allowed, file_transfer_enabled);
|
||||
if !stop {
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Err(e) = ContextSend::make_sure_enabled() {
|
||||
log::error!("failed to restart clipboard context: {}", e);
|
||||
};
|
||||
let _ = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context
|
||||
.server_clip_file(self.client_conn_id, clip)
|
||||
.map_err(|e| e.into())
|
||||
});
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context
|
||||
.server_clip_file(self.client_conn_id, clip)
|
||||
.map_err(|e| e.into())
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) {
|
||||
if let Some(msg) = unix_file_clip::serve_clip_messages(
|
||||
ClipboardSide::Client,
|
||||
clip,
|
||||
self.client_conn_id,
|
||||
) {
|
||||
allow_err!(_peer.send(&msg).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
461
src/clipboard.rs
461
src/clipboard.rs
@@ -1,15 +1,14 @@
|
||||
#[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},
|
||||
thread::JoinHandle,
|
||||
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.
|
||||
@@ -43,115 +42,12 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[
|
||||
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(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||
static X11_CLIPBOARD: once_cell::sync::OnceCell<x11_clipboard::Clipboard> =
|
||||
once_cell::sync::OnceCell::new();
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||
fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> {
|
||||
X11_CLIPBOARD
|
||||
.get_or_try_init(|| x11_clipboard::Clipboard::new())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||
pub struct ClipboardContext {
|
||||
string_setter: x11rb::protocol::xproto::Atom,
|
||||
string_getter: x11rb::protocol::xproto::Atom,
|
||||
text_uri_list: x11rb::protocol::xproto::Atom,
|
||||
|
||||
clip: x11rb::protocol::xproto::Atom,
|
||||
prop: x11rb::protocol::xproto::Atom,
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||
fn parse_plain_uri_list(v: Vec<u8>) -> Result<String, String> {
|
||||
let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?;
|
||||
let mut list = String::new();
|
||||
for line in text.lines() {
|
||||
if !line.starts_with("file://") {
|
||||
continue;
|
||||
}
|
||||
let decoded = percent_encoding::percent_decode_str(line)
|
||||
.decode_utf8()
|
||||
.map_err(|_| "ConversionFailure".to_owned())?;
|
||||
list = list + "\n" + decoded.trim_start_matches("file://");
|
||||
}
|
||||
list = list.trim().to_owned();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))]
|
||||
impl ClipboardContext {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let clipboard = get_clipboard()?;
|
||||
let string_getter = clipboard
|
||||
.getter
|
||||
.get_atom("UTF8_STRING")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let string_setter = clipboard
|
||||
.setter
|
||||
.get_atom("UTF8_STRING")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let text_uri_list = clipboard
|
||||
.getter
|
||||
.get_atom("text/uri-list")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let prop = clipboard.getter.atoms.property;
|
||||
let clip = clipboard.getter.atoms.clipboard;
|
||||
Ok(Self {
|
||||
text_uri_list,
|
||||
string_setter,
|
||||
string_getter,
|
||||
clip,
|
||||
prop,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_text(&mut self) -> Result<String, String> {
|
||||
let clip = self.clip;
|
||||
let prop = self.prop;
|
||||
|
||||
const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120);
|
||||
|
||||
let text_content = get_clipboard()?
|
||||
.load(clip, self.string_getter, prop, TIMEOUT)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT)?;
|
||||
|
||||
if file_urls.is_err() || file_urls.as_ref().is_empty() {
|
||||
log::trace!("clipboard get text, no file urls");
|
||||
return String::from_utf8(text_content).map_err(|e| e.to_string());
|
||||
}
|
||||
|
||||
let file_urls = parse_plain_uri_list(file_urls)?;
|
||||
|
||||
let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?;
|
||||
|
||||
if text_content.trim() == file_urls.trim() {
|
||||
log::trace!("clipboard got text but polluted");
|
||||
return Err(String::from("polluted text"));
|
||||
}
|
||||
|
||||
Ok(text_content)
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, content: String) -> Result<(), String> {
|
||||
let clip = self.clip;
|
||||
|
||||
let value = content.clone().into_bytes();
|
||||
get_clipboard()?
|
||||
.store(clip, self.string_setter, value)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn check_clipboard(
|
||||
ctx: &mut Option<ClipboardContext>,
|
||||
@@ -179,6 +75,73 @@ pub fn check_clipboard(
|
||||
None
|
||||
}
|
||||
|
||||
#[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(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) {
|
||||
#[cfg(target_os = "linux")]
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mut ctx) = ctx.as_mut() {
|
||||
use clipboard::platform::unix;
|
||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||
ctx.try_empty_clipboard_files(_side);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[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();
|
||||
@@ -203,10 +166,15 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
let mut to_update_data = proto::from_multi_clipbards(multi_clipboards);
|
||||
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() {
|
||||
@@ -240,13 +208,11 @@ pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||
pub struct ClipboardContext {
|
||||
inner: arboard::Clipboard,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))]
|
||||
#[allow(unreachable_code)]
|
||||
impl ClipboardContext {
|
||||
pub fn new() -> ResultType<ClipboardContext> {
|
||||
@@ -293,7 +259,7 @@ impl ClipboardContext {
|
||||
// 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(SUPPORTED_FORMATS) {
|
||||
match self.inner.get_formats(formats) {
|
||||
Ok(data) => {
|
||||
return Ok(data
|
||||
.into_iter()
|
||||
@@ -316,8 +282,26 @@ impl ClipboardContext {
|
||||
}
|
||||
|
||||
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(SUPPORTED_FORMATS)?;
|
||||
let data = self.get_formats(formats)?;
|
||||
if data.is_empty() {
|
||||
return Ok(data);
|
||||
}
|
||||
@@ -334,16 +318,98 @@ impl ClipboardContext {
|
||||
.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(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]) {
|
||||
#[cfg(target_os = "linux")]
|
||||
let exclude_path =
|
||||
clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client);
|
||||
#[cfg(target_os = "macos")]
|
||||
let exclude_path: Arc<String> = Default::default();
|
||||
let urls = 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<_>>();
|
||||
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 = {
|
||||
let is_kde = std::process::Command::new("sh")
|
||||
.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 {
|
||||
@@ -427,36 +493,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"))]
|
||||
@@ -671,3 +707,140 @@ 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.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,3 +189,206 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option<ClipboardFile> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub mod unix_file_clip {
|
||||
use crate::clipboard::try_empty_clipboard_files;
|
||||
|
||||
use super::{
|
||||
super::clipboard::{update_clipboard_files, ClipboardSide},
|
||||
*,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
use clipboard::platform::unix::fuse;
|
||||
use clipboard::platform::unix::{
|
||||
get_local_format, serv_files, FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME,
|
||||
FILEDESCRIPTORW_FORMAT_NAME, FILEDESCRIPTOR_FORMAT_ID,
|
||||
};
|
||||
use hbb_common::log;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CLIPBOARD_CTX: Arc<Mutex<Option<crate::clipboard::ClipboardContext>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
pub fn get_format_list() -> ClipboardFile {
|
||||
let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID)
|
||||
.unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string());
|
||||
let fc_format_name = get_local_format(FILECONTENTS_FORMAT_ID)
|
||||
.unwrap_or(FILECONTENTS_FORMAT_NAME.to_string());
|
||||
ClipboardFile::FormatList {
|
||||
format_list: vec![
|
||||
(FILEDESCRIPTOR_FORMAT_ID, fd_format_name),
|
||||
(FILECONTENTS_FORMAT_ID, fc_format_name),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn msg_resp_format_data_failure() -> Message {
|
||||
clip_2_msg(ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 0x2,
|
||||
format_data: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn resp_file_contents_fail(stream_id: i32) -> Message {
|
||||
clip_2_msg(ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x2,
|
||||
stream_id,
|
||||
requested_data: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serve_clip_messages(
|
||||
side: ClipboardSide,
|
||||
clip: ClipboardFile,
|
||||
conn_id: i32,
|
||||
) -> Option<Message> {
|
||||
log::debug!("got clipfile from client peer");
|
||||
match clip {
|
||||
ClipboardFile::MonitorReady => {
|
||||
log::debug!("client is ready for clipboard");
|
||||
}
|
||||
ClipboardFile::FormatList { format_list } => {
|
||||
if !format_list
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
.is_some()
|
||||
{
|
||||
log::error!("no file contents format found");
|
||||
return None;
|
||||
};
|
||||
let Some(file_descriptor_id) = format_list
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)
|
||||
else {
|
||||
log::error!("no file descriptor format found");
|
||||
return None;
|
||||
};
|
||||
// sync file system from peer
|
||||
let data = ClipboardFile::FormatDataRequest {
|
||||
requested_format_id: file_descriptor_id,
|
||||
};
|
||||
return Some(clip_2_msg(data));
|
||||
}
|
||||
ClipboardFile::FormatListResponse {
|
||||
msg_flags: _msg_flags,
|
||||
} => {}
|
||||
ClipboardFile::FormatDataRequest {
|
||||
requested_format_id: _requested_format_id,
|
||||
} => {
|
||||
log::debug!("requested format id: {}", _requested_format_id);
|
||||
let format_data = serv_files::get_file_list_pdu();
|
||||
if !format_data.is_empty() {
|
||||
return Some(clip_2_msg(ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 1,
|
||||
format_data,
|
||||
}));
|
||||
}
|
||||
// empty file list, send failure message
|
||||
return Some(msg_resp_format_data_failure());
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags,
|
||||
format_data,
|
||||
} => {
|
||||
log::debug!("format data response: msg_flags: {}", msg_flags);
|
||||
|
||||
if msg_flags != 0x1 {
|
||||
// return failure message?
|
||||
}
|
||||
|
||||
log::debug!("parsing file descriptors");
|
||||
if fuse::init_fuse_context(true).is_ok() {
|
||||
match fuse::format_data_response_to_urls(
|
||||
side == ClipboardSide::Client,
|
||||
format_data,
|
||||
conn_id,
|
||||
) {
|
||||
Ok(files) => {
|
||||
update_clipboard_files(files, side);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to parse file descriptors: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// send error message to server
|
||||
}
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
..
|
||||
} => {
|
||||
log::debug!("file contents request: stream_id: {}, list_index: {}, dw_flags: {}, n_position_low: {}, n_position_high: {}, cb_requested: {}", stream_id, list_index, dw_flags, n_position_low, n_position_high, cb_requested);
|
||||
match serv_files::read_file_contents(
|
||||
conn_id,
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
) {
|
||||
Ok(data) => {
|
||||
return Some(clip_2_msg(data));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to read file contents: {:?}", e);
|
||||
return Some(resp_file_contents_fail(stream_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags,
|
||||
stream_id,
|
||||
..
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents response: msg_flags: {}, stream_id: {}",
|
||||
msg_flags,
|
||||
stream_id,
|
||||
);
|
||||
if fuse::init_fuse_context(true).is_ok() {
|
||||
hbb_common::allow_err!(fuse::handle_file_content_response(
|
||||
side == ClipboardSide::Client,
|
||||
clip
|
||||
));
|
||||
} else {
|
||||
// send error message to server
|
||||
}
|
||||
}
|
||||
ClipboardFile::NotifyCallback {
|
||||
r#type,
|
||||
title,
|
||||
text,
|
||||
} => {
|
||||
// unreachable, but still log it
|
||||
log::debug!(
|
||||
"notify callback: type: {}, title: {}, text: {}",
|
||||
r#type,
|
||||
title,
|
||||
text
|
||||
);
|
||||
}
|
||||
ClipboardFile::TryEmpty => {
|
||||
try_empty_clipboard_files(side, conn_id);
|
||||
}
|
||||
_ => {
|
||||
log::error!("unsupported clipboard file type");
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub struct SimpleCallOnReturn {
|
||||
pub b: bool,
|
||||
pub f: Box<dyn Fn() + 'static>,
|
||||
pub f: Box<dyn Fn() + Send + 'static>,
|
||||
}
|
||||
|
||||
impl Drop for SimpleCallOnReturn {
|
||||
@@ -127,6 +127,18 @@ pub fn is_support_multi_ui_session_num(ver: i64) -> bool {
|
||||
ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn is_support_file_copy_paste(ver: &str) -> bool {
|
||||
is_support_file_copy_paste_num(hbb_common::get_version_number(ver))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn is_support_file_copy_paste_num(ver: i64) -> bool {
|
||||
ver >= hbb_common::get_version_number("1.3.8")
|
||||
}
|
||||
|
||||
// is server process, with "--server" args
|
||||
#[inline]
|
||||
pub fn is_server() -> bool {
|
||||
@@ -751,7 +763,6 @@ pub fn get_sysinfo() -> serde_json::Value {
|
||||
os = format!("{os} - {}", system.os_version().unwrap_or_default());
|
||||
}
|
||||
let hostname = hostname(); // sys.hostname() return localhost on android in my test
|
||||
use serde_json::json;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let out;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -1057,7 +1068,6 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Strin
|
||||
}
|
||||
|
||||
pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Map<String, Value> {
|
||||
use serde_json::json;
|
||||
let mut fd_json = serde_json::Map::new();
|
||||
fd_json.insert("id".into(), json!(id));
|
||||
fd_json.insert("path".into(), json!(path));
|
||||
|
||||
@@ -1305,9 +1305,26 @@ pub fn update_text_clipboard_required() {
|
||||
Client::set_is_text_clipboard_required(is_required);
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn update_file_clipboard_required() {
|
||||
let is_required = sessions::get_sessions()
|
||||
.iter()
|
||||
.any(|s| s.is_file_clipboard_required());
|
||||
Client::set_is_file_clipboard_required(is_required);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn send_text_clipboard_msg(msg: Message) {
|
||||
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
||||
for s in sessions::get_sessions() {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if _is_file {
|
||||
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
|
||||
&& s.is_file_clipboard_required()
|
||||
{
|
||||
s.send(Data::Message(msg.clone()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if s.is_text_clipboard_required() {
|
||||
// Check if the client supports multi clipboards
|
||||
if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union {
|
||||
|
||||
@@ -275,6 +275,12 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
|
||||
if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" {
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if sessions::get_session_by_session_id(&session_id).is_some()
|
||||
&& value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE
|
||||
{
|
||||
crate::flutter::update_file_clipboard_required();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_toggle_privacy_mode(session_id: SessionID, impl_key: String, on: bool) {
|
||||
@@ -1948,13 +1954,7 @@ pub fn main_hide_dock() -> SyncReturn<bool> {
|
||||
}
|
||||
|
||||
pub fn main_has_file_clipboard() -> SyncReturn<bool> {
|
||||
let ret = cfg!(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
feature = "unix-file-copy-paste",
|
||||
any(target_os = "linux", target_os = "macos")
|
||||
)
|
||||
));
|
||||
let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",));
|
||||
SyncReturn(ret)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ use hbb_common::{
|
||||
config::{self, Config, Config2},
|
||||
futures::StreamExt as _,
|
||||
futures_util::sink::SinkExt,
|
||||
log, password_security as password,
|
||||
sodiumoxide::base64,
|
||||
timeout,
|
||||
log, password_security as password, timeout,
|
||||
tokio::{
|
||||
self,
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
@@ -230,7 +228,7 @@ pub enum Data {
|
||||
FS(FS),
|
||||
Test,
|
||||
SyncConfig(Option<Box<(Config, Config2)>>),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(target_os = "windows")]
|
||||
ClipboardFile(ClipboardFile),
|
||||
ClipboardFileEnabled(bool),
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -106,7 +106,13 @@ pub fn new() -> ServerPtr {
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
{
|
||||
server.add_service(Box::new(display_service::new()));
|
||||
server.add_service(Box::new(clipboard_service::new()));
|
||||
server.add_service(Box::new(clipboard_service::new(
|
||||
clipboard_service::NAME.to_owned(),
|
||||
)));
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
server.add_service(Box::new(clipboard_service::new(
|
||||
clipboard_service::FILE_NAME.to_owned(),
|
||||
)));
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
use super::*;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use crate::clipboard::clipboard_listener;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub use crate::clipboard::{check_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, ClipboardHandler};
|
||||
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, Sender},
|
||||
sync::mpsc::{channel, RecvTimeoutError},
|
||||
time::Duration,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
@@ -23,9 +32,7 @@ static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
struct Handler {
|
||||
sp: EmptyExtraFieldService,
|
||||
ctx: Option<ClipboardContext>,
|
||||
tx_cb_result: Sender<CallbackResult>,
|
||||
#[cfg(target_os = "windows")]
|
||||
stream: Option<ipc::ConnectionTmpl<parity_tokio_ipc::ConnectionClient>>,
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -37,39 +44,51 @@ pub fn is_clipboard_service_ok() -> bool {
|
||||
CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn new() -> GenericService {
|
||||
let svc = EmptyExtraFieldService::new(NAME.to_owned(), false);
|
||||
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 handler = Handler {
|
||||
sp: sp.clone(),
|
||||
ctx: Some(ClipboardContext::new()?),
|
||||
tx_cb_result,
|
||||
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,
|
||||
};
|
||||
|
||||
let (tx_start_res, rx_start_res) = 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)) => {
|
||||
bail!(err);
|
||||
}
|
||||
Err(e) => {
|
||||
bail!("Failed to create clipboard listener: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -78,36 +97,40 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
||||
bail!("Clipboard listener stopped with error: {}", err);
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {}
|
||||
_ => {}
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
log::error!("Clipboard listener disconnected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.signal();
|
||||
h.join().ok();
|
||||
|
||||
clipboard_listener::unsubscribe(&sp.name());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
impl ClipboardHandler for Handler {
|
||||
fn on_clipboard_change(&mut self) -> CallbackResult {
|
||||
if self.sp.ok() {
|
||||
if let Some(msg) = self.get_clipboard_msg() {
|
||||
self.sp.send(msg);
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CallbackResult::Next
|
||||
}
|
||||
|
||||
fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult {
|
||||
self.tx_cb_result
|
||||
.send(CallbackResult::StopWithError(error))
|
||||
.ok();
|
||||
CallbackResult::Next
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
impl Handler {
|
||||
fn get_clipboard_msg(&mut self) -> Option<Message> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if crate::common::is_server() && crate::platform::is_root() {
|
||||
@@ -144,6 +167,7 @@ impl Handler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::{input_service::*, *};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
use crate::clipboard::try_empty_clipboard_files;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::clipboard::{update_clipboard, ClipboardSide};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
@@ -6,8 +8,6 @@ use crate::clipboard_file::*;
|
||||
#[cfg(target_os = "android")]
|
||||
use crate::keyboard::client::map_key_to_control_key;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::platform::linux::is_x11;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::platform::linux_desktop_manager;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use crate::platform::WallPaperRemover;
|
||||
@@ -441,6 +441,28 @@ impl Connection {
|
||||
std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned));
|
||||
let mut second_timer = crate::rustdesk_interval(time::interval(Duration::from_secs(1)));
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
let rx_clip_holder;
|
||||
let mut rx_clip;
|
||||
let _tx_clip: mpsc::UnboundedSender<i32>;
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
{
|
||||
rx_clip_holder = (
|
||||
clipboard::get_rx_cliprdr_server(id),
|
||||
crate::SimpleCallOnReturn {
|
||||
b: true,
|
||||
f: Box::new(move || {
|
||||
clipboard::remove_channel_by_conn_id(id);
|
||||
}),
|
||||
},
|
||||
);
|
||||
rx_clip = rx_clip_holder.0.lock().await;
|
||||
}
|
||||
#[cfg(not(feature = "unix-file-copy-paste"))]
|
||||
{
|
||||
(_tx_clip, rx_clip) = mpsc::unbounded_channel::<i32>();
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// biased; // video has higher priority // causing test_delay_timer failed while transferring big file
|
||||
@@ -488,6 +510,12 @@ impl Connection {
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::NAME,
|
||||
conn.inner.clone(), conn.can_sub_clipboard_service());
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::FILE_NAME,
|
||||
conn.inner.clone(),
|
||||
conn.can_sub_file_clipboard_service(),
|
||||
);
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
conn.inner.clone(), enabled || conn.show_remote_cursor);
|
||||
@@ -513,6 +541,18 @@ impl Connection {
|
||||
} else if &name == "file" {
|
||||
conn.file = enabled;
|
||||
conn.send_permission(Permission::File, enabled).await;
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if !enabled {
|
||||
conn.try_empty_file_clipboard();
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if let Some(s) = conn.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::FILE_NAME,
|
||||
conn.inner.clone(),
|
||||
conn.can_sub_file_clipboard_service(),
|
||||
);
|
||||
}
|
||||
} else if &name == "restart" {
|
||||
conn.restart = enabled;
|
||||
conn.send_permission(Permission::Restart, enabled).await;
|
||||
@@ -527,7 +567,7 @@ impl Connection {
|
||||
ipc::Data::RawMessage(bytes) => {
|
||||
allow_err!(conn.stream.send_raw(bytes).await);
|
||||
}
|
||||
#[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
ipc::Data::ClipboardFile(clip) => {
|
||||
allow_err!(conn.stream.send(&clip_2_msg(clip)).await);
|
||||
}
|
||||
@@ -740,9 +780,26 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
}
|
||||
clip_file = rx_clip.recv() => match clip_file {
|
||||
Some(_clip) => {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if crate::is_support_file_copy_paste(&conn.lr.version)
|
||||
{
|
||||
conn.handle_file_clip(_clip).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
//
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
{
|
||||
conn.try_empty_file_clipboard();
|
||||
}
|
||||
|
||||
if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() {
|
||||
if video_privacy_conn_id == id {
|
||||
let _ = Self::turn_off_privacy_to_msg(id);
|
||||
@@ -1202,15 +1259,20 @@ impl Connection {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
feature = "unix-file-copy-paste"
|
||||
)
|
||||
))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
{
|
||||
platform_additions.insert("has_file_clipboard".into(), json!(true));
|
||||
let is_both_windows = cfg!(target_os = "windows")
|
||||
&& self.lr.my_platform == whoami::Platform::Windows.to_string();
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version);
|
||||
#[cfg(not(feature = "unix-file-copy-paste"))]
|
||||
let is_unix_and_peer_supported = false;
|
||||
// to-do: add file clipboard support for macos
|
||||
let is_both_macos = cfg!(target_os = "macos")
|
||||
&& self.lr.my_platform == whoami::Platform::MacOS.to_string();
|
||||
let has_file_clipboard =
|
||||
is_both_windows || (is_unix_and_peer_supported && !is_both_macos);
|
||||
platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard));
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
@@ -1375,6 +1437,10 @@ impl Connection {
|
||||
if !self.can_sub_clipboard_service() {
|
||||
noperms.push(super::clipboard_service::NAME);
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if !self.can_sub_file_clipboard_service() {
|
||||
noperms.push(super::clipboard_service::FILE_NAME);
|
||||
}
|
||||
if !self.audio_enabled() {
|
||||
noperms.push(super::audio_service::NAME);
|
||||
}
|
||||
@@ -1455,11 +1521,18 @@ impl Connection {
|
||||
self.audio && !self.disable_audio
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
fn file_transfer_enabled(&self) -> bool {
|
||||
self.file && self.enable_file_transfer
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn can_sub_file_clipboard_service(&self) -> bool {
|
||||
self.clipboard_enabled()
|
||||
&& self.file_transfer_enabled()
|
||||
&& crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) != "Y"
|
||||
}
|
||||
|
||||
fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) {
|
||||
self.send_to_cm(ipc::Data::Login {
|
||||
id: self.inner.id(),
|
||||
@@ -2113,12 +2186,23 @@ impl Connection {
|
||||
#[cfg(target_os = "android")]
|
||||
crate::clipboard::handle_msg_multi_clipboards(_mcb);
|
||||
}
|
||||
Some(message::Union::Cliprdr(_clip)) =>
|
||||
{
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
if let Some(clip) = msg_2_clip(_clip) {
|
||||
log::debug!("got clipfile from client peer");
|
||||
self.send_to_cm(ipc::Data::ClipboardFile(clip))
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
Some(message::Union::Cliprdr(clip)) => {
|
||||
if let Some(clip) = msg_2_clip(clip) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.send_to_cm(ipc::Data::ClipboardFile(clip));
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if crate::is_support_file_copy_paste(&self.lr.version) {
|
||||
if let Some(msg) = unix_file_clip::serve_clip_messages(
|
||||
ClipboardSide::Host,
|
||||
clip,
|
||||
self.inner.id(),
|
||||
) {
|
||||
self.send(msg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::FileAction(fa)) => {
|
||||
@@ -2911,13 +2995,26 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
if let Ok(q) = o.enable_file_transfer.enum_value() {
|
||||
if q != BoolOption::NotSet {
|
||||
self.enable_file_transfer = q == BoolOption::Yes;
|
||||
#[cfg(target_os = "windows")]
|
||||
self.send_to_cm(ipc::Data::ClipboardFileEnabled(
|
||||
self.file_transfer_enabled(),
|
||||
));
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if !self.enable_file_transfer {
|
||||
self.try_empty_file_clipboard();
|
||||
}
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
if let Some(s) = self.server.upgrade() {
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::FILE_NAME,
|
||||
self.inner.clone(),
|
||||
self.can_sub_file_clipboard_service(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(q) = o.disable_clipboard.enum_value() {
|
||||
@@ -2941,6 +3038,12 @@ impl Connection {
|
||||
self.inner.clone(),
|
||||
self.can_sub_clipboard_service(),
|
||||
);
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
s.write().unwrap().subscribe(
|
||||
super::clipboard_service::FILE_NAME,
|
||||
self.inner.clone(),
|
||||
self.can_sub_file_clipboard_service(),
|
||||
);
|
||||
s.write().unwrap().subscribe(
|
||||
NAME_CURSOR,
|
||||
self.inner.clone(),
|
||||
@@ -3330,6 +3433,41 @@ impl Connection {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) {
|
||||
let is_stopping_allowed = clip.is_stopping_allowed();
|
||||
let is_keyboard_enabled = self.peer_keyboard_enabled();
|
||||
let file_transfer_enabled = self.file_transfer_enabled();
|
||||
let stop = is_stopping_allowed && !file_transfer_enabled;
|
||||
log::debug!(
|
||||
"Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}",
|
||||
stop, is_stopping_allowed, file_transfer_enabled);
|
||||
if !stop {
|
||||
use hbb_common::config::keys::OPTION_ONE_WAY_FILE_TRANSFER;
|
||||
// Note: Code will not reach here if `crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"` is true.
|
||||
// Because `file-clipboard` service will not be subscribed.
|
||||
// But we still check it here to keep the same logic to windows version in `ui_cm_interface.rs`.
|
||||
if clip.is_beginning_message()
|
||||
&& crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"
|
||||
{
|
||||
// If one way file transfer is enabled, don't send clipboard file to client
|
||||
} else {
|
||||
// Maybe we should end the connection, because copy&paste files causes everything to wait.
|
||||
allow_err!(
|
||||
self.stream
|
||||
.send(&crate::clipboard_file::clip_2_msg(clip))
|
||||
.await
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn try_empty_file_clipboard(&mut self) {
|
||||
try_empty_clipboard_files(ClipboardSide::Host, self.inner.id());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
||||
|
||||
@@ -21,12 +21,6 @@ pub fn init() {
|
||||
}
|
||||
|
||||
fn map_err_scrap(err: String) -> io::Error {
|
||||
// to-do: Remove this the following log
|
||||
log::error!(
|
||||
"REMOVE ME ===================================== wayland scrap error {}",
|
||||
&err
|
||||
);
|
||||
|
||||
// to-do: Handle error better, do not restart server
|
||||
if err.starts_with("Did not receive a reply") {
|
||||
log::error!("Fatal pipewire error, {}", &err);
|
||||
|
||||
@@ -174,6 +174,13 @@ class Header: Reactor.Component {
|
||||
}
|
||||
}
|
||||
|
||||
var is_file_copy_paste_supported = false;
|
||||
if (handler.version_cmp(pi.version, '1.2.4') < 0) {
|
||||
is_file_copy_paste_supported = is_win && pi.platform == "Windows";
|
||||
} else {
|
||||
is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions.has_file_clipboard;
|
||||
}
|
||||
|
||||
return <popup>
|
||||
<menu.context #display-options>
|
||||
<li #adjust-window style="display:none">{translate('Adjust Window')}</li>
|
||||
@@ -201,7 +208,7 @@ class Header: Reactor.Component {
|
||||
{<li #follow-remote-window .toggle-option><span>{svg_checkmark}</span>{translate('Follow remote window focus')}</li>}
|
||||
<li #show-quality-monitor .toggle-option><span>{svg_checkmark}</span>{translate('Show quality monitor')}</li>
|
||||
{audio_enabled ? <li #disable-audio .toggle-option><span>{svg_checkmark}</span>{translate('Mute')}</li> : ""}
|
||||
{(is_win && pi.platform == "Windows") && file_enabled ? <li #enable-file-copy-paste .toggle-option><span>{svg_checkmark}</span>{translate('Enable file copy and paste')}</li> : ""}
|
||||
{is_file_copy_paste_supported && file_enabled ? <li #enable-file-copy-paste .toggle-option><span>{svg_checkmark}</span>{translate('Enable file copy and paste')}</li> : ""}
|
||||
{keyboard_enabled && clipboard_enabled ? <li #disable-clipboard .toggle-option><span>{svg_checkmark}</span>{translate('Disable clipboard')}</li> : ""}
|
||||
{keyboard_enabled ? <li #lock-after-session-end .toggle-option><span>{svg_checkmark}</span>{translate('Lock after session end')}</li> : ""}
|
||||
{keyboard_enabled && pi.platform == "Windows" ? <li #privacy-mode><span>{svg_checkmark}</span>{translate('Privacy mode')}</li> : ""}
|
||||
|
||||
@@ -66,6 +66,39 @@ impl SciterHandler {
|
||||
}
|
||||
displays_value
|
||||
}
|
||||
|
||||
fn make_platform_additions(data: &str) -> Option<Value> {
|
||||
if let Ok(v2) = serde_json::from_str::<HashMap<String, serde_json::Value>>(data) {
|
||||
let mut value = Value::map();
|
||||
for (k, v) in v2 {
|
||||
match v {
|
||||
serde_json::Value::String(s) => {
|
||||
value.set_item(k, s);
|
||||
}
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(n) = n.as_i64() {
|
||||
value.set_item(k, n as i32);
|
||||
} else if let Some(n) = n.as_f64() {
|
||||
value.set_item(k, n);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Bool(b) => {
|
||||
value.set_item(k, b);
|
||||
}
|
||||
_ => {
|
||||
// ignore for now
|
||||
}
|
||||
}
|
||||
}
|
||||
if value.len() > 0 {
|
||||
return Some(value);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InvokeUiSession for SciterHandler {
|
||||
@@ -245,6 +278,9 @@ impl InvokeUiSession for SciterHandler {
|
||||
pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays));
|
||||
pi_sciter.set_item("current_display", pi.current_display);
|
||||
pi_sciter.set_item("version", pi.version.clone());
|
||||
if let Some(v) = Self::make_platform_additions(&pi.platform_additions) {
|
||||
pi_sciter.set_item("platform_additions", v);
|
||||
}
|
||||
self.call("updatePi", &make_args!(pi_sciter));
|
||||
}
|
||||
|
||||
@@ -500,6 +536,7 @@ impl sciter::EventHandler for SciterSession {
|
||||
fn version_cmp(String, String);
|
||||
fn set_selected_windows_session_id(String);
|
||||
fn is_recording();
|
||||
fn has_file_clipboard();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +644,10 @@ impl SciterSession {
|
||||
self.send_selected_session_id(u_sid);
|
||||
}
|
||||
|
||||
fn has_file_clipboard(&self) -> bool {
|
||||
cfg!(any(target_os = "windows", feature = "unix-file-copy-paste"))
|
||||
}
|
||||
|
||||
fn get_port_forwards(&mut self) -> Value {
|
||||
let port_forwards = self.lc.read().unwrap().port_forwards.clone();
|
||||
let mut v = Value::array(0);
|
||||
|
||||
@@ -3,8 +3,11 @@ use crate::ipc::ClipboardNonFile;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::ipc::Connection;
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
use crate::ipc::{self, Data};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
use crate::{
|
||||
clipboard::ClipboardSide,
|
||||
ipc::{self, Data},
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use clipboard::ContextSend;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::tokio::sync::mpsc::unbounded_channel;
|
||||
@@ -71,9 +74,9 @@ struct IpcTaskRunner<T: InvokeUiCM> {
|
||||
close: bool,
|
||||
running: bool,
|
||||
conn_id: i32,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
file_transfer_enabled: bool,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
file_transfer_enabled_peer: bool,
|
||||
}
|
||||
|
||||
@@ -169,7 +172,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn is_authorized(&self, id: i32) -> bool {
|
||||
CLIENTS
|
||||
.read()
|
||||
@@ -190,12 +193,9 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
.map(|c| c.disconnected = true);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.empty_clipboard(id)?;
|
||||
Ok(())
|
||||
});
|
||||
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Host, id);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android"))]
|
||||
@@ -345,31 +345,40 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
// for tmp use, without real conn id
|
||||
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
let is_authorized = self.cm.is_authorized(self.conn_id);
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let rx_clip1;
|
||||
#[cfg(target_os = "windows")]
|
||||
let rx_clip_holder;
|
||||
let mut rx_clip;
|
||||
let _tx_clip;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.conn_id > 0 && is_authorized {
|
||||
log::debug!("Clipboard is enabled from client peer: type 1");
|
||||
rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id);
|
||||
rx_clip = rx_clip1.lock().await;
|
||||
let conn_id = self.conn_id;
|
||||
rx_clip_holder = (
|
||||
clipboard::get_rx_cliprdr_server(conn_id),
|
||||
Some(crate::SimpleCallOnReturn {
|
||||
b: true,
|
||||
f: Box::new(move || {
|
||||
clipboard::remove_channel_by_conn_id(conn_id);
|
||||
}),
|
||||
}),
|
||||
);
|
||||
rx_clip = rx_clip_holder.0.lock().await;
|
||||
} else {
|
||||
log::debug!("Clipboard is enabled from client peer, actually useless: type 2");
|
||||
let rx_clip2;
|
||||
(_tx_clip, rx_clip2) = unbounded_channel::<clipboard::ClipboardFile>();
|
||||
rx_clip1 = Arc::new(TokioMutex::new(rx_clip2));
|
||||
rx_clip = rx_clip1.lock().await;
|
||||
rx_clip_holder = (Arc::new(TokioMutex::new(rx_clip2)), None);
|
||||
rx_clip = rx_clip_holder.0.lock().await;
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
(_tx_clip, rx_clip) = unbounded_channel::<i32>();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if ContextSend::is_enabled() {
|
||||
log::debug!("Clipboard is enabled");
|
||||
@@ -397,7 +406,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
log::debug!("conn_id: {}", id);
|
||||
self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
|
||||
self.conn_id = id;
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.file_transfer_enabled = _file_transfer_enabled;
|
||||
}
|
||||
@@ -438,34 +447,31 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
Data::FileTransferLog((action, log)) => {
|
||||
self.cm.ui_handler.file_transfer_log(&action, &log);
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[cfg(target_os = "windows")]
|
||||
Data::ClipboardFile(_clip) => {
|
||||
#[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))]
|
||||
{
|
||||
let is_stopping_allowed = _clip.is_beginning_message();
|
||||
let is_clipboard_enabled = ContextSend::is_enabled();
|
||||
let file_transfer_enabled = self.file_transfer_enabled;
|
||||
let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled);
|
||||
log::debug!(
|
||||
"Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}",
|
||||
stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled);
|
||||
if stop {
|
||||
ContextSend::set_is_stopped();
|
||||
} else {
|
||||
if !is_authorized {
|
||||
log::debug!("Clipboard message from client peer, but not authorized");
|
||||
continue;
|
||||
}
|
||||
let conn_id = self.conn_id;
|
||||
let _ = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.server_clip_file(conn_id, _clip)
|
||||
.map_err(|e| e.into())
|
||||
});
|
||||
let is_stopping_allowed = _clip.is_beginning_message();
|
||||
let is_clipboard_enabled = ContextSend::is_enabled();
|
||||
let file_transfer_enabled = self.file_transfer_enabled;
|
||||
let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled);
|
||||
log::debug!(
|
||||
"Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}",
|
||||
stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled);
|
||||
if stop {
|
||||
ContextSend::set_is_stopped();
|
||||
} else {
|
||||
if !is_authorized {
|
||||
log::debug!("Clipboard message from client peer, but not authorized");
|
||||
continue;
|
||||
}
|
||||
let conn_id = self.conn_id;
|
||||
let _ = ContextSend::proc(|context| -> ResultType<()> {
|
||||
context.server_clip_file(conn_id, _clip)
|
||||
.map_err(|e| e.into())
|
||||
});
|
||||
}
|
||||
}
|
||||
Data::ClipboardFileEnabled(_enabled) => {
|
||||
#[cfg(any(target_os= "windows",target_os ="linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.file_transfer_enabled_peer = _enabled;
|
||||
}
|
||||
@@ -543,7 +549,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
}
|
||||
match &data {
|
||||
Data::SwitchPermission{name: _name, enabled: _enabled} => {
|
||||
#[cfg(any(target_os="linux", target_os="windows", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
if _name == "file" {
|
||||
self.file_transfer_enabled = *_enabled;
|
||||
}
|
||||
@@ -558,7 +564,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
},
|
||||
clip_file = rx_clip.recv() => match clip_file {
|
||||
Some(_clip) => {
|
||||
#[cfg(any(target_os = "windows", target_os ="linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let is_stopping_allowed = _clip.is_stopping_allowed();
|
||||
let is_clipboard_enabled = ContextSend::is_enabled();
|
||||
@@ -602,9 +608,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
close: true,
|
||||
running: true,
|
||||
conn_id: 0,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
file_transfer_enabled: false,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
file_transfer_enabled_peer: false,
|
||||
};
|
||||
|
||||
@@ -623,13 +629,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
feature = "unix-file-copy-paste"
|
||||
),
|
||||
))]
|
||||
#[cfg(target_os = "windows")]
|
||||
ContextSend::enable(option2bool(
|
||||
OPTION_ENABLE_FILE_TRANSFER,
|
||||
&Config::get_option(OPTION_ENABLE_FILE_TRANSFER),
|
||||
|
||||
@@ -23,7 +23,6 @@ use serde_derive::Serialize;
|
||||
use std::process::Child;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
@@ -213,6 +212,7 @@ pub fn get_local_option(key: String) -> String {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "flutter")]
|
||||
pub fn get_hard_option(key: String) -> String {
|
||||
config::HARD_SETTINGS
|
||||
.read()
|
||||
@@ -491,6 +491,7 @@ pub fn set_socks(proxy: String, username: String, password: String) {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "flutter")]
|
||||
pub fn get_proxy_status() -> bool {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return ipc::get_proxy_status();
|
||||
@@ -1150,13 +1151,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc:
|
||||
let mut video_conn_count = 0;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let mut id = "".to_owned();
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
feature = "unix-file-copy-paste"
|
||||
)
|
||||
))]
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut enable_file_transfer = "".to_owned();
|
||||
let is_cm = crate::common::is_cm();
|
||||
|
||||
@@ -1183,13 +1178,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc:
|
||||
*OPTIONS.lock().unwrap() = v;
|
||||
*OPTION_SYNCED.lock().unwrap() = true;
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os="linux", target_os = "macos"),
|
||||
feature = "unix-file-copy-paste"
|
||||
)
|
||||
))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let b = OPTIONS.lock().unwrap().get(OPTION_ENABLE_FILE_TRANSFER).map(|x| x.to_string()).unwrap_or_default();
|
||||
if b != enable_file_transfer {
|
||||
|
||||
@@ -162,6 +162,13 @@ impl SessionPermissionConfig {
|
||||
&& *self.server_keyboard_enabled.read().unwrap()
|
||||
&& !self.lc.read().unwrap().disable_clipboard.v
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
pub fn is_file_clipboard_required(&self) -> bool {
|
||||
*self.server_keyboard_enabled.read().unwrap()
|
||||
&& *self.server_file_transfer_enabled.read().unwrap()
|
||||
&& self.lc.read().unwrap().enable_file_copy_paste.v
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: InvokeUiSession> Session<T> {
|
||||
@@ -324,7 +331,7 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
|
||||
pub fn toggle_option(&self, name: String) {
|
||||
let msg = self.lc.write().unwrap().toggle_option(name.clone());
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
if name == hbb_common::config::keys::OPTION_ENABLE_FILE_COPY_PASTE {
|
||||
self.send(Data::ToggleClipboardFile);
|
||||
}
|
||||
@@ -361,6 +368,13 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
&& !self.lc.read().unwrap().disable_clipboard.v
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
pub fn is_file_clipboard_required(&self) -> bool {
|
||||
*self.server_keyboard_enabled.read().unwrap()
|
||||
&& *self.server_file_transfer_enabled.read().unwrap()
|
||||
&& self.lc.read().unwrap().enable_file_copy_paste.v
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
pub fn refresh_video(&self, display: i32) {
|
||||
if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) {
|
||||
|
||||
Reference in New Issue
Block a user