From 541d9c6b86fb2cb649659106eacb982c7c61357f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 28 Jul 2024 17:26:54 +0800 Subject: [PATCH] feat: clipboard, multi formats (#8733) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 322 ++-------------- Cargo.toml | 4 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/clipboard/src/platform/mod.rs | 3 + libs/hbb_common/protos/message.proto | 14 + libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- src/client.rs | 177 ++++++--- src/client/io_loop.rs | 46 ++- src/clipboard.rs | 556 +++++++++++++++------------ src/flutter.rs | 13 + src/server/clipboard_service.rs | 98 +++-- src/server/connection.rs | 22 +- 20 files changed, 622 insertions(+), 653 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index e4bf8eaf6..0e2b45063 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,7 +33,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.2.7" + VERSION: "1.3.0" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index d788858ab..3fdcc4cfe 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.2.7" + VERSION: "1.3.0" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index ee757b7f6..43b8ed497 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#27b4e503caa70ec6306e5270461429f2cf907ad6" +source = "git+https://github.com/rustdesk-org/arboard#75166f255bf2fd6c662269029f5130b11d024f46" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,24 +234,11 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", - "resvg", "windows-sys 0.48.0", "wl-clipboard-rs", "x11rb 0.13.1", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "async-broadcast" version = "0.5.1" @@ -987,11 +974,14 @@ dependencies = [ [[package]] name = "clipboard-master" version = "4.0.0-beta.6" -source = "git+https://github.com/rustdesk-org/clipboard-master#5268c7b3d7728699566ad863da0911f249706f8c" +source = "git+https://github.com/rustdesk-org/clipboard-master#4fb62e5b62fb6350d82b571ec7ba94b3cd466695" dependencies = [ "objc", "objc-foundation", "objc_id", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", "windows-win", "wl-clipboard-rs", "x11-clipboard 0.9.2", @@ -1000,9 +990,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -1526,12 +1516,6 @@ dependencies = [ "dasp_sample", ] -[[package]] -name = "data-url" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" - [[package]] name = "dbus" version = "0.9.7" @@ -2081,12 +2065,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "flume" version = "0.11.0" @@ -2143,29 +2121,6 @@ dependencies = [ "libm", ] -[[package]] -name = "fontconfig-parser" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" -dependencies = [ - "roxmltree 0.19.0", -] - -[[package]] -name = "fontdb" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - [[package]] name = "foreign-types" version = "0.3.2" @@ -3213,12 +3168,6 @@ dependencies = [ "tiff", ] -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - [[package]] name = "impersonate_system" version = "0.1.0" @@ -3462,16 +3411,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kurbo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" -dependencies = [ - "arrayvec", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3777,15 +3716,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.6.5" @@ -4732,15 +4662,9 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher 0.2.3", + "siphasher", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.5" @@ -5065,6 +4989,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "0.6.13" @@ -5414,31 +5347,6 @@ dependencies = [ "winreg 0.50.0", ] -[[package]] -name = "resvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" -dependencies = [ - "gif", - "jpeg-decoder", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - -[[package]] -name = "rgb" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7439be6844e40133eda024efd85bf07f59d0dd2f59b10c00dd6cfb92cc5c741" -dependencies = [ - "bytemuck", -] - [[package]] name = "ring" version = "0.17.8" @@ -5463,18 +5371,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "roxmltree" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rpassword" version = "2.1.0" @@ -5572,7 +5468,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.2.7" +version = "1.3.0" dependencies = [ "android-wakelock", "android_logger", @@ -5670,7 +5566,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.2.7" +version = "1.3.0" dependencies = [ "brotli", "dirs 5.0.1", @@ -5853,22 +5749,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "rustybuzz" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" -dependencies = [ - "bitflags 2.6.0", - "bytemuck", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "ryu" version = "1.0.18" @@ -6159,27 +6039,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - [[package]] name = "siphasher" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -6189,15 +6054,6 @@ dependencies = [ "autocfg 1.3.0", ] -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -6268,15 +6124,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - [[package]] name = "strsim" version = "0.8.0" @@ -6338,16 +6185,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "svgtypes" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae3064df9b89391c9a76a0425a69d124aee9c5c28455204709e72c39868a43c" -dependencies = [ - "kurbo", - "siphasher 1.0.1", -] - [[package]] name = "syn" version = "0.15.44" @@ -6687,32 +6524,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if 1.0.0", - "log", - "png", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinyvec" version = "1.6.1" @@ -7004,12 +6815,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "typenum" version = "1.17.0" @@ -7082,18 +6887,6 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" - -[[package]] -name = "unicode-ccc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -7109,30 +6902,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode-script" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" - [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.1.13" @@ -7195,33 +6970,6 @@ dependencies = [ "log", ] -[[package]] -name = "usvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree 0.20.0", - "rustybuzz", - "simplecss", - "siphasher 1.0.1", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - [[package]] name = "utf16string" version = "0.2.0" @@ -7414,9 +7162,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" +checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" dependencies = [ "cc", "downcast-rs", @@ -7428,9 +7176,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.3" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" +checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ "bitflags 2.6.0", "rustix 0.38.34", @@ -7440,9 +7188,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d0f1056570486e26a3773ec633885124d79ae03827de05ba6c85f79904026c" +checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -7452,9 +7200,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dab47671043d9f5397035975fe1cac639e5bca5cc0b3c32d09f01612e34d24" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -7465,20 +7213,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" +checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ "proc-macro2 1.0.86", - "quick-xml 0.31.0", + "quick-xml 0.34.0", "quote 1.0.36", ] [[package]] name = "wayland-sys" -version = "0.31.2" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" +checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" dependencies = [ "dlib", "log", @@ -8220,12 +7968,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "zbus" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index 85b2b5401..c1a841358 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.2.7" +version = "1.3.0" authors = ["rustdesk "] edition = "2021" build= "build.rs" @@ -90,7 +90,7 @@ enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } ctrlc = "3.2" # arboard = { version = "3.4.0", features = ["wayland-data-control"] } -arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control", "image-data"] } +arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } system_shutdown = "4.0" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 411d7bb57..c64966f28 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.2.7 + version: 1.3.0 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9c6860dd4..3c0479d96 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.2.7 + version: 1.3.0 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c6e1e61cc..a0a24d54d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -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.2.7+46 +version: 1.3.0+46 environment: sdk: '^3.1.0' diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 2be4ce809..5db271129 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::{CliprdrError, CliprdrServiceContext}; #[cfg(target_os = "windows")] @@ -63,8 +64,10 @@ pub fn create_cliprdr_context( 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(()) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index f346a7228..d7a8cf0a7 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -81,6 +81,7 @@ message LoginRequest { uint64 session_id = 10; string version = 11; OSLogin os_login = 12; + string my_platform = 13; } message Auth2FA { @@ -315,13 +316,25 @@ message Hash { string challenge = 2; } +enum ClipboardFormat { + Text = 0; + Rtf = 1; + Html = 2; + ImageRgba = 21; + ImagePng = 22; + ImageSvg = 23; +} + message Clipboard { bool compress = 1; bytes content = 2; int32 width = 3; int32 height = 4; + ClipboardFormat format = 5; } +message MultiClipboards { repeated Clipboard clipboards = 1; } + enum FileType { Dir = 0; DirLink = 2; @@ -816,5 +829,6 @@ message Message { PeerInfo peer_info = 25; PointerDeviceEvent pointer_device_event = 26; Auth2FA auth_2fa = 27; + MultiClipboards multi_clipboards = 28; } } diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index e39212bf2..ce1c10c09 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.2.7" +version = "1.3.0" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 6f1b4e680..94ccce693 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.2.7 +pkgver=1.3.0 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index c4fe69e67..053099c07 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 90e45af9c..f962a2ed1 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index a6d6a956a..633c2a220 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.2.7 +Version: 1.3.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/src/client.rs b/src/client.rs index e5823e187..6fc8ce1a6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,14 +1,7 @@ -use std::{ - collections::HashMap, - ffi::c_void, - net::SocketAddr, - ops::Deref, - str::FromStr, - sync::{mpsc, Arc, Mutex, RwLock}, -}; - use async_trait::async_trait; use bytes::Bytes; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use clipboard_master::{CallbackResult, ClipboardHandler}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -19,6 +12,18 @@ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use ringbuf::{ring_buffer::RbBase, Rb}; use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + ffi::c_void, + io, + net::SocketAddr, + ops::Deref, + str::FromStr, + sync::{ + mpsc::{self, RecvTimeoutError, Sender}, + Arc, Mutex, RwLock, + }, +}; use uuid::Uuid; pub use file_trait::FileManager; @@ -65,7 +70,7 @@ use crate::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL}; +use crate::clipboard::{check_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; @@ -136,18 +141,11 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); - static ref OLD_CLIPBOARD_DATA: Arc> = Default::default(); static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_old_clipboard_text() -> Arc> { - OLD_CLIPBOARD_DATA.clone() -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_key_state(key: enigo::Key) -> bool { use enigo::KeyboardControllable; @@ -714,6 +712,13 @@ impl Client { #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_stop_clipboard() { + // There's a bug here. + // If session is closed by the peer, `has_sessions_running()` will always return true. + // It's better to check if the active session number. + // But it's not a problem, because the clipboard thread does not consume CPU. + // + // If we want to fix it, we can add a flag to indicate if session is active. + // But I think it's not necessary to introduce complexity at this point. #[cfg(feature = "flutter")] if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; @@ -727,18 +732,44 @@ impl Client { // // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn try_start_clipboard(_ctx: Option) -> Option> { + fn try_start_clipboard( + _client_clip_ctx: Option, + ) -> Option> { let mut clipboard_lock = TEXT_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; + } + }; + clipboard_lock.running = true; - let (tx, rx) = unbounded_channel(); + + let (tx_started, rx_started) = unbounded_channel(); log::info!("Start text clipboard loop"); std::thread::spawn(move || { let mut is_sent = false; - let mut ctx = None; + loop { if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { break; @@ -748,39 +779,31 @@ impl Client { continue; } - if let Some(msg) = check_clipboard(&mut ctx, Some(OLD_CLIPBOARD_DATA.clone())) { - #[cfg(feature = "flutter")] - crate::flutter::send_text_clipboard_msg(msg); - #[cfg(not(feature = "flutter"))] - if let Some(ctx) = &_ctx { - if ctx.cfg.is_text_clipboard_required() { - let _ = ctx.tx.send(Data::Message(msg)); - } - } - } - if !is_sent { is_sent = true; - tx.send(()).ok(); + tx_started.send(()).ok(); } - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + log::error!("Clipboard listener stopped with error: {}", err); + break; + } + Err(RecvTimeoutError::Timeout) => {} + _ => {} + } } log::info!("Stop text clipboard loop"); + shutdown.signal(); + h.join().ok(); + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; }); - Some(rx) - } - - #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn get_current_clipboard_msg() -> Option { - let data = &*OLD_CLIPBOARD_DATA.lock().unwrap(); - if data.is_empty() { - None - } else { - Some(data.create_msg()) - } + Some(rx_started) } } @@ -794,6 +817,65 @@ impl TextClipboardState { } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct ClientClipboardHandler { + ctx: Option, + tx_cb_result: Sender, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: Option, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl ClientClipboardHandler { + #[inline] + #[cfg(feature = "flutter")] + fn send_msg(&self, msg: Message) { + crate::flutter::send_text_clipboard_msg(msg); + } + + #[cfg(not(feature = "flutter"))] + fn send_msg(&self, msg: Message) { + 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; + } + } + } + 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 { @@ -2031,11 +2113,16 @@ impl LoginConfigHandler { if display_name.is_empty() { display_name = crate::username(); } + #[cfg(not(target_os = "android"))] + let my_platform = whoami::platform().to_string(); + #[cfg(target_os = "android")] + let my_platform = "Android".into(); let mut lr = LoginRequest { username: pure_id, password: password.into(), my_id, my_name: display_name, + my_platform, option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1280fb801..a182a5fe0 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -7,11 +7,21 @@ use std::{ }, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide, CLIPBOARD_INTERVAL}; +#[cfg(not(any(target_os = "ios")))] +use crate::{audio_service, ConnInner, CLIENT_SERVER}; +use crate::{ + client::{ + self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, + }, + common::get_default_sound_input, + ui_session_interface::{InvokeUiSession, Session}, +}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::sleep; #[cfg(not(target_os = "ios"))] use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ @@ -37,17 +47,6 @@ use hbb_common::{ use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use scrap::CodecFormat; -use crate::client::{ - self, new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, -}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL}; -use crate::common::get_default_sound_input; -use crate::ui_session_interface::{InvokeUiSession, Session}; -#[cfg(not(any(target_os = "ios")))] -use crate::{audio_service, ConnInner, CLIENT_SERVER}; -use crate::{client::Data, client::Interface}; - pub struct Remote { handler: Session, video_queue_map: Arc>>>, @@ -173,8 +172,7 @@ impl Remote { crate::rustdesk_interval(time::interval(Duration::new(1, 0))); let mut fps_instant = Instant::now(); - let _keep_it = - client::hc_connection(feedback, rendezvous_server, token).await; + let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; loop { tokio::select! { @@ -1123,6 +1121,8 @@ impl Remote { } } Some(login_response::Union::PeerInfo(pi)) => { + let peer_version = pi.version.clone(); + let peer_platform = pi.platform.clone(); self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { @@ -1144,12 +1144,14 @@ impl Remote { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg_out) = Client::get_current_clipboard_msg() { + if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) { let sender = self.sender.clone(); let permission_config = self.handler.get_permission_config(); tokio::spawn(async move { - // due to clipboard service interval time - sleep(CLIPBOARD_INTERVAL as f32 / 1_000.).await; if permission_config.is_text_clipboard_required() { sender.send(Data::Message(msg_out)).ok(); } @@ -1185,7 +1187,7 @@ impl Remote { Some(message::Union::Clipboard(cb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, Some(crate::client::get_old_clipboard_text())); + update_clipboard(vec![cb], ClipboardSide::Client); #[cfg(any(target_os = "android", target_os = "ios"))] { let content = if cb.compress { @@ -1199,6 +1201,12 @@ impl Remote { } } } + Some(message::Union::MultiClipboards(_mcb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard.v { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(_mcb.clipboards, ClipboardSide::Client); + } + } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] Some(message::Union::Cliprdr(clip)) => { self.handle_cliprdr_msg(clip); diff --git a/src/clipboard.rs b/src/clipboard.rs index 0db9f59c1..fe30189ca 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,26 +1,35 @@ -use std::sync::{ - atomic::{AtomicU64, Ordering}, - Arc, Mutex, -}; - -use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; -use hbb_common::{ - allow_err, - compress::{compress as compress_func, decompress}, - log, - message_proto::*, - ResultType, +use arboard::{ClipboardData, ClipboardFormat}; +use clipboard_master::{ClipboardHandler, Master, Shutdown}; +use hbb_common::{log, message_proto::*, ResultType}; +use std::{ + sync::{mpsc::Sender, Arc, Mutex}, + thread, + thread::JoinHandle, + time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; -const FAKE_SVG_WIDTH: usize = 999999; + +// This format is used to store the flag in the clipboard. +const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; lazy_static::lazy_static! { - pub static ref CONTENT: Arc> = Default::default(); static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); + // cache the clipboard msg + static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); } +const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ + ClipboardFormat::Text, + ClipboardFormat::Html, + ClipboardFormat::Rtf, + ClipboardFormat::ImageRgba, + ClipboardFormat::ImagePng, + ClipboardFormat::ImageSvg, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), +]; + #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] static X11_CLIPBOARD: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -61,7 +70,7 @@ fn parse_plain_uri_list(v: Vec) -> Result { #[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] impl ClipboardContext { - pub fn new(_listen: bool) -> Result { + pub fn new() -> Result { let clipboard = get_clipboard()?; let string_getter = clipboard .getter @@ -128,56 +137,42 @@ impl ClipboardContext { pub fn check_clipboard( ctx: &mut Option, - old: Option>>, + side: ClipboardSide, + force: bool, ) -> Option { if ctx.is_none() { - *ctx = ClipboardContext::new(true).ok(); + *ctx = ClipboardContext::new().ok(); } let ctx2 = ctx.as_mut()?; - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - let content = ctx2.get(); + let content = ctx2.get(side, force); if let Ok(content) = content { if !content.is_empty() { - if matches!(content, ClipboardData::Text(_)) { - // Skip the text if the last content is image-svg/html - if ctx2.is_last_plain { - return None; - } - } - - let changed = content != *old.lock().unwrap(); - if changed { - log::info!("{} update found on {}", CLIPBOARD_NAME, side); - let msg = content.create_msg(); - *old.lock().unwrap() = content; - return Some(msg); - } + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + return Some(msg); } } None } -fn update_clipboard_(clipboard: Clipboard, old: Option>>) { - let content = ClipboardData::from_msg(clipboard); - if content.is_empty() { +fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { + let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); + if to_update_data.is_empty() { return; } - match ClipboardContext::new(false) { + match ClipboardContext::new() { Ok(mut ctx) => { - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old + to_update_data.push(ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + ))); + if let Err(e) = ctx.set(&to_update_data) { + log::debug!("Failed to set clipboard: {}", e); } else { - CONTENT.clone() - }; - allow_err!(ctx.set(&content)); - *old.lock().unwrap() = content; - log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } } Err(err) => { log::error!("Failed to create clipboard context: {}", err); @@ -185,143 +180,21 @@ fn update_clipboard_(clipboard: Clipboard, old: Option> } } -pub fn update_clipboard(clipboard: Clipboard, old: Option>>) { +pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { std::thread::spawn(move || { - update_clipboard_(clipboard, old); + update_clipboard_(multi_clipboards, side); }); } -#[derive(Clone)] -pub enum ClipboardData { - Text(String), - Image(arboard::ImageData<'static>, u64), - Empty, -} - -impl Default for ClipboardData { - fn default() -> Self { - ClipboardData::Empty - } -} - -impl ClipboardData { - fn image(image: arboard::ImageData<'static>) -> ClipboardData { - let hash = 0; - /* - use std::hash::{DefaultHasher, Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - image.bytes.hash(&mut hasher); - let hash = hasher.finish(); - */ - ClipboardData::Image(image, hash) - } - - pub fn is_empty(&self) -> bool { - match self { - ClipboardData::Empty => true, - ClipboardData::Text(s) => s.is_empty(), - ClipboardData::Image(a, _) => a.bytes().is_empty(), - } - } - - fn from_msg(clipboard: Clipboard) -> Self { - let is_image = clipboard.width > 0; - let data = if clipboard.compress { - decompress(&clipboard.content) - } else { - clipboard.content.into() - }; - if is_image { - // We cannot use data.start_with(b" Message { - let mut msg = Message::new(); - - match self { - ClipboardData::Text(s) => { - let compressed = compress_func(s.as_bytes()); - let compress = compressed.len() < s.as_bytes().len(); - let content = if compress { - compressed - } else { - s.clone().into_bytes() - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - ..Default::default() - }); - } - ClipboardData::Image(a, _) => { - let compressed = compress_func(&a.bytes()); - let compress = compressed.len() < a.bytes().len(); - let content = if compress { - compressed - } else { - a.bytes().to_vec() - }; - let (w, h) = match a { - arboard::ImageData::Rgba(a) => (a.width, a.height), - arboard::ImageData::Svg(_) => (FAKE_SVG_WIDTH as _, 0 as _), - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - width: w as _, - height: h as _, - ..Default::default() - }); - } - _ => {} - } - msg - } -} - -impl PartialEq for ClipboardData { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (ClipboardData::Text(a), ClipboardData::Text(b)) => a == b, - (ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => match (a, b) { - (arboard::ImageData::Rgba(a), arboard::ImageData::Rgba(b)) => { - a.width == b.width && a.height == b.height && a.bytes == b.bytes - } - (arboard::ImageData::Svg(a), arboard::ImageData::Svg(b)) => a == b, - _ => false, - }, - (ClipboardData::Empty, ClipboardData::Empty) => true, - _ => false, - } - } -} - #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, - counter: (Arc, u64), - shutdown: Option, - is_last_plain: bool, } #[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] #[allow(unreachable_code)] impl ClipboardContext { - pub fn new(listen: bool) -> ResultType { + pub fn new() -> ResultType { let board; #[cfg(not(target_os = "linux"))] { @@ -351,94 +224,275 @@ impl ClipboardContext { } } - // starting from 1 so that we can always get initial clipboard data no matter if change - let change_count: Arc = Arc::new(AtomicU64::new(1)); - let mut shutdown = None; - if listen { - struct Handler(Arc); - impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - self.0.fetch_add(1, Ordering::SeqCst); - CallbackResult::Next - } + Ok(ClipboardContext { inner: board }) + } - fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult { - log::trace!("Error of clipboard listener: {}", error); - CallbackResult::Next - } - } - let change_count_cloned = change_count.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. - std::thread::spawn(move || match Master::new(Handler(change_count_cloned)) { - Ok(mut master) => { - tx.send(master.shutdown_channel()).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"); + pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let _lock = ARBOARD_MTX.lock().unwrap(); + let data = self.inner.get_formats(SUPPORTED_FORMATS)?; + if data.is_empty() { + return Ok(data); + } + if !force { + for c in data.iter() { + if let ClipboardData::Special((_, d)) = c { + if side.is_owner(d) { + return Ok(vec![]); } } - Err(err) => { - log::error!("Failed to create clipboard listener: {}", err); - } - }); - if let Ok(st) = rx.recv() { - shutdown = Some(st); } } - Ok(ClipboardContext { - inner: board, - counter: (change_count, 0), - shutdown, - is_last_plain: false, - }) + Ok(data + .into_iter() + .filter(|c| !matches!(c, ClipboardData::Special(_))) + .collect()) } - #[inline] - pub fn change_count(&self) -> u64 { - debug_assert!(self.shutdown.is_some()); - self.counter.0.load(Ordering::SeqCst) - } - - pub fn get(&mut self) -> ResultType { - let cn = self.change_count(); + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { let _lock = ARBOARD_MTX.lock().unwrap(); - // only for image for the time being, - // because I do not want to change behavior of text clipboard for the time being - if cn != self.counter.1 { - self.is_last_plain = false; - self.counter.1 = cn; - if let Ok(image) = self.inner.get_image() { - // Both text and image svg may be set by some applications - // But we only want to send the svg content. - // - // We can't call `get_text()` and store current text in `old` in outer scope, - // because it may be updated later than svg. - // Then the text will still be sent and replace the image svg content. - self.is_last_plain = matches!(image, arboard::ImageData::Svg(_)); - return Ok(ClipboardData::image(image)); - } - } - Ok(ClipboardData::Text(self.inner.get_text()?)) - } - - fn set(&mut self, data: &ClipboardData) -> ResultType<()> { - let _lock = ARBOARD_MTX.lock().unwrap(); - match data { - ClipboardData::Text(s) => self.inner.set_text(s)?, - ClipboardData::Image(a, _) => self.inner.set_image(a.clone())?, - _ => {} - } + self.inner.set_formats(data)?; Ok(()) } } -impl Drop for ClipboardContext { - fn drop(&mut self) { - if let Some(shutdown) = self.shutdown.take() { - let _ = shutdown.signal(); +pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { + use hbb_common::get_version_number; + get_version_number(peer_version) >= get_version_number("1.3.0") + && !["", "Android", &whoami::Platform::Ios.to_string()].contains(&peer_platform) +} + +pub fn get_current_clipboard_msg( + peer_version: &str, + peer_platform: &str, + side: ClipboardSide, +) -> Option { + let mut multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap(); + if multi_clipboards.clipboards.is_empty() { + let mut ctx = ClipboardContext::new().ok()?; + *multi_clipboards = proto::create_multi_clipboards(ctx.get(side, true).ok()?); + } + if multi_clipboards.clipboards.is_empty() { + return None; + } + + if is_support_multi_clipboard(peer_version, peer_platform) { + let mut msg = Message::new(); + msg.set_multi_clipboards(multi_clipboards.clone()); + Some(msg) + } else { + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ClipboardSide { + Host, + Client, +} + +impl ClipboardSide { + // 01: the clipboard is owned by the host + // 10: the clipboard is owned by the client + fn get_owner_data(&self) -> Vec { + match self { + ClipboardSide::Host => vec![0b01], + ClipboardSide::Client => vec![0b10], + } + } + + fn is_owner(&self, data: &[u8]) -> bool { + if data.len() == 0 { + return false; + } + data[0] & 0b11 != 0 + } +} + +impl std::fmt::Display for ClipboardSide { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClipboardSide::Host => write!(f, "host"), + ClipboardSide::Client => write!(f, "client"), } } } + +pub fn start_clipbard_master_thread( + handler: impl ClipboardHandler + Send + 'static, + tx_start_res: Sender<(Option, 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 { + use arboard::ClipboardData; + use hbb_common::{ + compress::{compress as compress_func, decompress}, + message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}, + }; + + fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard { + let compressed = compress_func(s.as_bytes()); + let compress = compressed.len() < s.as_bytes().len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: format.into(), + ..Default::default() + } + } + + fn image_to_proto(a: arboard::ImageData) -> Clipboard { + match &a { + arboard::ImageData::Rgba(rgba) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + width: rgba.width as _, + height: rgba.height as _, + format: ClipboardFormat::ImageRgba.into(), + ..Default::default() + } + } + arboard::ImageData::Png(png) => Clipboard { + compress: false, + content: png.to_owned().to_vec().into(), + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }, + arboard::ImageData::Svg(_) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::ImageSvg.into(), + ..Default::default() + } + } + } + } + + fn clipboard_data_to_proto(data: ClipboardData) -> Option { + let d = match data { + ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), + ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), + ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), + ClipboardData::Image(a) => image_to_proto(a), + _ => return None, + }; + Some(d) + } + + pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { + MultiClipboards { + clipboards: vec_data + .into_iter() + .filter_map(clipboard_data_to_proto) + .collect(), + ..Default::default() + } + } + + fn from_clipboard(clipboard: Clipboard) -> Option { + let data = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content.into() + }; + match clipboard.format.enum_value() { + Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text), + Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf), + Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html), + Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba( + clipboard.width as _, + clipboard.height as _, + data.into(), + ))), + Ok(ClipboardFormat::ImagePng) => { + Some(ClipboardData::Image(arboard::ImageData::png(data.into()))) + } + Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( + std::str::from_utf8(&data).unwrap_or_default(), + ))), + _ => None, + } + } + + pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { + multi_clipboards + .into_iter() + .filter_map(from_clipboard) + .collect() + } + + pub fn get_msg_if_not_support_multi_clip( + version: &str, + platform: &str, + multi_clipboards: &MultiClipboards, + ) -> Option { + if crate::clipboard::is_support_multi_clipboard(version, platform) { + return None; + } + + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} diff --git a/src/flutter.rs b/src/flutter.rs index cd6e51ea1..e60063357 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1256,6 +1256,19 @@ pub fn update_text_clipboard_required() { pub fn send_text_clipboard_msg(msg: Message) { for s in sessions::get_sessions() { if s.is_text_clipboard_required() { + // Check if the client supports multi clipboards + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + let version = s.ui_handler.peer_info.read().unwrap().version.clone(); + let platform = s.ui_handler.peer_info.read().unwrap().platform.clone(); + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &version, + &platform, + multi_clipboards, + ) { + s.send(Data::Message(msg_out)); + continue; + } + } s.send(Data::Message(msg.clone())); } } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index eeeea4999..c0d081eef 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,49 +1,79 @@ use super::*; pub use crate::clipboard::{ - check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, - CONTENT, + check_clipboard, ClipboardContext, ClipboardSide, CLIPBOARD_INTERVAL as INTERVAL, + CLIPBOARD_NAME as NAME, +}; +use clipboard_master::{CallbackResult, ClipboardHandler}; +use std::{ + io, + sync::mpsc::{channel, RecvTimeoutError, Sender}, + time::Duration, }; -#[derive(Default)] -struct State { +struct Handler { + sp: EmptyExtraFieldService, ctx: Option, -} - -impl super::service::Reset for State { - fn reset(&mut self) { - *CONTENT.lock().unwrap() = Default::default(); - self.ctx = None; - } - - fn init(&mut self) { - let ctx = match ClipboardContext::new(true) { - Ok(ctx) => Some(ctx), - Err(err) => { - log::error!("Failed to start {}: {}", NAME, err); - None - } - }; - self.ctx = ctx; - } + tx_cb_result: Sender, } pub fn new() -> GenericService { let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); - GenericService::repeat::(&svc.clone(), INTERVAL, run); + GenericService::run(&svc.clone(), run); svc.sp } -fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { - if let Some(msg) = check_clipboard(&mut state.ctx, None) { - sp.send(msg); - } - sp.snapshot(|sps| { - let data = CONTENT.lock().unwrap().clone(); - if !data.is_empty() { - let msg_out = data.create_msg(); - sps.send_shared(Arc::new(msg_out)); +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + let (tx_cb_result, rx_cb_result) = channel(); + let handler = Handler { + sp: sp.clone(), + ctx: Some(ClipboardContext::new()?), + tx_cb_result, + }; + + 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); } - Ok(()) - })?; + Err(e) => { + bail!("Failed to create clipboard listener: {}", e); + } + }; + + while sp.ok() { + match rx_cb_result.recv_timeout(Duration::from_millis(INTERVAL)) { + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + bail!("Clipboard listener stopped with error: {}", err); + } + Err(RecvTimeoutError::Timeout) => {} + _ => {} + } + } + shutdown.signal(); + h.join().ok(); + Ok(()) } + +impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + self.sp.snapshot(|_sps| Ok(())).ok(); + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Host, false) { + self.sp.send(msg); + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + self.tx_cb_result + .send(CallbackResult::StopWithError(error)) + .ok(); + CallbackResult::Next + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 4a383fe35..5ea1e923a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,6 +1,6 @@ use super::{input_service::*, *}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::clipboard::update_clipboard; +use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(target_os = "android")] @@ -682,8 +682,19 @@ impl Connection { msg = Arc::new(new_msg); } } + Some(message::Union::MultiClipboards(_multi_clipboards)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { + if let Err(err) = conn.stream.send(&msg_out).await { + conn.on_close(&err.to_string(), false).await; + break; + } + continue; + } + } _ => {} } + let msg: &Message = &msg; if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; @@ -2049,7 +2060,7 @@ impl Connection { Some(message::Union::Clipboard(cb)) => { if self.clipboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, None); + update_clipboard(vec![cb], ClipboardSide::Host); #[cfg(all(feature = "flutter", target_os = "android"))] { let content = if cb.compress { @@ -2070,6 +2081,13 @@ impl Connection { } } } + Some(message::Union::MultiClipboards(_mcb)) => + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(_mcb.clipboards, ClipboardSide::Host); + } + } Some(message::Union::Cliprdr(_clip)) => { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]