From d1013487e2f3862e2f801ef1a704bb61fdff8bbb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 29 Mar 2021 15:59:14 +0800 Subject: [PATCH] source code --- .gitignore | 8 + Cargo.lock | 4296 +++++++++++++++++ Cargo.toml | 109 + LICENSE | 674 +++ README.md | 34 +- build.rs | 74 + .../.github/ISSUE_TEMPLATE/bug_report.md | 25 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + libs/enigo/.github/ISSUE_TEMPLATE/question.md | 19 + libs/enigo/.gitignore | 14 + libs/enigo/.travis.yml | 15 + libs/enigo/.vscode/launch.json | 13 + libs/enigo/Cargo.toml | 41 + libs/enigo/LICENSE | 21 + libs/enigo/README.md | 46 + libs/enigo/appveyor.yml | 121 + libs/enigo/build.rs | 61 + libs/enigo/examples/dsl.rs | 11 + libs/enigo/examples/key.rs | 12 + libs/enigo/examples/keyboard.rs | 16 + libs/enigo/examples/mouse.rs | 37 + libs/enigo/examples/timer.rs | 22 + libs/enigo/rustfmt.toml | 1 + libs/enigo/src/dsl.rs | 184 + libs/enigo/src/lib.rs | 525 ++ libs/enigo/src/linux.rs | 361 ++ libs/enigo/src/macos/keycodes.rs | 72 + libs/enigo/src/macos/macos_impl.rs | 680 +++ libs/enigo/src/macos/mod.rs | 4 + libs/enigo/src/win/keycodes.rs | 73 + libs/enigo/src/win/mod.rs | 4 + libs/enigo/src/win/win_impl.rs | 366 ++ libs/hbb_common/.gitignore | 4 + libs/hbb_common/Cargo.toml | 46 + libs/hbb_common/build.rs | 9 + libs/hbb_common/protos/message.proto | 401 ++ libs/hbb_common/protos/rendezvous.proto | 133 + libs/hbb_common/src/bytes_codec.rs | 274 ++ libs/hbb_common/src/compress.rs | 50 + libs/hbb_common/src/config.rs | 688 +++ libs/hbb_common/src/fs.rs | 554 +++ libs/hbb_common/src/lib.rs | 215 + libs/hbb_common/src/quic.rs | 135 + libs/hbb_common/src/tcp.rs | 146 + libs/hbb_common/src/udp.rs | 75 + libs/magnum-opus/.gitignore | 2 + libs/magnum-opus/Cargo.toml | 18 + libs/magnum-opus/LICENSE-APACHE | 201 + libs/magnum-opus/LICENSE-MIT | 19 + libs/magnum-opus/README.md | 22 + libs/magnum-opus/build.rs | 89 + libs/magnum-opus/opus_ffi.h | 1 + libs/magnum-opus/src/lib.rs | 843 ++++ libs/magnum-opus/src/opus_ffi.rs | 6 + .../tests/compile-fail/repacketize.rs | 10 + libs/magnum-opus/tests/fec.rs | 13 + libs/magnum-opus/tests/opus-padding.rs | 29 + libs/magnum-opus/tests/tests.rs | 127 + libs/parity-tokio-ipc/.gitignore | 3 + libs/parity-tokio-ipc/.travis.yml | 20 + libs/parity-tokio-ipc/Cargo.toml | 24 + libs/parity-tokio-ipc/LICENSE-APACHE | 201 + libs/parity-tokio-ipc/LICENSE-MIT | 25 + libs/parity-tokio-ipc/README.md | 28 + libs/parity-tokio-ipc/appveyor.yml | 16 + libs/parity-tokio-ipc/examples/client.rs | 24 + libs/parity-tokio-ipc/examples/server.rs | 47 + .../parity-tokio-ipc/examples/spam-clients.sh | 7 + libs/parity-tokio-ipc/src/lib.rs | 154 + libs/parity-tokio-ipc/src/unix.rs | 163 + libs/parity-tokio-ipc/src/win.rs | 488 ++ libs/pulsectl/.gitignore | 4 + libs/pulsectl/Cargo.lock | 129 + libs/pulsectl/Cargo.toml | 19 + libs/pulsectl/LICENSE.md | 13 + libs/pulsectl/README.md | 43 + libs/pulsectl/examples/change_device_vol.rs | 34 + libs/pulsectl/src/controllers/errors.rs | 51 + libs/pulsectl/src/controllers/mod.rs | 595 +++ libs/pulsectl/src/controllers/types.rs | 354 ++ libs/pulsectl/src/errors.rs | 54 + libs/pulsectl/src/lib.rs | 166 + libs/scrap/.gitignore | 4 + libs/scrap/Cargo.toml | 32 + libs/scrap/README.md | 60 + libs/scrap/build.rs | 118 + libs/scrap/examples/ffplay.rs | 51 + libs/scrap/examples/list.rs | 16 + libs/scrap/examples/record-screen.rs | 161 + libs/scrap/examples/screenshot.rs | 122 + libs/scrap/src/common/codec.rs | 536 ++ libs/scrap/src/common/convert.rs | 188 + libs/scrap/src/common/dxgi.rs | 104 + libs/scrap/src/common/mod.rs | 23 + libs/scrap/src/common/quartz.rs | 125 + libs/scrap/src/common/vpx.rs | 25 + libs/scrap/src/common/x11.rs | 87 + libs/scrap/src/dxgi/gdi.rs | 212 + libs/scrap/src/dxgi/mod.rs | 539 +++ libs/scrap/src/lib.rs | 20 + libs/scrap/src/quartz/capturer.rs | 111 + libs/scrap/src/quartz/config.rs | 75 + libs/scrap/src/quartz/display.rs | 63 + libs/scrap/src/quartz/ffi.rs | 240 + libs/scrap/src/quartz/frame.rs | 79 + libs/scrap/src/quartz/mod.rs | 11 + libs/scrap/src/x11/capturer.rs | 123 + libs/scrap/src/x11/display.rs | 55 + libs/scrap/src/x11/ffi.rs | 205 + libs/scrap/src/x11/iter.rs | 93 + libs/scrap/src/x11/mod.rs | 10 + libs/scrap/src/x11/server.rs | 122 + libs/scrap/vpx_ffi.h | 9 + libs/systray-rs/.gitignore | 5 + libs/systray-rs/.travis.yml | 32 + libs/systray-rs/CHANGELOG.md | 33 + libs/systray-rs/Cargo.toml | 28 + libs/systray-rs/LICENSE.txt | 28 + libs/systray-rs/README.md | 58 + libs/systray-rs/appveyor.yml | 22 + libs/systray-rs/ci/before_install.sh | 39 + libs/systray-rs/examples/systray-example.rs | 43 + libs/systray-rs/resources/rust.ico | Bin 0 -> 64752 bytes libs/systray-rs/src/api/cocoa/mod.rs | 28 + libs/systray-rs/src/api/linux/mod.rs | 182 + libs/systray-rs/src/api/mod.rs | 11 + libs/systray-rs/src/api/win32/mod.rs | 460 ++ libs/systray-rs/src/lib.rs | 192 + src/cli.rs | 94 + src/client.rs | 1113 +++++ src/common.rs | 365 ++ src/ipc.rs | 464 ++ src/lib.rs | 29 + src/main.rs | 148 + src/platform/linux.rs | 446 ++ src/platform/macos.rs | 335 ++ src/platform/mod.rs | 46 + src/platform/windows.rs | 981 ++++ src/port_forward.rs | 163 + src/rendezvous_mediator.rs | 416 ++ src/server.rs | 335 ++ src/server/audio_service.rs | 350 ++ src/server/clipboard_service.rs | 53 + src/server/connection.rs | 979 ++++ src/server/input_service.rs | 499 ++ src/server/service.rs | 249 + src/server/video_service.rs | 384 ++ src/tray-icon.ico | Bin 0 -> 4286 bytes src/ui.rs | 647 +++ src/ui/chatbox.html | 27 + src/ui/cm.css | 218 + src/ui/cm.html | 21 + src/ui/cm.rs | 465 ++ src/ui/cm.tis | 409 ++ src/ui/common.css | 319 ++ src/ui/common.tis | 297 ++ src/ui/file_transfer.css | 255 + src/ui/file_transfer.tis | 617 +++ src/ui/grid.tis | 234 + src/ui/header.css | 67 + src/ui/header.tis | 377 ++ src/ui/index.css | 261 + src/ui/index.html | 30 + src/ui/index.tis | 713 +++ src/ui/install.html | 22 + src/ui/install.tis | 45 + src/ui/macos.rs | 145 + src/ui/msgbox.html | 69 + src/ui/msgbox.tis | 271 ++ src/ui/port_forward.tis | 77 + src/ui/remote.css | 37 + src/ui/remote.html | 33 + src/ui/remote.rs | 1660 +++++++ src/ui/remote.tis | 434 ++ src/windows.cc | 366 ++ 175 files changed, 35074 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 build.rs create mode 100644 libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 libs/enigo/.github/ISSUE_TEMPLATE/question.md create mode 100644 libs/enigo/.gitignore create mode 100644 libs/enigo/.travis.yml create mode 100644 libs/enigo/.vscode/launch.json create mode 100644 libs/enigo/Cargo.toml create mode 100644 libs/enigo/LICENSE create mode 100644 libs/enigo/README.md create mode 100644 libs/enigo/appveyor.yml create mode 100644 libs/enigo/build.rs create mode 100644 libs/enigo/examples/dsl.rs create mode 100644 libs/enigo/examples/key.rs create mode 100644 libs/enigo/examples/keyboard.rs create mode 100644 libs/enigo/examples/mouse.rs create mode 100644 libs/enigo/examples/timer.rs create mode 100644 libs/enigo/rustfmt.toml create mode 100644 libs/enigo/src/dsl.rs create mode 100644 libs/enigo/src/lib.rs create mode 100644 libs/enigo/src/linux.rs create mode 100644 libs/enigo/src/macos/keycodes.rs create mode 100644 libs/enigo/src/macos/macos_impl.rs create mode 100644 libs/enigo/src/macos/mod.rs create mode 100644 libs/enigo/src/win/keycodes.rs create mode 100644 libs/enigo/src/win/mod.rs create mode 100644 libs/enigo/src/win/win_impl.rs create mode 100644 libs/hbb_common/.gitignore create mode 100644 libs/hbb_common/Cargo.toml create mode 100644 libs/hbb_common/build.rs create mode 100644 libs/hbb_common/protos/message.proto create mode 100644 libs/hbb_common/protos/rendezvous.proto create mode 100644 libs/hbb_common/src/bytes_codec.rs create mode 100644 libs/hbb_common/src/compress.rs create mode 100644 libs/hbb_common/src/config.rs create mode 100644 libs/hbb_common/src/fs.rs create mode 100644 libs/hbb_common/src/lib.rs create mode 100644 libs/hbb_common/src/quic.rs create mode 100644 libs/hbb_common/src/tcp.rs create mode 100644 libs/hbb_common/src/udp.rs create mode 100644 libs/magnum-opus/.gitignore create mode 100644 libs/magnum-opus/Cargo.toml create mode 100644 libs/magnum-opus/LICENSE-APACHE create mode 100644 libs/magnum-opus/LICENSE-MIT create mode 100644 libs/magnum-opus/README.md create mode 100644 libs/magnum-opus/build.rs create mode 100644 libs/magnum-opus/opus_ffi.h create mode 100644 libs/magnum-opus/src/lib.rs create mode 100644 libs/magnum-opus/src/opus_ffi.rs create mode 100644 libs/magnum-opus/tests/compile-fail/repacketize.rs create mode 100644 libs/magnum-opus/tests/fec.rs create mode 100644 libs/magnum-opus/tests/opus-padding.rs create mode 100644 libs/magnum-opus/tests/tests.rs create mode 100644 libs/parity-tokio-ipc/.gitignore create mode 100644 libs/parity-tokio-ipc/.travis.yml create mode 100644 libs/parity-tokio-ipc/Cargo.toml create mode 100644 libs/parity-tokio-ipc/LICENSE-APACHE create mode 100644 libs/parity-tokio-ipc/LICENSE-MIT create mode 100644 libs/parity-tokio-ipc/README.md create mode 100644 libs/parity-tokio-ipc/appveyor.yml create mode 100644 libs/parity-tokio-ipc/examples/client.rs create mode 100644 libs/parity-tokio-ipc/examples/server.rs create mode 100755 libs/parity-tokio-ipc/examples/spam-clients.sh create mode 100644 libs/parity-tokio-ipc/src/lib.rs create mode 100644 libs/parity-tokio-ipc/src/unix.rs create mode 100644 libs/parity-tokio-ipc/src/win.rs create mode 100644 libs/pulsectl/.gitignore create mode 100644 libs/pulsectl/Cargo.lock create mode 100644 libs/pulsectl/Cargo.toml create mode 100644 libs/pulsectl/LICENSE.md create mode 100644 libs/pulsectl/README.md create mode 100644 libs/pulsectl/examples/change_device_vol.rs create mode 100644 libs/pulsectl/src/controllers/errors.rs create mode 100644 libs/pulsectl/src/controllers/mod.rs create mode 100644 libs/pulsectl/src/controllers/types.rs create mode 100644 libs/pulsectl/src/errors.rs create mode 100644 libs/pulsectl/src/lib.rs create mode 100644 libs/scrap/.gitignore create mode 100644 libs/scrap/Cargo.toml create mode 100644 libs/scrap/README.md create mode 100644 libs/scrap/build.rs create mode 100644 libs/scrap/examples/ffplay.rs create mode 100644 libs/scrap/examples/list.rs create mode 100644 libs/scrap/examples/record-screen.rs create mode 100644 libs/scrap/examples/screenshot.rs create mode 100644 libs/scrap/src/common/codec.rs create mode 100644 libs/scrap/src/common/convert.rs create mode 100644 libs/scrap/src/common/dxgi.rs create mode 100644 libs/scrap/src/common/mod.rs create mode 100644 libs/scrap/src/common/quartz.rs create mode 100644 libs/scrap/src/common/vpx.rs create mode 100644 libs/scrap/src/common/x11.rs create mode 100644 libs/scrap/src/dxgi/gdi.rs create mode 100644 libs/scrap/src/dxgi/mod.rs create mode 100644 libs/scrap/src/lib.rs create mode 100644 libs/scrap/src/quartz/capturer.rs create mode 100644 libs/scrap/src/quartz/config.rs create mode 100644 libs/scrap/src/quartz/display.rs create mode 100644 libs/scrap/src/quartz/ffi.rs create mode 100644 libs/scrap/src/quartz/frame.rs create mode 100644 libs/scrap/src/quartz/mod.rs create mode 100644 libs/scrap/src/x11/capturer.rs create mode 100644 libs/scrap/src/x11/display.rs create mode 100644 libs/scrap/src/x11/ffi.rs create mode 100644 libs/scrap/src/x11/iter.rs create mode 100644 libs/scrap/src/x11/mod.rs create mode 100644 libs/scrap/src/x11/server.rs create mode 100644 libs/scrap/vpx_ffi.h create mode 100644 libs/systray-rs/.gitignore create mode 100644 libs/systray-rs/.travis.yml create mode 100644 libs/systray-rs/CHANGELOG.md create mode 100644 libs/systray-rs/Cargo.toml create mode 100644 libs/systray-rs/LICENSE.txt create mode 100644 libs/systray-rs/README.md create mode 100644 libs/systray-rs/appveyor.yml create mode 100644 libs/systray-rs/ci/before_install.sh create mode 100644 libs/systray-rs/examples/systray-example.rs create mode 100644 libs/systray-rs/resources/rust.ico create mode 100644 libs/systray-rs/src/api/cocoa/mod.rs create mode 100644 libs/systray-rs/src/api/linux/mod.rs create mode 100644 libs/systray-rs/src/api/mod.rs create mode 100644 libs/systray-rs/src/api/win32/mod.rs create mode 100644 libs/systray-rs/src/lib.rs create mode 100644 src/cli.rs create mode 100644 src/client.rs create mode 100644 src/common.rs create mode 100644 src/ipc.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/platform/linux.rs create mode 100644 src/platform/macos.rs create mode 100644 src/platform/mod.rs create mode 100644 src/platform/windows.rs create mode 100644 src/port_forward.rs create mode 100644 src/rendezvous_mediator.rs create mode 100644 src/server.rs create mode 100644 src/server/audio_service.rs create mode 100644 src/server/clipboard_service.rs create mode 100644 src/server/connection.rs create mode 100644 src/server/input_service.rs create mode 100644 src/server/service.rs create mode 100644 src/server/video_service.rs create mode 100644 src/tray-icon.ico create mode 100644 src/ui.rs create mode 100644 src/ui/chatbox.html create mode 100644 src/ui/cm.css create mode 100644 src/ui/cm.html create mode 100644 src/ui/cm.rs create mode 100644 src/ui/cm.tis create mode 100644 src/ui/common.css create mode 100644 src/ui/common.tis create mode 100644 src/ui/file_transfer.css create mode 100644 src/ui/file_transfer.tis create mode 100644 src/ui/grid.tis create mode 100644 src/ui/header.css create mode 100644 src/ui/header.tis create mode 100644 src/ui/index.css create mode 100644 src/ui/index.html create mode 100644 src/ui/index.tis create mode 100644 src/ui/install.html create mode 100644 src/ui/install.tis create mode 100644 src/ui/macos.rs create mode 100644 src/ui/msgbox.html create mode 100644 src/ui/msgbox.tis create mode 100644 src/ui/port_forward.tis create mode 100644 src/ui/remote.css create mode 100644 src/ui/remote.html create mode 100644 src/ui/remote.rs create mode 100644 src/ui/remote.tis create mode 100644 src/windows.cc diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..cdf54e40b14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +.idea +.DS_Store +src/ui/inline.rs +extractor +__pycache__ +src/version.rs +*dmg diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000000..533fbc4096b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4296 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.15.0", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + +[[package]] +name = "android_logger" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec2333c185d826313162cee39d3fcc6a84ba08114a839bebf53b961e7e75773" +dependencies = [ + "android_log-sys", + "env_logger 0.7.1", + "lazy_static", + "log", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anyhow" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812b4911e210bd51b24596244523c856ca749e6223c50a7fbbba3f89ee37c426" +dependencies = [ + "atk-sys", + "bitflags", + "glib", + "glib-sys", + "gobject-sys", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bindgen" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c85344eb535a31b62f0af37be84441ba9e7f0f4111eb0530f43d15e513fe57" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "clap", + "env_logger 0.7.1", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bindgen" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99de13bb6361e01e493b3db7928085dcc474b7ba4f5481818e53a89d76b8393f" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "clap", + "env_logger 0.7.1", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cairo-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce5b5fb86b0c57c20c834c1b412fd09c77c8a59b9473f86272709e78874cd1d" +dependencies = [ + "nom 4.2.3", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits 0.2.14", + "time 0.1.43", + "winapi 0.3.9", +] + +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + +[[package]] +name = "clang-sys" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81de550971c976f176130da4b2978d3b524eaa0fd9ac31f3ceb5ae1231fb4853" +dependencies = [ + "glob", + "libc", + "libloading 0.5.2", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi 0.3.9", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cocoa" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation 0.9.1", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags", + "block", + "core-foundation 0.9.1", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "combine" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e" +dependencies = [ + "bytes 1.0.1", + "memchr", +] + +[[package]] +name = "confy" +version = "0.4.1" +source = "git+https://github.com/open-trade/confy#27fa12941291b44ccd856aef4a5452c1eb646047" +dependencies = [ + "directories", + "serde 1.0.123", + "toml", +] + +[[package]] +name = "const_fn" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +dependencies = [ + "percent-encoding", + "time 0.2.25", + "version_check 0.9.2", +] + +[[package]] +name = "cookie_store" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" +dependencies = [ + "cookie", + "idna", + "log", + "publicsuffix", + "serde 1.0.123", + "serde_json 1.0.62", + "time 0.2.25", + "url", +] + +[[package]] +name = "copypasta" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4423d79fed83ebd9ab81ec21fa97144300a961782158287dc9bf7eddac37ff0b" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys 0.8.2", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "core-graphics" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269f35f69b542b80e736a20a89a05215c0ce80c2c03c514abb2e318b78379d86" +dependencies = [ + "bitflags", + "core-foundation 0.9.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation 0.9.1", + "foreign-types", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f73df0f29f4c3c374854f076c47dc018f19acaa63538880dba0937ad4fa8d7" +dependencies = [ + "bindgen 0.53.1", +] + +[[package]] +name = "cpal" +version = "0.13.1" +source = "git+https://github.com/rustaudio/cpal#1d72ae74535fea1cebc5158d8a63362042e2633f" +dependencies = [ + "alsa", + "core-foundation-sys 0.6.2", + "coreaudio-rs", + "jni 0.17.0", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk", + "ndk-glue", + "nix 0.15.0", + "oboe", + "parking_lot", + "stdweb 0.1.3", + "thiserror", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae8f328835f8f5a6ceb6a7842a7f2d0c03692adb5c889347235d59194731fe3" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 1.0.0", + "lazy_static", + "loom", +] + +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + +[[package]] +name = "ctrlc" +version = "3.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b57a92e9749e10f25a171adcebfafe72991d45e7ec2dcb853e8f83d9dafaeb08" +dependencies = [ + "nix 0.18.0", + "winapi 0.3.9", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "darwin-libproc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb90051930c9a0f09e585762152048e23ac74d20c10590ef7cf01c0343c3046" +dependencies = [ + "darwin-libproc-sys", + "libc", + "memchr", +] + +[[package]] +name = "darwin-libproc-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57cebb5bde66eecdd30ddc4b9cd208238b15db4982ccc72db59d699ea10867c1" +dependencies = [ + "libc", +] + +[[package]] +name = "dasp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_signal", + "dasp_slice", + "dasp_window", +] + +[[package]] +name = "dasp_envelope" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" +dependencies = [ + "dasp_frame", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_interpolate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_peak" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_ring_buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" + +[[package]] +name = "dasp_rms" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dasp_signal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_window", +] + +[[package]] +name = "dasp_slice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_window" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bcb90ea007ba45fc48d426e28af3e8a653634f9a7174d768dcfe90fa6211f4" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "directories" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", + "winapi 0.3.9", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" +dependencies = [ + "libloading 0.6.7", +] + +[[package]] +name = "docopt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969" +dependencies = [ + "lazy_static", + "regex", + "serde 1.0.123", + "strsim 0.9.3", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dtoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "enigo" +version = "0.0.14" +dependencies = [ + "core-graphics", + "libc", + "log", + "objc", + "pkg-config", + "serde 1.0.123", + "serde_derive", + "unicode-segmentation", + "winapi 0.3.9", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +dependencies = [ + "atty", + "humantime 2.1.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "err-derive" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22deed3a8124cff5fa835713fa105621e43bbdc46690c3a6b68328a012d350d4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + +[[package]] +name = "err-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc7f65832b62ed38939f98966824eb6294911c3629b0e9a262bfb80836d9686" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check 0.9.2", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", +] + +[[package]] +name = "fetch_unroll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d44807d562d137f063cbfe209da1c3f9f2fa8375e11166ef495daab7b847f9" +dependencies = [ + "libflate", + "tar", + "ureq", +] + +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.5", + "winapi 0.3.9", +] + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "flexi_logger" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291b6ce7b3ed2dda82efa6aee4c6bdb55fd11bc88b06c55b01851e94b96e5322" +dependencies = [ + "atty", + "chrono", + "glob", + "lazy_static", + "log", + "regex", + "thiserror", + "yansi", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" + +[[package]] +name = "futures-executor" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" + +[[package]] +name = "futures-macro" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" + +[[package]] +name = "futures-task" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.4", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "gdk" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db00839b2a68a7a10af3fa28dfb3febaba3a20c3a9ac2425a33b7df1f84a6b7d" +dependencies = [ + "bitflags", + "cairo-rs", + "cairo-sys-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6dae3cb99dd49b758b88f0132f8d401108e63ae8edd45f432d42cdff99998a" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bfe468a7f43e97b8d193a762b6c5cf67a7d36cacbc0b9291dbcae24bfea1e8f" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generator" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fed24fd1e18827652b4d55652899a1e9da8e54d91624dc3437a5bc3a9f9a9c" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "winapi 0.3.9", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check 0.9.2", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "gio" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb60242bfff700772dae5d9e3a1f7aa2e4ebccf18b89662a16acb2822568561" +dependencies = [ + "bitflags", + "futures", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e24fb752f8f5d2cf6bbc2c606fd2bc989c81c5e2fe321ab974d54f8b6344eac" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi 0.3.9", +] + +[[package]] +name = "glib" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", +] + +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +dependencies = [ + "anyhow", + "heck", + "itertools", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "gobject-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f022f2054072b3af07666341984562c8e626a79daa8be27b955d12d06a5ad6a" +dependencies = [ + "atk", + "bitflags", + "cairo-rs", + "cairo-sys-rs", + "cc", + "gdk", + "gdk-pixbuf", + "gdk-pixbuf-sys", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk-sys", + "libc", + "once_cell", + "pango", + "pango-sys", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89acda6f084863307d948ba64a4b1ef674e8527dddab147ee4cdcc194c880457" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes 0.5.6", + "confy", + "directories-next", + "dirs-next", + "env_logger 0.8.3", + "filetime", + "futures", + "futures-util", + "lazy_static", + "log", + "mac_address", + "protobuf", + "protobuf-codegen-pure", + "quinn", + "rand 0.7.3", + "serde 1.0.123", + "serde_derive", + "serde_json 1.0.62", + "socket2", + "sodiumoxide", + "tokio", + "tokio-util", + "toml", + "winapi 0.3.9", + "zstd", +] + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "hound" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "jni" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" +dependencies = [ + "cesu8", + "combine 3.8.1", + "error-chain", + "jni-sys", + "log", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c" +dependencies = [ + "cesu8", + "combine 4.5.2", + "error-chain", + "jni-sys", + "log", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libappindicator" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3fc57353f50e2faf5741438df9d2b628d896e2c5bb31156306b015f59d037" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9719ba201a6cb121f59a36bae0bd2bd91762c4af6fbdaaffe804cfe2f9c8651d" +dependencies = [ + "bindgen 0.52.0", + "gtk-sys", + "pkg-config", +] + +[[package]] +name = "libc" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" + +[[package]] +name = "libflate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", + "rle-decode-fast", +] + +[[package]] +name = "libflate_lz77" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" + +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi 0.3.9", +] + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "libpulse-binding" +version = "2.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" +dependencies = [ + "bitflags", + "libc", + "libpulse-sys", + "num-derive", + "num-traits 0.2.14", + "winapi 0.3.9", +] + +[[package]] +name = "libpulse-simple-binding" +version = "2.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a574975292db859087c3957b9182f7d53278553f06bddaa2099c90e4ac3a0ee0" +dependencies = [ + "libpulse-binding", + "libpulse-simple-sys", + "libpulse-sys", +] + +[[package]] +name = "libpulse-simple-sys" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468cf582b7b022c0d1b266fefc7fc8fa7b1ddcb61214224f2f105c95a9c2d5c1" +dependencies = [ + "libpulse-sys", + "pkg-config", +] + +[[package]] +name = "libpulse-sys" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf17e9832643c4f320c42b7d78b2c0510f45aa5e823af094413b94e45076ba82" +dependencies = [ + "libc", + "num-derive", + "num-traits 0.2.14", + "pkg-config", + "winapi 0.3.9", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a685b64f837b339074115f2e7f7b431ac73681d08d75b389db7498b8892b8a58" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "loom" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44c73b4636e497b4917eb21c33539efa3816741a2d3ff26c6316f1b529481a4" +dependencies = [ + "cfg-if 1.0.0", + "generator", + "scoped-tls", +] + +[[package]] +name = "mac_address" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9bb26482176bddeea173ceaa2acec85146d20cdcc631eafaf9d605d3d4fc23" +dependencies = [ + "nix 0.19.1", + "winapi 0.3.9", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "machine-uid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" +dependencies = [ + "winreg 0.6.2", +] + +[[package]] +name = "magnum-opus" +version = "0.3.4" +dependencies = [ + "bindgen 0.53.1", + "target_build_utils", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memmap2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.6", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "ndk" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", +] + +[[package]] +name = "nix" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "memchr", + "version_check 0.9.2", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + +[[package]] +name = "oboe" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" +dependencies = [ + "jni 0.14.0", + "ndk", + "ndk-glue", + "num-derive", + "num-traits 0.2.14", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" +dependencies = [ + "fetch_unroll", +] + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "pango" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9937068580bebd8ced19975938573803273ccbcbd598c58d4906efd4ac87c438" +dependencies = [ + "bitflags", + "glib", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parity-tokio-ipc" +version = "0.7.2" +dependencies = [ + "futures", + "libc", + "log", + "mio-named-pipes", + "miow 0.3.6", + "rand 0.7.3", + "tokio", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.5", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared", + "rand 0.6.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-project-lite" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "platforms" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb3b2b1033b8a60b4da6ee470325f887758c95d5320f52f9ce0df055a55940e" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.2", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.2", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "protobuf" +version = "3.0.0-pre" +source = "git+https://github.com/stepancheg/rust-protobuf#5af22c6456a61eea324aff15a8edcaf6a57d8c5f" + +[[package]] +name = "protobuf-codegen" +version = "3.0.0-pre" +source = "git+https://github.com/stepancheg/rust-protobuf#5af22c6456a61eea324aff15a8edcaf6a57d8c5f" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protobuf-codegen-pure" +version = "3.0.0-pre" +source = "git+https://github.com/stepancheg/rust-protobuf#5af22c6456a61eea324aff15a8edcaf6a57d8c5f" +dependencies = [ + "protobuf", + "protobuf-codegen", +] + +[[package]] +name = "psutil" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cdb732329774b8765346796abd1e896e9b3c86aae7f135bb1dda98c2c460f55" +dependencies = [ + "cfg-if 0.1.10", + "darwin-libproc", + "derive_more", + "glob", + "mach", + "nix 0.17.0", + "num_cpus", + "once_cell", + "platforms", + "thiserror", + "unescape", +] + +[[package]] +name = "publicsuffix" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" +dependencies = [ + "error-chain", + "idna", + "lazy_static", + "regex", + "url", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quest" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8" +dependencies = [ + "cfg-if 0.1.10", + "rpassword 2.1.0", + "tempfile", + "termios", + "winapi 0.3.9", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88de58d76d8f82fb28e5c89302119c102bb5b9ce57b034186b559b63ba147a0f" +dependencies = [ + "bytes 0.5.6", + "err-derive 0.2.4", + "futures", + "libc", + "mio", + "quinn-proto", + "rustls 0.17.0", + "tokio", + "tracing", + "webpki", +] + +[[package]] +name = "quinn-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ea0a358c179c6b7af34805c675d1664a9c6a234a7acd7efdbb32d2f39d3d2a" +dependencies = [ + "bytes 0.5.6", + "ct-logs", + "err-derive 0.2.4", + "rand 0.7.3", + "ring", + "rustls 0.17.0", + "rustls-native-certs", + "slab", + "tracing", + "webpki", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.2", + "rand_hc 0.3.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.2", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.2", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.2", + "redox_syscall 0.2.5", +] + +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "repng" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd57cd2cb5cc699b3eb4824d654e5a32f3bc013766da4966f71fe94805abbda" +dependencies = [ + "byteorder", + "flate2", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" + +[[package]] +name = "rpassword" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37473170aedbe66ffa3ad3726939ba677d83c646ad4fd99e5b4bc38712f45ec" +dependencies = [ + "kernel32-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "runas" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a620b0994a180cdfa25c0439e6d58c0628272571501880d626ffff58e96a0799" +dependencies = [ + "cc", + "which", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust-pulsectl" +version = "0.2.10" +dependencies = [ + "libpulse-binding", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustdesk" +version = "1.1.2" +dependencies = [ + "android_logger", + "async-trait", + "cc", + "cfg-if 1.0.0", + "clap", + "cocoa", + "copypasta", + "core-foundation 0.9.1", + "core-graphics", + "cpal", + "crc32fast", + "ctrlc", + "dasp", + "dispatch", + "enigo", + "flexi_logger", + "hbb_common", + "hound", + "lazy_static", + "libc", + "libpulse-binding", + "libpulse-simple-binding", + "mac_address", + "machine-uid", + "magnum-opus", + "objc", + "parity-tokio-ipc", + "psutil", + "repng", + "rpassword 5.0.1", + "runas", + "rust-pulsectl", + "sciter-rs", + "scrap", + "serde 1.0.123", + "serde_derive", + "serde_json 1.0.62", + "sha2", + "systray", + "uuid", + "whoami", + "winapi 0.3.9", + "windows-service", + "winreg 0.7.0", + "winres", +] + +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +dependencies = [ + "base64 0.11.0", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" +dependencies = [ + "base64 0.13.0", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5" +dependencies = [ + "openssl-probe", + "rustls 0.17.0", + "schannel", + "security-framework", +] + +[[package]] +name = "rustversion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "sciter-rs" +version = "0.5.54" +source = "git+https://github.com/sciter-sdk/rust-sciter#3428c3a7e2837770f3b20e7cd045328a8fa91c22" +dependencies = [ + "lazy_static", + "libc", + "objc", + "objc-foundation", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scrap" +version = "0.5.0" +dependencies = [ + "bindgen 0.53.1", + "block", + "cfg-if 1.0.0", + "docopt", + "libc", + "num_cpus", + "quest", + "repng", + "serde 1.0.123", + "target_build_utils", + "webm", + "winapi 0.3.9", +] + +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" + +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8bcf487be7d2e15d3d543f04312de991d631cfe1b43ea0ade69e6a8a5b16a1" +dependencies = [ + "dtoa", + "itoa 0.3.4", + "num-traits 0.1.43", + "serde 0.9.15", +] + +[[package]] +name = "serde_json" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +dependencies = [ + "itoa 0.4.7", + "ryu", + "serde 1.0.123", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "sha2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "signal-hook-registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316e13a3eb853ce7bf72ad3530dc186cb2005c57c521ef5f4ada5ee4eed74de6" +dependencies = [ + "bitflags", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.18.0", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "smithay-clipboard" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06384dfaf645908220d976ae24ed39f6cf92efecb0225ea0a948e403014de527" +dependencies = [ + "smithay-client-toolkit", + "wayland-client", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7038b67c941e23501573cb7242ffb08709abe9b11eb74bceff875bbda024a6a8" +dependencies = [ + "libc", + "libsodium-sys", + "serde 1.0.123", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "standback" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" +dependencies = [ + "version_check 0.9.2", +] + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde 1.0.123", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde 1.0.123", + "serde_derive", + "serde_json 1.0.62", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +dependencies = [ + "heck", + "pkg-config", + "strum", + "strum_macros", + "thiserror", + "toml", + "version-compare", +] + +[[package]] +name = "systray" +version = "0.4.1" +dependencies = [ + "glib", + "gtk", + "libappindicator", + "libc", + "log", + "winapi 0.3.9", +] + +[[package]] +name = "tar" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bcfbd6a598361fda270d82469fff3d65089dc33e175c9a131f7b4cd395f228" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target_build_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013d134ae4a25ee744ad6129db589018558f620ddfa44043887cdd45fa08e75c" +dependencies = [ + "phf", + "phf_codegen", + "serde_json 0.9.10", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.3", + "redox_syscall 0.2.5", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb 0.4.20", + "time-macros", + "version_check 0.9.2", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite 0.1.11", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-io", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde 1.0.123", +] + +[[package]] +name = "tracing" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f77d3842f76ca899ff2dbcf231c5c65813dea431301d6eb686279c15c4464f12" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite 0.2.4", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a9bd1db7706f2373a190b0d067146caa39350c486f3d455b0e33b431f94c07" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unescape" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" +dependencies = [ + "base64 0.13.0", + "chunked_transfer", + "cookie", + "cookie_store", + "log", + "once_cell", + "qstring", + "rustls 0.19.0", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" + +[[package]] +name = "wayland-client" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbdbe01d03b2267809f3ed99495b37395387fde789e0f2ebb78e8b43f75b6d7" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.18.0", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480450f76717edd64ad04a4426280d737fc3d10a236b982df7b1aee19f0e2d56" +dependencies = [ + "nix 0.18.0", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eb122c160223a7660feeaf949d0100281d1279acaaed3720eb3c9894496e5f" +dependencies = [ + "nix 0.18.0", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319a82b4d3054dd25acc32d9aee0f84fa95b63bc983fffe4703b6b8d47e01a30" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7010ba5767b3fcd350decc59055390b4ebe6bd1b9279a9feb1f1888987f1133d" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6793834e0c35d11fd96a97297abe03d37be627e1847da52e17d7e0e3b51cc099" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webm" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a860ee65db880db9709b62dd5ea898678260555daf3572e1fbb112ee954e35" +dependencies = [ + "webm-sys", +] + +[[package]] +name = "webm-sys" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d64dd814981fa97a3d01d41c095b55e88187f09335b0e74835984024c1991d" +dependencies = [ + "cc", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" +dependencies = [ + "webpki", +] + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "failure", + "libc", +] + +[[package]] +name = "whoami" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7884773ab69074615cb8f8425d0e53f11710786158704fca70f53e71b0e05504" + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-service" +version = "0.3.1" +source = "git+https://github.com/mullvad/windows-service-rs.git#f4433728f2a220118c4119f958996be948575613" +dependencies = [ + "bitflags", + "err-derive 0.3.0", + "widestring", + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winres" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc" +dependencies = [ + "toml", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "x11-clipboard" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e937afd03b64b7be4f959cc044e09260a47241b71e56933f37db097bf7859d" +dependencies = [ + "xcb", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "xcb" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62056f63138b39116f82a540c983cc11f1c90cd70b3d492a70c25eaa50bd22a6" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "xcursor" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08" +dependencies = [ + "nom 6.1.2", +] + +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" + +[[package]] +name = "zstd" +version = "0.5.4+zstd.1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69996ebdb1ba8b1517f61387a883857818a66c8a295f487b1ffd8fd9d2c82910" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "2.0.6+zstd.1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98aa931fb69ecee256d44589d19754e61851ae4769bf963b385119b1cc37a49e" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.4.18+zstd.1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6e8778706838f43f771d80d37787cb2fe06dafe89dd3aebaf6721b9eaec81" +dependencies = [ + "cc", + "glob", + "itertools", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000000..9f6f4c2b468 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,109 @@ +[package] +name = "rustdesk" +version = "1.1.2" +authors = ["rustdesk "] +edition = "2018" +build= "build.rs" +description = "A remote control software." + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +[features] +inline = [] +cli = [] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +whoami = "0.9" +scrap = { path = "libs/scrap" } +hbb_common = { path = "libs/hbb_common" } +enigo = { path = "libs/enigo" } +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +cfg-if = "1.0" +lazy_static = "1.4" +sha2 = "0.9" +repng = "0.2" +libc = "0.2" +parity-tokio-ipc = { path = "libs/parity-tokio-ipc" } +flexi_logger = "0.16" +runas = "0.2" +magnum-opus = { path = "libs/magnum-opus" } +dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"] } +async-trait = "0.1" +crc32fast = "1.2" +uuid = { version = "0.8", features = ["v4"] } +copypasta = "0.7" +clap = "2.33" +rpassword = "5.0" + +[target.'cfg(not(any(target_os = "android")))'.dependencies] +cpal = { git = "https://github.com/rustaudio/cpal" } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +machine-uid = "0.2" +mac_address = "1.1" +sciter-rs = { git = "https://github.com/sciter-sdk/rust-sciter" } + +[target.'cfg(target_os = "windows")'.dependencies] +systray = { path = "libs/systray-rs" } +winapi = { version = "0.3", features = ["winuser"] } +winreg = "0.7" +windows-service = { git = 'https://github.com/mullvad/windows-service-rs.git' } + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +cocoa = "0.24" +dispatch = "0.2" +core-foundation = "0.9" +core-graphics = "0.22" + +[target.'cfg(target_os = "linux")'.dependencies] +libpulse-simple-binding = "2.16" +libpulse-binding = "2.16" +rust-pulsectl = { path = "libs/pulsectl" } +ctrlc = "3.1" + +[target.'cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))'.dependencies] +psutil = "3.2" + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.9" + +[workspace] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo"] + +[package.metadata.winres] +LegalCopyright = "Copyright © 2020" +# this FileDescription overrides package.description +FileDescription = "RustDesk" + +[target.'cfg(target_os="windows")'.build-dependencies] +winres = "0.1" +winapi = { version = "0.3", features = [ "winnt" ] } + +[build-dependencies] +cc = "1.0" +hbb_common = { path = "libs/hbb_common" } + +[dev-dependencies] +hound = "3.4" + +[package.metadata.bundle] +name = "RustDesk" +identifier = "com.carriez.rustdesk" +icon = ["32x32.png", "128x128.png", "128x128@2x.png"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio"] +osx_minimum_system_version = "10.14" + +#https://github.com/johnthagen/min-sized-rust +#!!! rembember call "strip target/release/rustdesk" +# which reduce binary size a lot +[profile.release] +#lto = true +#codegen-units = 1 +#panic = 'abort' +#opt-level = 'z' # only have smaller size after strip diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..f288702d2fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index a4b3b62cd74..6e69b88ea1b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,35 @@ ### RustDesk | Your Remote Desktop Software -This is a repository used to release RustDesk software and track issues. +The best open source remote desktop software written with Rust. -[**DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Dependence + +Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself. + +[Windows](https://github.com/c-smile/sciter-sdk/blob/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.win/x64/sciter.dll) +[Linux](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.lnx/x64/libsciter-gtk.so) +[Osx](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.osx/sciter-osx-64.dylib) + +## How To Build + +* Prepare your Rust development env and C++ build env + +* Install [vcpkg](https://github.com/microsoft/vcpkg), and set VCPKG_ROOT env variable correctly + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/Osx: vcpkg install libvpx libyuv opus + +* cargo run + +## File Structure + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code diff --git a/build.rs b/build.rs new file mode 100644 index 00000000000..7127fa0fa5d --- /dev/null +++ b/build.rs @@ -0,0 +1,74 @@ +#[cfg(windows)] +fn build_windows() { + cc::Build::new().file("src/windows.cc").compile("windows"); + // println!("cargo:rustc-link-lib=WtsApi32"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=windows.cc"); +} + +#[cfg(all(windows, feature = "inline"))] +fn build_manifest() { + use std::io::Write; + if std::env::var("PROFILE").unwrap() == "release" { + let mut res = winres::WindowsResource::new(); + res.set_icon("icon.ico") + .set_language(winapi::um::winnt::MAKELANGID( + winapi::um::winnt::LANG_ENGLISH, + winapi::um::winnt::SUBLANG_ENGLISH_US, + )) + .set_manifest_file("manifest.xml"); + match res.compile() { + Err(e) => { + write!(std::io::stderr(), "{}", e).unwrap(); + std::process::exit(1); + } + Ok(_) => {} + } + } +} + +fn install_oboe() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os != "android" { + return; + } + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let target = format!("{}-android-static", target_arch); + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: std::path::PathBuf = vcpkg_root.into(); + path.push("installed"); + path.push(target); + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + println!("cargo:rustc-link-lib=oboe"); + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=OpenSLES"); + // I always got some strange link error with oboe, so as workaround, put oboe.cc into oboe src: src/common/AudioStreamBuilder.cpp + // also to avoid libc++_shared not found issue, cp ndk's libc++_shared.so to jniLibs, e.g. + // ./flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/libc++_shared.so + // let include = path.join("include"); + //cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper"); +} + +fn main() { + #[cfg(all(windows, feature = "inline"))] + build_manifest(); + #[cfg(windows)] + build_windows(); + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-lib=framework=ApplicationServices"); + hbb_common::gen_version(); + install_oboe(); +} diff --git a/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md b/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..a5a2f77a5c7 --- /dev/null +++ b/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug, needs investigation +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps or a minimal code example to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - OS: [e.g. Linux, Windows, macOS ..] + - Rust [e.g. rustc --version] + - Library Version [e.g. enigo 0.0.13 or commit hash fa448be ] + +**Additional context** +Add any other context about the problem here. diff --git a/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md b/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..4cd9c3fc106 --- /dev/null +++ b/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement, needs investigation +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/libs/enigo/.github/ISSUE_TEMPLATE/question.md b/libs/enigo/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000000..ef240a0525e --- /dev/null +++ b/libs/enigo/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,19 @@ +--- +name: Question +about: Ask your Question here +title: '' +labels: question +assignees: '' + +--- + +**Describe your Question** +A clear and concise description of what you want to know. + +**Describe your Goal** +A clear and concise description of what you want to achieve. Consider the [XYProblem](http://xyproblem.info/) + +**Environment (please complete the following information):** + - OS: [e.g. Linux, Windows, macOS ..] + - Rust [e.g. rustc --version] + - Library Version [e.g. enigo 0.0.13 or commit hash fa448be ] diff --git a/libs/enigo/.gitignore b/libs/enigo/.gitignore new file mode 100644 index 00000000000..0e497c2069b --- /dev/null +++ b/libs/enigo/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock + +# RustFmt files +**/*.rs.bk + +# intellij +.idea \ No newline at end of file diff --git a/libs/enigo/.travis.yml b/libs/enigo/.travis.yml new file mode 100644 index 00000000000..0152a83c758 --- /dev/null +++ b/libs/enigo/.travis.yml @@ -0,0 +1,15 @@ +language: rust +rust: + - stable + - beta + - nightly +matrix: + allow_failures: + - rust: nightly +before_install: + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get -qq update; fi + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get install -y libxdo-dev; fi +os: + - linux + - osx + diff --git a/libs/enigo/.vscode/launch.json b/libs/enigo/.vscode/launch.json new file mode 100644 index 00000000000..a7a40dcfe82 --- /dev/null +++ b/libs/enigo/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Debug", + "type": "gdb", + "request": "launch", + "target": "./target/debug/examples/keyboard", + "cwd": "${workspaceRoot}" + } + ] +} \ No newline at end of file diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml new file mode 100644 index 00000000000..6842dab5663 --- /dev/null +++ b/libs/enigo/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "enigo" +version = "0.0.14" +authors = ["Dustin Bensing "] +edition = "2018" +build = "build.rs" + +description = "Enigo lets you control your mouse and keyboard in an abstract way on different operating systems (currently only Linux, macOS, Win – Redox and *BSD planned)" +documentation = "https://docs.rs/enigo/" +homepage = "https://github.com/enigo-rs/enigo" +repository = "https://github.com/enigo-rs/enigo" +readme = "README.md" +keywords = ["input", "mouse", "testing", "keyboard", "automation"] +categories = ["development-tools::testing", "api-bindings", "hardware-support"] +license = "MIT" + +[badges] +travis-ci = { repository = "enigo-rs/enigo" } +appveyor = { repository = "pythoneer/enigo-85xiy" } + +[dependencies] +serde = { version = "1.0", optional = true } +serde_derive = { version = "1.0", optional = true } +log = "0.4" + +[features] +with_serde = ["serde", "serde_derive"] + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser", "winbase"] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-graphics = "0.22" +objc = "0.2" +unicode-segmentation = "1.6" + +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" + +[build-dependencies] +pkg-config = "0.3" diff --git a/libs/enigo/LICENSE b/libs/enigo/LICENSE new file mode 100644 index 00000000000..d4b9c099baa --- /dev/null +++ b/libs/enigo/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 pythoneer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/enigo/README.md b/libs/enigo/README.md new file mode 100644 index 00000000000..df6ca799756 --- /dev/null +++ b/libs/enigo/README.md @@ -0,0 +1,46 @@ +[![Build Status](https://travis-ci.org/enigo-rs/enigo.svg?branch=master)](https://travis-ci.org/enigo-rs/enigo) +[![Build status](https://ci.appveyor.com/api/projects/status/6cd00pajx4tvvl3e?svg=true)](https://ci.appveyor.com/project/pythoneer/enigo-85xiy) +[![Dependency Status](https://dependencyci.com/github/pythoneer/enigo/badge)](https://dependencyci.com/github/pythoneer/enigo) +[![Docs](https://docs.rs/enigo/badge.svg)](https://docs.rs/enigo) +[![Crates.io](https://img.shields.io/crates/v/enigo.svg)](https://crates.io/crates/enigo) +[![Discord chat](https://img.shields.io/discord/315925376486342657.svg)](https://discord.gg/Eb8CsnN) +[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/enigo-rs/Lobby) + + +# enigo +Cross platform input simulation in Rust! + +- [x] Linux (X11) mouse +- [x] Linux (X11) text +- [ ] Linux (Wayland) mouse +- [ ] Linux (Wayland) text +- [x] MacOS mouse +- [x] MacOS text +- [x] Win mouse +- [x] Win text +- [x] Custom Parser + + +```Rust +let mut enigo = Enigo::new(); + +enigo.mouse_move_to(500, 200); +enigo.mouse_click(MouseButton::Left); +enigo.key_sequence_parse("{+CTRL}a{-CTRL}{+SHIFT}Hello World{-SHIFT}"); +``` + +for more look at examples + +Runtime dependencies +-------------------- + +Linux users may have to install libxdo-dev. For example, on Ubuntu: + +```Bash +apt install libxdo-dev +``` +On Arch: + +```Bash +pacman -S xdotool +``` diff --git a/libs/enigo/appveyor.yml b/libs/enigo/appveyor.yml new file mode 100644 index 00000000000..af3142ad9d6 --- /dev/null +++ b/libs/enigo/appveyor.yml @@ -0,0 +1,121 @@ +# Appveyor configuration template for Rust using rustup for Rust installation +# https://github.com/starkat99/appveyor-rust + +## Operating System (VM environment) ## + +# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. +os: Visual Studio 2015 + +## Build Matrix ## + +# This configuration will setup a build for each channel & target combination (12 windows +# combinations in all). +# +# There are 3 channels: stable, beta, and nightly. +# +# Alternatively, the full version may be specified for the channel to build using that specific +# version (e.g. channel: 1.5.0) +# +# The values for target are the set of windows Rust build targets. Each value is of the form +# +# ARCH-pc-windows-TOOLCHAIN +# +# Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker +# toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for +# a description of the toolchain differences. +# See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of +# toolchains and host triples. +# +# Comment out channel/target combos you do not wish to build in CI. +# +# You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands +# and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly +# channels to enable unstable features when building for nightly. Or you could add additional +# matrix entries to test different combinations of features. +environment: + matrix: + +### MSVC Toolchains ### + + # Stable 64-bit MSVC + - channel: stable + target: x86_64-pc-windows-msvc + # Stable 32-bit MSVC + - channel: stable + target: i686-pc-windows-msvc + # Beta 64-bit MSVC + - channel: beta + target: x86_64-pc-windows-msvc + # Beta 32-bit MSVC + - channel: beta + target: i686-pc-windows-msvc + # Nightly 64-bit MSVC + - channel: nightly + target: x86_64-pc-windows-msvc + #cargoflags: --features "unstable" + # Nightly 32-bit MSVC + - channel: nightly + target: i686-pc-windows-msvc + #cargoflags: --features "unstable" + +### GNU Toolchains ### + + # Stable 64-bit GNU + - channel: stable + target: x86_64-pc-windows-gnu + # Stable 32-bit GNU + - channel: stable + target: i686-pc-windows-gnu + # Beta 64-bit GNU + - channel: beta + target: x86_64-pc-windows-gnu + # Beta 32-bit GNU + - channel: beta + target: i686-pc-windows-gnu + # Nightly 64-bit GNU + - channel: nightly + target: x86_64-pc-windows-gnu + #cargoflags: --features "unstable" + # Nightly 32-bit GNU + - channel: nightly + target: i686-pc-windows-gnu + #cargoflags: --features "unstable" + +### Allowed failures ### + +# See Appveyor documentation for specific details. In short, place any channel or targets you wish +# to allow build failures on (usually nightly at least is a wise choice). This will prevent a build +# or test failure in the matching channels/targets from failing the entire build. +matrix: + allow_failures: + - channel: nightly + +# If you only care about stable channel build failures, uncomment the following line: + #- channel: beta + +## Install Script ## + +# This is the most important part of the Appveyor configuration. This installs the version of Rust +# specified by the 'channel' and 'target' environment variables from the build matrix. This uses +# rustup to install Rust. +# +# For simple configurations, instead of using the build matrix, you can simply set the +# default-toolchain and default-host manually here. +install: + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain %channel% --default-host %target% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin + - rustc -vV + - cargo -vV + +## Build Script ## + +# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents +# the "directory does not contain a project or solution file" error. +build: false + +# Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs +#directly or perform other testing commands. Rust will automatically be placed in the PATH +# environment variable. +test_script: + - cargo test --verbose %cargoflags% diff --git a/libs/enigo/build.rs b/libs/enigo/build.rs new file mode 100644 index 00000000000..6672b22a8c0 --- /dev/null +++ b/libs/enigo/build.rs @@ -0,0 +1,61 @@ +#[cfg(target_os = "windows")] +fn main() {} + +#[cfg(target_os = "macos")] +fn main() {} + +#[cfg(target_os = "linux")] +use pkg_config; +#[cfg(target_os = "linux")] +use std::env; +#[cfg(target_os = "linux")] +use std::fs::File; +#[cfg(target_os = "linux")] +use std::io::Write; +#[cfg(target_os = "linux")] +use std::path::Path; + +#[cfg(target_os = "linux")] +fn main() { + let libraries = [ + "xext", + "gl", + "xcursor", + "xxf86vm", + "xft", + "xinerama", + "xi", + "x11", + "xlib_xcb", + "xmu", + "xrandr", + "xtst", + "xrender", + "xscrnsaver", + "xt", + ]; + + let mut config = String::new(); + for lib in libraries.iter() { + let libdir = match pkg_config::get_variable(lib, "libdir") { + Ok(libdir) => format!("Some(\"{}\")", libdir), + Err(_) => "None".to_string(), + }; + config.push_str(&format!( + "pub const {}: Option<&'static str> = {};\n", + lib, libdir + )); + } + let config = format!("pub mod config {{ pub mod libdir {{\n{}}}\n}}", config); + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("config.rs"); + let mut f = File::create(&dest_path).unwrap(); + f.write_all(&config.into_bytes()).unwrap(); + + let target = env::var("TARGET").unwrap(); + if target.contains("linux") { + println!("cargo:rustc-link-lib=dl"); + } else if target.contains("freebsd") || target.contains("dragonfly") { + println!("cargo:rustc-link-lib=c"); + } +} diff --git a/libs/enigo/examples/dsl.rs b/libs/enigo/examples/dsl.rs new file mode 100644 index 00000000000..e58ec44725c --- /dev/null +++ b/libs/enigo/examples/dsl.rs @@ -0,0 +1,11 @@ +use enigo::{Enigo, KeyboardControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + // write text and select all + enigo.key_sequence_parse("{+UNICODE}{{Hello World!}} ❤️{-UNICODE}{+CTRL}a{-CTRL}"); +} diff --git a/libs/enigo/examples/key.rs b/libs/enigo/examples/key.rs new file mode 100644 index 00000000000..c806451ef11 --- /dev/null +++ b/libs/enigo/examples/key.rs @@ -0,0 +1,12 @@ +use enigo::{Enigo, Key, KeyboardControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + enigo.key_down(Key::Layout('a')); + thread::sleep(Duration::from_secs(1)); + enigo.key_up(Key::Layout('a')); +} diff --git a/libs/enigo/examples/keyboard.rs b/libs/enigo/examples/keyboard.rs new file mode 100644 index 00000000000..f2c290356ac --- /dev/null +++ b/libs/enigo/examples/keyboard.rs @@ -0,0 +1,16 @@ +use enigo::{Enigo, Key, KeyboardControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + // write text + enigo.key_sequence("Hello World! here is a lot of text ❤️"); + + // select all + enigo.key_down(Key::Control); + enigo.key_click(Key::Layout('a')); + enigo.key_up(Key::Control); +} diff --git a/libs/enigo/examples/mouse.rs b/libs/enigo/examples/mouse.rs new file mode 100644 index 00000000000..7026b9af743 --- /dev/null +++ b/libs/enigo/examples/mouse.rs @@ -0,0 +1,37 @@ +use enigo::{Enigo, MouseButton, MouseControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + let wait_time = Duration::from_secs(2); + let mut enigo = Enigo::new(); + + thread::sleep(wait_time); + + enigo.mouse_move_to(500, 200); + thread::sleep(wait_time); + + enigo.mouse_down(MouseButton::Left); + thread::sleep(wait_time); + + enigo.mouse_move_relative(100, 100); + thread::sleep(wait_time); + + enigo.mouse_up(MouseButton::Left); + thread::sleep(wait_time); + + enigo.mouse_click(MouseButton::Left); + thread::sleep(wait_time); + + enigo.mouse_scroll_x(2); + thread::sleep(wait_time); + + enigo.mouse_scroll_x(-2); + thread::sleep(wait_time); + + enigo.mouse_scroll_y(2); + thread::sleep(wait_time); + + enigo.mouse_scroll_y(-2); + thread::sleep(wait_time); +} diff --git a/libs/enigo/examples/timer.rs b/libs/enigo/examples/timer.rs new file mode 100644 index 00000000000..5e451f3a8da --- /dev/null +++ b/libs/enigo/examples/timer.rs @@ -0,0 +1,22 @@ +use enigo::{Enigo, Key, KeyboardControllable}; +use std::thread; +use std::time::Duration; +use std::time::Instant; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + let now = Instant::now(); + + // write text + enigo.key_sequence("Hello World! ❤️"); + + let time = now.elapsed(); + println!("{:?}", time); + + // select all + enigo.key_down(Key::Control); + enigo.key_click(Key::Layout('a')); + enigo.key_up(Key::Control); +} diff --git a/libs/enigo/rustfmt.toml b/libs/enigo/rustfmt.toml new file mode 100644 index 00000000000..b2715b26833 --- /dev/null +++ b/libs/enigo/rustfmt.toml @@ -0,0 +1 @@ +wrap_comments = true diff --git a/libs/enigo/src/dsl.rs b/libs/enigo/src/dsl.rs new file mode 100644 index 00000000000..dfb8adbb622 --- /dev/null +++ b/libs/enigo/src/dsl.rs @@ -0,0 +1,184 @@ +use crate::{Key, KeyboardControllable}; +use std::error::Error; +use std::fmt; + +/// An error that can occur when parsing DSL +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + /// When a tag doesn't exist. + /// Example: {+TEST}{-TEST} + /// ^^^^ ^^^^ + UnknownTag(String), + + /// When a { is encountered inside a {TAG}. + /// Example: {+HELLO{WORLD} + /// ^ + UnexpectedOpen, + + /// When a { is never matched with a }. + /// Example: {+SHIFT}Hello{-SHIFT + /// ^ + UnmatchedOpen, + + /// Opposite of UnmatchedOpen. + /// Example: +SHIFT}Hello{-SHIFT} + /// ^ + UnmatchedClose, +} +impl Error for ParseError { + fn description(&self) -> &str { + match *self { + ParseError::UnknownTag(_) => "Unknown tag", + ParseError::UnexpectedOpen => "Unescaped open bracket ({) found inside tag name", + ParseError::UnmatchedOpen => "Unmatched open bracket ({). No matching close (})", + ParseError::UnmatchedClose => "Unmatched close bracket (}). No previous open ({)", + } + } +} +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_string()) + } +} + +/// Evaluate the DSL. This tokenizes the input and presses the keys. +pub fn eval(enigo: &mut K, input: &str) -> Result<(), ParseError> +where + K: KeyboardControllable, +{ + for token in tokenize(input)? { + match token { + Token::Sequence(buffer) => { + for key in buffer.chars() { + enigo.key_click(Key::Layout(key)); + } + } + Token::Unicode(buffer) => enigo.key_sequence(&buffer), + Token::KeyUp(key) => enigo.key_up(key), + Token::KeyDown(key) => enigo.key_down(key).unwrap_or(()), + } + } + Ok(()) +} + +#[derive(Debug, PartialEq, Eq)] +enum Token { + Sequence(String), + Unicode(String), + KeyUp(Key), + KeyDown(Key), +} + +fn tokenize(input: &str) -> Result, ParseError> { + let mut unicode = false; + + let mut tokens = Vec::new(); + let mut buffer = String::new(); + let mut iter = input.chars().peekable(); + + fn flush(tokens: &mut Vec, buffer: String, unicode: bool) { + if !buffer.is_empty() { + if unicode { + tokens.push(Token::Unicode(buffer)); + } else { + tokens.push(Token::Sequence(buffer)); + } + } + } + + while let Some(c) = iter.next() { + if c == '{' { + match iter.next() { + Some('{') => buffer.push('{'), + Some(mut c) => { + flush(&mut tokens, buffer, unicode); + buffer = String::new(); + + let mut tag = String::new(); + loop { + tag.push(c); + match iter.next() { + Some('{') => match iter.peek() { + Some(&'{') => { + iter.next(); + c = '{' + } + _ => return Err(ParseError::UnexpectedOpen), + }, + Some('}') => match iter.peek() { + Some(&'}') => { + iter.next(); + c = '}' + } + _ => break, + }, + Some(new) => c = new, + None => return Err(ParseError::UnmatchedOpen), + } + } + match &*tag { + "+UNICODE" => unicode = true, + "-UNICODE" => unicode = false, + "+SHIFT" => tokens.push(Token::KeyDown(Key::Shift)), + "-SHIFT" => tokens.push(Token::KeyUp(Key::Shift)), + "+CTRL" => tokens.push(Token::KeyDown(Key::Control)), + "-CTRL" => tokens.push(Token::KeyUp(Key::Control)), + "+META" => tokens.push(Token::KeyDown(Key::Meta)), + "-META" => tokens.push(Token::KeyUp(Key::Meta)), + "+ALT" => tokens.push(Token::KeyDown(Key::Alt)), + "-ALT" => tokens.push(Token::KeyUp(Key::Alt)), + _ => return Err(ParseError::UnknownTag(tag)), + } + } + None => return Err(ParseError::UnmatchedOpen), + } + } else if c == '}' { + match iter.next() { + Some('}') => buffer.push('}'), + _ => return Err(ParseError::UnmatchedClose), + } + } else { + buffer.push(c); + } + } + + flush(&mut tokens, buffer, unicode); + + Ok(tokens) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!( + tokenize("{{Hello World!}} {+CTRL}hi{-CTRL}"), + Ok(vec![ + Token::Sequence("{Hello World!} ".into()), + Token::KeyDown(Key::Control), + Token::Sequence("hi".into()), + Token::KeyUp(Key::Control) + ]) + ); + } + #[test] + fn unexpected_open() { + assert_eq!(tokenize("{hello{}world}"), Err(ParseError::UnexpectedOpen)); + } + #[test] + fn unmatched_open() { + assert_eq!( + tokenize("{this is going to fail"), + Err(ParseError::UnmatchedOpen) + ); + } + #[test] + fn unmatched_close() { + assert_eq!( + tokenize("{+CTRL}{{this}} is going to fail}"), + Err(ParseError::UnmatchedClose) + ); + } +} diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs new file mode 100644 index 00000000000..bf289c8ed68 --- /dev/null +++ b/libs/enigo/src/lib.rs @@ -0,0 +1,525 @@ +//! Enigo lets you simulate mouse and keyboard input-events as if they were +//! made by the actual hardware. The goal is to make it available on different +//! operating systems like Linux, macOS and Windows – possibly many more but +//! [Redox](https://redox-os.org/) and *BSD are planned. Please see the +//! [Repo](https://github.com/enigo-rs/enigo) for the current status. +//! +//! I consider this library in an early alpha status, the API will change in +//! in the future. The keyboard handling is far from being very usable. I plan +//! to build a simple +//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) +//! that will resemble something like: +//! +//! `"hello {+SHIFT}world{-SHIFT} and break line{ENTER}"` +//! +//! The current status is that you can just print +//! [unicode](http://unicode.org/) +//! characters like [emoji](http://getemoji.com/) without the `{+SHIFT}` +//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) +//! or any other "special" key on the Linux, macOS and Windows operating system. +//! +//! Possible use cases could be for testing user interfaces on different +//! plattforms, +//! building remote control applications or just automating tasks for user +//! interfaces unaccessible by a public API or scripting laguage. +//! +//! For the keyboard there are currently two modes you can use. The first mode +//! is represented by the [key_sequence]() function +//! its purpose is to simply write unicode characters. This is independent of +//! the keyboardlayout. Please note that +//! you're not be able to use modifier keys like Control +//! to influence the outcome. If you want to use modifier keys to e.g. +//! copy/paste +//! use the Layout variant. Please note that this is indeed layout dependent. + +//! # Examples +//! ```no_run +//! use enigo::*; +//! let mut enigo = Enigo::new(); +//! //paste +//! enigo.key_down(Key::Control); +//! enigo.key_click(Key::Layout('v')); +//! enigo.key_up(Key::Control); +//! ``` +//! +//! ```no_run +//! use enigo::*; +//! let mut enigo = Enigo::new(); +//! enigo.mouse_move_to(500, 200); +//! enigo.mouse_down(MouseButton::Left); +//! enigo.mouse_move_relative(100, 100); +//! enigo.mouse_up(MouseButton::Left); +//! enigo.key_sequence("hello world"); +//! ``` +#![deny(missing_docs)] + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; + +// TODO(dustin) use interior mutability not &mut self + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +pub use win::Enigo; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::Enigo; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use crate::linux::Enigo; + +/// DSL parser module +pub mod dsl; + +#[cfg(feature = "with_serde")] +#[macro_use] +extern crate serde_derive; + +#[cfg(feature = "with_serde")] +extern crate serde; + +/// +pub type ResultType = std::result::Result<(), Box>; + +#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq)] +/// MouseButton represents a mouse button, +/// and is used in for example +/// [mouse_click](trait.MouseControllable.html#tymethod.mouse_click). +/// WARNING: Types with the prefix Scroll +/// IS NOT intended to be used, and may not work on +/// all operating systems. +pub enum MouseButton { + /// Left mouse button + Left, + /// Middle mouse button + Middle, + /// Right mouse button + Right, + + /// Scroll up button + ScrollUp, + /// Left right button + ScrollDown, + /// Left right button + ScrollLeft, + /// Left right button + ScrollRight, +} + +/// Representing an interface and a set of mouse functions every +/// operating system implementation _should_ implement. +pub trait MouseControllable { + /// Lets the mouse cursor move to the specified x and y coordinates. + /// + /// The topleft corner of your monitor screen is x=0 y=0. Move + /// the cursor down the screen by increasing the y and to the right + /// by increasing x coordinate. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_move_to(500, 200); + /// ``` + fn mouse_move_to(&mut self, x: i32, y: i32); + + /// Lets the mouse cursor move the specified amount in the x and y + /// direction. + /// + /// The amount specified in the x and y parameters are added to the + /// current location of the mouse cursor. A positive x values lets + /// the mouse cursor move an amount of `x` pixels to the right. A negative + /// value for `x` lets the mouse cursor go to the left. A positive value + /// of y + /// lets the mouse cursor go down, a negative one lets the mouse cursor go + /// up. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_move_relative(100, 100); + /// ``` + fn mouse_move_relative(&mut self, x: i32, y: i32); + + /// Push down one of the mouse buttons + /// + /// Push down the mouse button specified by the parameter `button` of + /// type [MouseButton](enum.MouseButton.html) + /// and holds it until it is released by + /// [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). + /// Calls to [mouse_move_to](trait.MouseControllable.html#tymethod. + /// mouse_move_to) or + /// [mouse_move_relative](trait.MouseControllable.html#tymethod. + /// mouse_move_relative) + /// will work like expected and will e.g. drag widgets or highlight text. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_down(MouseButton::Left); + /// ``` + fn mouse_down(&mut self, button: MouseButton) -> ResultType; + + /// Lift up a pushed down mouse button + /// + /// Lift up a previously pushed down button (by invoking + /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down)). + /// If the button was not pushed down or consecutive calls without + /// invoking [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) + /// will emit lift up events. It depends on the + /// operating system whats actually happening – my guess is it will just + /// get ignored. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_up(MouseButton::Right); + /// ``` + fn mouse_up(&mut self, button: MouseButton); + + /// Click a mouse button + /// + /// it's esentially just a consecutive invokation of + /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed + /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just + /// for + /// convenience. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_click(MouseButton::Right); + /// ``` + fn mouse_click(&mut self, button: MouseButton); + + /// Scroll the mouse (wheel) left or right + /// + /// Positive numbers for length lets the mouse wheel scroll to the right + /// and negative ones to the left. The value that is specified translates + /// to `lines` defined by the operating system and is essentially one 15° + /// (click)rotation on the mouse wheel. How many lines it moves depends + /// on the current setting in the operating system. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_scroll_x(2); + /// ``` + fn mouse_scroll_x(&mut self, length: i32); + + /// Scroll the mouse (wheel) up or down + /// + /// Positive numbers for length lets the mouse wheel scroll down + /// and negative ones up. The value that is specified translates + /// to `lines` defined by the operating system and is essentially one 15° + /// (click)rotation on the mouse wheel. How many lines it moves depends + /// on the current setting in the operating system. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_scroll_y(2); + /// ``` + fn mouse_scroll_y(&mut self, length: i32); +} + +/// A key on the keyboard. +/// For alphabetical keys, use Key::Layout for a system independent key. +/// If a key is missing, you can use the raw keycode with Key::Raw. +#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Key { + /// alt key on Linux and Windows (option key on macOS) + Alt, + /// backspace key + Backspace, + /// caps lock key + CapsLock, + #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// command key on macOS (super key on Linux, windows key on Windows) + Command, + /// control key + Control, + /// delete key + Delete, + /// down arrow key + DownArrow, + /// end key + End, + /// escape key (esc) + Escape, + /// F1 key + F1, + /// F10 key + F10, + /// F11 key + F11, + /// F12 key + F12, + /// F2 key + F2, + /// F3 key + F3, + /// F4 key + F4, + /// F5 key + F5, + /// F6 key + F6, + /// F7 key + F7, + /// F8 key + F8, + /// F9 key + F9, + /// home key + Home, + /// left arrow key + LeftArrow, + /// meta key (also known as "windows", "super", and "command") + Meta, + /// option key on macOS (alt key on Linux and Windows) + Option, + /// page down key + PageDown, + /// page up key + PageUp, + /// return key + Return, + /// right arrow key + RightArrow, + /// shift key + Shift, + /// space key + Space, + #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// super key on linux (command key on macOS, windows key on Windows) + Super, + /// tab key (tabulator) + Tab, + /// up arrow key + UpArrow, + #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// windows key on Windows (super key on Linux, command key on macOS) + Windows, + /// + Numpad0, + /// + Numpad1, + /// + Numpad2, + /// + Numpad3, + /// + Numpad4, + /// + Numpad5, + /// + Numpad6, + /// + Numpad7, + /// + Numpad8, + /// + Numpad9, + /// + Cancel, + /// + Clear, + /// + Menu, + /// + Pause, + /// + Kana, + /// + Hangul, + /// + Junja, + /// + Final, + /// + Hanja, + /// + Kanji, + /// + Convert, + /// + Select, + /// + Print, + /// + Execute, + /// + Snapshot, + /// + Insert, + /// + Help, + /// + Sleep, + /// + Separator, + /// + VolumeUp, + /// + VolumeDown, + /// + Mute, + /// + Scroll, + /// scroll lock + NumLock, + /// + RWin, + /// + Apps, + /// + Multiply, + /// + Add, + /// + Subtract, + /// + Decimal, + /// + Divide, + /// + Equals, + /// + NumpadEnter, + /// + /// Function, /// mac + /// keyboard layout dependent key + Layout(char), + /// raw keycode eg 0x38 + Raw(u16), +} + +/// Representing an interface and a set of keyboard functions every +/// operating system implementation _should_ implement. +pub trait KeyboardControllable { + /// Types the string parsed with DSL. + /// + /// Typing {+SHIFT}hello{-SHIFT} becomes HELLO. + /// TODO: Full documentation + fn key_sequence_parse(&mut self, sequence: &str) + where + Self: Sized, + { + self.key_sequence_parse_try(sequence) + .expect("Could not parse sequence"); + } + /// Same as key_sequence_parse except returns any errors + fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError> + where + Self: Sized, + { + dsl::eval(self, sequence) + } + + /// Types the string + /// + /// Emits keystrokes such that the given string is inputted. + /// + /// You can use many unicode here like: ❤️. This works + /// regadless of the current keyboardlayout. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.key_sequence("hello world ❤️"); + /// ``` + fn key_sequence(&mut self, sequence: &str); + + /// presses a given key down + fn key_down(&mut self, key: Key) -> ResultType; + + /// release a given key formally pressed down by + /// [key_down](trait.KeyboardControllable.html#tymethod.key_down) + fn key_up(&mut self, key: Key); + + /// Much like the + /// [key_down](trait.KeyboardControllable.html#tymethod.key_down) and + /// [key_up](trait.KeyboardControllable.html#tymethod.key_up) + /// function they're just invoked consecutively + fn key_click(&mut self, key: Key); + + /// + fn get_key_state(&mut self, key: Key) -> bool; +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +struct Enigo; + +impl Enigo { + /// Constructs a new `Enigo` instance. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// ``` + pub fn new() -> Self { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Enigo{}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Self::default() + } +} + +use std::fmt; + +impl fmt::Debug for Enigo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Enigo") + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_key_state() { + let mut enigo = Enigo::new(); + let keys = [Key::CapsLock, Key::NumLock]; + for k in keys.iter() { + enigo.key_click(k.clone()); + let a = enigo.get_key_state(k.clone()); + enigo.key_click(k.clone()); + let b = enigo.get_key_state(k.clone()); + assert!(a != b); + } + let keys = [Key::Control, Key::Alt, Key::Shift]; + for k in keys.iter() { + enigo.key_down(k.clone()).ok(); + let a = enigo.get_key_state(k.clone()); + enigo.key_up(k.clone()); + let b = enigo.get_key_state(k.clone()); + assert!(a != b); + } + } +} diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs new file mode 100644 index 00000000000..0e8078b100b --- /dev/null +++ b/libs/enigo/src/linux.rs @@ -0,0 +1,361 @@ +use libc; + +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; + +use self::libc::{c_char, c_int, c_void, useconds_t}; +use std::{borrow::Cow, ffi::CString, ptr}; + +const CURRENT_WINDOW: c_int = 0; +const DEFAULT_DELAY: u64 = 12000; +type Window = c_int; +type Xdo = *const c_void; + +#[link(name = "xdo")] +extern "C" { + fn xdo_free(xdo: Xdo); + fn xdo_new(display: *const c_char) -> Xdo; + + fn xdo_click_window(xdo: Xdo, window: Window, button: c_int) -> c_int; + fn xdo_mouse_down(xdo: Xdo, window: Window, button: c_int) -> c_int; + fn xdo_mouse_up(xdo: Xdo, window: Window, button: c_int) -> c_int; + fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int; + fn xdo_move_mouse_relative(xdo: Xdo, x: c_int, y: c_int) -> c_int; + + fn xdo_enter_text_window( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_send_keysequence_window( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_send_keysequence_window_down( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_send_keysequence_window_up( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_get_input_state(xdo: Xdo) -> u32; +} + +fn mousebutton(button: MouseButton) -> c_int { + match button { + MouseButton::Left => 1, + MouseButton::Middle => 2, + MouseButton::Right => 3, + MouseButton::ScrollUp => 4, + MouseButton::ScrollDown => 5, + MouseButton::ScrollLeft => 6, + MouseButton::ScrollRight => 7, + } +} + +/// The main struct for handling the event emitting +pub struct Enigo { + xdo: Xdo, + delay: u64, +} +// This is safe, we have a unique pointer. +// TODO: use Unique once stable. +unsafe impl Send for Enigo {} + +impl Default for Enigo { + /// Create a new Enigo instance + fn default() -> Self { + Self { + xdo: unsafe { xdo_new(ptr::null()) }, + delay: DEFAULT_DELAY, + } + } +} +impl Enigo { + /// Get the delay per keypress. + /// Default value is 12000. + /// This is Linux-specific. + pub fn delay(&self) -> u64 { + self.delay + } + /// Set the delay per keypress. + /// This is Linux-specific. + pub fn set_delay(&mut self, delay: u64) { + self.delay = delay; + } +} +impl Drop for Enigo { + fn drop(&mut self) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_free(self.xdo); + } + } +} +impl MouseControllable for Enigo { + fn mouse_move_to(&mut self, x: i32, y: i32) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_move_mouse(self.xdo, x as c_int, y as c_int, 0); + } + } + fn mouse_move_relative(&mut self, x: i32, y: i32) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_move_mouse_relative(self.xdo, x as c_int, y as c_int); + } + } + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + if self.xdo.is_null() { + return Ok(()); + } + unsafe { + xdo_mouse_down(self.xdo, CURRENT_WINDOW, mousebutton(button)); + } + Ok(()) + } + fn mouse_up(&mut self, button: MouseButton) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_mouse_up(self.xdo, CURRENT_WINDOW, mousebutton(button)); + } + } + fn mouse_click(&mut self, button: MouseButton) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_click_window(self.xdo, CURRENT_WINDOW, mousebutton(button)); + } + } + fn mouse_scroll_x(&mut self, length: i32) { + let button; + let mut length = length; + + if length < 0 { + button = MouseButton::ScrollLeft; + } else { + button = MouseButton::ScrollRight; + } + + if length < 0 { + length = -length; + } + + for _ in 0..length { + self.mouse_click(button); + } + } + fn mouse_scroll_y(&mut self, length: i32) { + let button; + let mut length = length; + + if length < 0 { + button = MouseButton::ScrollUp; + } else { + button = MouseButton::ScrollDown; + } + + if length < 0 { + length = -length; + } + + for _ in 0..length { + self.mouse_click(button); + } + } +} +fn keysequence<'a>(key: Key) -> Cow<'a, str> { + if let Key::Layout(c) = key { + return Cow::Owned(format!("U{:X}", c as u32)); + } + if let Key::Raw(k) = key { + return Cow::Owned(format!("{}", k as u16)); + } + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + // https://www.rubydoc.info/gems/xdo/XDo/Keyboard + // https://gitlab.com/cunidev/gestures/-/wikis/xdotool-list-of-key-codes + Cow::Borrowed(match key { + Key::Alt => "Alt", + Key::Backspace => "BackSpace", + Key::CapsLock => "Caps_Lock", + Key::Control => "Control", + Key::Delete => "Delete", + Key::DownArrow => "Down", + Key::End => "End", + Key::Escape => "Escape", + Key::F1 => "F1", + Key::F10 => "F10", + Key::F11 => "F11", + Key::F12 => "F12", + Key::F2 => "F2", + Key::F3 => "F3", + Key::F4 => "F4", + Key::F5 => "F5", + Key::F6 => "F6", + Key::F7 => "F7", + Key::F8 => "F8", + Key::F9 => "F9", + Key::Home => "Home", + //Key::Layout(_) => unreachable!(), + Key::LeftArrow => "Left", + Key::Option => "Option", + Key::PageDown => "Page_Down", + Key::PageUp => "Page_Up", + //Key::Raw(_) => unreachable!(), + Key::Return => "Return", + Key::RightArrow => "Right", + Key::Shift => "Shift", + Key::Space => "space", + Key::Tab => "Tab", + Key::UpArrow => "Up", + Key::Numpad0 => "U30", //"KP_0", + Key::Numpad1 => "U31", //"KP_1", + Key::Numpad2 => "U32", //"KP_2", + Key::Numpad3 => "U33", //"KP_3", + Key::Numpad4 => "U34", //"KP_4", + Key::Numpad5 => "U35", //"KP_5", + Key::Numpad6 => "U36", //"KP_6", + Key::Numpad7 => "U37", //"KP_7", + Key::Numpad8 => "U38", //"KP_8", + Key::Numpad9 => "U39", //"KP_9", + Key::Decimal => "U2E", //"KP_Decimal", + Key::Cancel => "Cancel", + Key::Clear => "Clear", + Key::Menu => "Menu", + Key::Pause => "Pause", + Key::Kana => "Kana", + Key::Hangul => "Hangul", + Key::Junja => "", + Key::Final => "", + Key::Hanja => "Hanja", + Key::Kanji => "Kanji", + Key::Convert => "", + Key::Select => "Select", + Key::Print => "Print", + Key::Execute => "Execute", + Key::Snapshot => "3270_PrintScreen", + Key::Insert => "Insert", + Key::Help => "Help", + Key::Sleep => "", + Key::Separator => "KP_Separator", + Key::VolumeUp => "", + Key::VolumeDown => "", + Key::Mute => "", + Key::Scroll => "Scroll_Lock", + Key::NumLock => "Num_Lock", + Key::RWin => "", + Key::Apps => "", + Key::Multiply => "KP_Multiply", + Key::Add => "KP_Add", + Key::Subtract => "KP_Subtract", + Key::Divide => "KP_Divide", + Key::Equals => "KP_Equal", + Key::NumpadEnter => "KP_Enter", + + Key::Command | Key::Super | Key::Windows | Key::Meta => "Super", + + _ => "", + }) +} +impl KeyboardControllable for Enigo { + fn get_key_state(&mut self, key: Key) -> bool { + if self.xdo.is_null() { + return false; + } + let mod_shift = 1 << 0; + let mod_lock = 1 << 1; + let mod_control = 1 << 2; + let mod_alt = 1 << 3; + let mod_numlock = 1 << 4; + let mod_meta = 1 << 6; + let mask = unsafe { xdo_get_input_state(self.xdo) }; + // println!("{:b}", mask); + match key { + Key::Shift => mask & mod_shift != 0, + Key::CapsLock => mask & mod_lock != 0, + Key::Control => mask & mod_control != 0, + Key::Alt => mask & mod_alt != 0, + Key::NumLock => mask & mod_numlock != 0, + Key::Meta => mask & mod_meta != 0, + _ => false, + } + } + + fn key_sequence(&mut self, sequence: &str) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(sequence) { + unsafe { + xdo_enter_text_window( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + } + } + fn key_down(&mut self, key: Key) -> crate::ResultType { + if self.xdo.is_null() { + return Ok(()); + } + let string = CString::new(&*keysequence(key))?; + unsafe { + xdo_send_keysequence_window_down( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + Ok(()) + } + fn key_up(&mut self, key: Key) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(&*keysequence(key)) { + unsafe { + xdo_send_keysequence_window_up( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + } + } + fn key_click(&mut self, key: Key) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(&*keysequence(key)) { + unsafe { + xdo_send_keysequence_window( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + } + } +} diff --git a/libs/enigo/src/macos/keycodes.rs b/libs/enigo/src/macos/keycodes.rs new file mode 100644 index 00000000000..e946789b173 --- /dev/null +++ b/libs/enigo/src/macos/keycodes.rs @@ -0,0 +1,72 @@ +// https://stackoverflow.com/questions/3202629/where-can-i-find-a-list-of-mac-virtual-key-codes + +/* keycodes for keys that are independent of keyboard layout */ + +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +pub const kVK_Return: u16 = 0x24; +pub const kVK_Tab: u16 = 0x30; +pub const kVK_Space: u16 = 0x31; +pub const kVK_Delete: u16 = 0x33; +pub const kVK_Escape: u16 = 0x35; +pub const kVK_Command: u16 = 0x37; +pub const kVK_Shift: u16 = 0x38; +pub const kVK_CapsLock: u16 = 0x39; +pub const kVK_Option: u16 = 0x3A; +pub const kVK_Control: u16 = 0x3B; +pub const kVK_RightShift: u16 = 0x3C; +pub const kVK_RightOption: u16 = 0x3D; +pub const kVK_RightControl: u16 = 0x3E; +pub const kVK_Function: u16 = 0x3F; +pub const kVK_F17: u16 = 0x40; +pub const kVK_VolumeUp: u16 = 0x48; +pub const kVK_VolumeDown: u16 = 0x49; +pub const kVK_Mute: u16 = 0x4A; +pub const kVK_F18: u16 = 0x4F; +pub const kVK_F19: u16 = 0x50; +pub const kVK_F20: u16 = 0x5A; +pub const kVK_F5: u16 = 0x60; +pub const kVK_F6: u16 = 0x61; +pub const kVK_F7: u16 = 0x62; +pub const kVK_F3: u16 = 0x63; +pub const kVK_F8: u16 = 0x64; +pub const kVK_F9: u16 = 0x65; +pub const kVK_F11: u16 = 0x67; +pub const kVK_F13: u16 = 0x69; +pub const kVK_F16: u16 = 0x6A; +pub const kVK_F14: u16 = 0x6B; +pub const kVK_F10: u16 = 0x6D; +pub const kVK_F12: u16 = 0x6F; +pub const kVK_F15: u16 = 0x71; +pub const kVK_Help: u16 = 0x72; +pub const kVK_Home: u16 = 0x73; +pub const kVK_PageUp: u16 = 0x74; +pub const kVK_ForwardDelete: u16 = 0x75; +pub const kVK_F4: u16 = 0x76; +pub const kVK_End: u16 = 0x77; +pub const kVK_F2: u16 = 0x78; +pub const kVK_PageDown: u16 = 0x79; +pub const kVK_F1: u16 = 0x7A; +pub const kVK_LeftArrow: u16 = 0x7B; +pub const kVK_RightArrow: u16 = 0x7C; +pub const kVK_DownArrow: u16 = 0x7D; +pub const kVK_UpArrow: u16 = 0x7E; +pub const kVK_ANSI_Keypad0: u16 = 0x52; +pub const kVK_ANSI_Keypad1: u16 = 0x53; +pub const kVK_ANSI_Keypad2: u16 = 0x54; +pub const kVK_ANSI_Keypad3: u16 = 0x55; +pub const kVK_ANSI_Keypad4: u16 = 0x56; +pub const kVK_ANSI_Keypad5: u16 = 0x57; +pub const kVK_ANSI_Keypad6: u16 = 0x58; +pub const kVK_ANSI_Keypad7: u16 = 0x59; +pub const kVK_ANSI_Keypad8: u16 = 0x5B; +pub const kVK_ANSI_Keypad9: u16 = 0x5C; +pub const kVK_ANSI_KeypadClear: u16 = 0x47; +pub const kVK_ANSI_KeypadDecimal: u16 = 0x41; +pub const kVK_ANSI_KeypadMultiply: u16 = 0x43; +pub const kVK_ANSI_KeypadPlus: u16 = 0x45; +pub const kVK_ANSI_KeypadDivide: u16 = 0x4B; +pub const kVK_ANSI_KeypadEnter: u16 = 0x4C; +pub const kVK_ANSI_KeypadMinus: u16 = 0x4E; +pub const kVK_ANSI_KeypadEquals: u16 = 0x51; diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs new file mode 100644 index 00000000000..746526dcd7b --- /dev/null +++ b/libs/enigo/src/macos/macos_impl.rs @@ -0,0 +1,680 @@ +use core_graphics; + +// TODO(dustin): use only the things i need + +use self::core_graphics::display::*; +use self::core_graphics::event::*; +use self::core_graphics::event_source::*; + +use crate::macos::keycodes::*; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use objc::runtime::Class; +use std::ffi::CStr; +use std::os::raw::*; + +// required for pressedMouseButtons on NSEvent +#[link(name = "AppKit", kind = "framework")] +extern "C" {} + +struct MyCGEvent; + +#[allow(improper_ctypes)] +#[allow(non_snake_case)] +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent); + // not present in servo/core-graphics + fn CGEventCreateScrollWheelEvent( + source: &CGEventSourceRef, + units: ScrollUnit, + wheelCount: u32, + wheel1: i32, + ... + ) -> *mut MyCGEvent; + fn CGEventSourceKeyState(stateID: i32, key: u16) -> bool; +} + +pub type CFDataRef = *const c_void; + +#[repr(C)] +#[derive(Clone, Copy)] +struct NSPoint { + x: f64, + y: f64, +} + +#[repr(C)] +pub struct __TISInputSource; +pub type TISInputSourceRef = *const __TISInputSource; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct __CFString([u8; 0]); +pub type CFStringRef = *const __CFString; +pub type Boolean = c_uchar; +pub type UInt8 = c_uchar; +pub type SInt32 = c_int; +pub type UInt16 = c_ushort; +pub type UInt32 = c_uint; +pub type UniChar = UInt16; +pub type UniCharCount = c_ulong; + +pub type OptionBits = UInt32; +pub type OSStatus = SInt32; + +pub type CFStringEncoding = UInt32; + +#[allow(non_upper_case_globals)] +pub const kUCKeyActionDisplay: _bindgen_ty_702 = _bindgen_ty_702::kUCKeyActionDisplay; + +#[allow(non_camel_case_types)] +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum _bindgen_ty_702 { + // kUCKeyActionDown = 0, + // kUCKeyActionUp = 1, + // kUCKeyActionAutoKey = 2, + kUCKeyActionDisplay = 3, +} + +#[allow(non_snake_case)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct UCKeyboardTypeHeader { + pub keyboardTypeFirst: UInt32, + pub keyboardTypeLast: UInt32, + pub keyModifiersToTableNumOffset: UInt32, + pub keyToCharTableIndexOffset: UInt32, + pub keyStateRecordsIndexOffset: UInt32, + pub keyStateTerminatorsOffset: UInt32, + pub keySequenceDataIndexOffset: UInt32, +} + +#[allow(non_snake_case)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct UCKeyboardLayout { + pub keyLayoutHeaderFormat: UInt16, + pub keyLayoutDataVersion: UInt16, + pub keyLayoutFeatureInfoOffset: UInt32, + pub keyboardTypeCount: UInt32, + pub keyboardTypeList: [UCKeyboardTypeHeader; 1usize], +} + +#[allow(non_upper_case_globals)] +pub const kUCKeyTranslateNoDeadKeysBit: _bindgen_ty_703 = + _bindgen_ty_703::kUCKeyTranslateNoDeadKeysBit; + +#[allow(non_camel_case_types)] +#[repr(u32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum _bindgen_ty_703 { + kUCKeyTranslateNoDeadKeysBit = 0, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct __CFAllocator([u8; 0]); +pub type CFAllocatorRef = *const __CFAllocator; + +// #[repr(u32)] +// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +// pub enum _bindgen_ty_15 { +// kCFStringEncodingMacRoman = 0, +// kCFStringEncodingWindowsLatin1 = 1280, +// kCFStringEncodingISOLatin1 = 513, +// kCFStringEncodingNextStepLatin = 2817, +// kCFStringEncodingASCII = 1536, +// kCFStringEncodingUnicode = 256, +// kCFStringEncodingUTF8 = 134217984, +// kCFStringEncodingNonLossyASCII = 3071, +// kCFStringEncodingUTF16BE = 268435712, +// kCFStringEncodingUTF16LE = 335544576, +// kCFStringEncodingUTF32 = 201326848, +// kCFStringEncodingUTF32BE = 402653440, +// kCFStringEncodingUTF32LE = 469762304, +// } + +#[allow(non_upper_case_globals)] +pub const kCFStringEncodingUTF8: u32 = 134_217_984; + +#[allow(improper_ctypes)] +#[link(name = "Carbon", kind = "framework")] +extern "C" { + fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; + + // extern void * + // TISGetInputSourceProperty( + // TISInputSourceRef inputSource, + // CFStringRef propertyKey) + + #[allow(non_upper_case_globals)] + #[link_name = "kTISPropertyUnicodeKeyLayoutData"] + pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + + #[allow(non_snake_case)] + pub fn TISGetInputSourceProperty( + inputSource: TISInputSourceRef, + propertyKey: CFStringRef, + ) -> *mut c_void; + + #[allow(non_snake_case)] + pub fn CFDataGetBytePtr(theData: CFDataRef) -> *const UInt8; + + #[allow(non_snake_case)] + pub fn UCKeyTranslate( + keyLayoutPtr: *const UInt8, //*const UCKeyboardLayout, + virtualKeyCode: UInt16, + keyAction: UInt16, + modifierKeyState: UInt32, + keyboardType: UInt32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut UInt32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut UniChar, + ) -> OSStatus; + + pub fn LMGetKbdType() -> UInt8; + + #[allow(non_snake_case)] + pub fn CFStringCreateWithCharacters( + alloc: CFAllocatorRef, + chars: *const UniChar, + numChars: CFIndex, + ) -> CFStringRef; + + #[allow(non_upper_case_globals)] + #[link_name = "kCFAllocatorDefault"] + pub static kCFAllocatorDefault: CFAllocatorRef; + + #[allow(non_snake_case)] + pub fn CFStringGetCString( + theString: CFStringRef, + buffer: *mut c_char, + bufferSize: CFIndex, + encoding: CFStringEncoding, + ) -> Boolean; +} + +// not present in servo/core-graphics +#[allow(dead_code)] +#[derive(Debug)] +enum ScrollUnit { + Pixel = 0, + Line = 1, +} +// hack + +/// The main struct for handling the event emitting +pub struct Enigo { + event_source: Option, + keycode_to_string_map: std::collections::HashMap, + double_click_interval: u32, + last_click_time: Option, + multiple_click: i64, + flags: CGEventFlags, +} + +impl Enigo { + /// + pub fn reset_flag(&mut self) { + self.flags = CGEventFlags::CGEventFlagNull; + } + + /// + pub fn add_flag(&mut self, key: &Key) { + let flag = match key { + &Key::CapsLock => CGEventFlags::CGEventFlagAlphaShift, + &Key::Shift => CGEventFlags::CGEventFlagShift, + &Key::Control => CGEventFlags::CGEventFlagControl, + &Key::Alt => CGEventFlags::CGEventFlagAlternate, + &Key::Meta => CGEventFlags::CGEventFlagCommand, + &Key::NumLock => CGEventFlags::CGEventFlagNumericPad, + _ => CGEventFlags::CGEventFlagNull, + }; + self.flags |= flag; + } + + fn post(&self, event: CGEvent) { + event.set_flags(self.flags); + event.post(CGEventTapLocation::HID); + } +} + +impl Default for Enigo { + fn default() -> Self { + let mut double_click_interval = 500; + if let Some(ns_event) = Class::get("NSEvent") { + let tm: f64 = unsafe { msg_send![ns_event, doubleClickInterval] }; + if tm > 0. { + double_click_interval = (tm * 1000.) as u32; + log::info!("double click interval: {}ms", double_click_interval); + } + } + Self { + // TODO(dustin): return error rather than panic here + event_source: if let Ok(src) = + CGEventSource::new(CGEventSourceStateID::CombinedSessionState) + { + Some(src) + } else { + None + }, + keycode_to_string_map: Default::default(), + double_click_interval, + multiple_click: 1, + last_click_time: None, + flags: CGEventFlags::CGEventFlagNull, + } + } +} + +impl MouseControllable for Enigo { + fn mouse_move_to(&mut self, x: i32, y: i32) { + let pressed = Self::pressed_buttons(); + + let event_type = if pressed & 1 > 0 { + CGEventType::LeftMouseDragged + } else if pressed & 2 > 0 { + CGEventType::RightMouseDragged + } else { + CGEventType::MouseMoved + }; + + let dest = CGPoint::new(x as f64, y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left) + { + self.post(event); + } + } + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + let (display_width, display_height) = Self::main_display_size(); + let (current_x, y_inv) = Self::mouse_location_raw_coords(); + let current_y = (display_height as i32) - y_inv; + let new_x = current_x + x; + let new_y = current_y + y; + + if new_x < 0 + || new_x as usize > display_width + || new_y < 0 + || new_y as usize > display_height + { + return; + } + + self.mouse_move_to(new_x, new_y); + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + let now = std::time::Instant::now(); + if let Some(t) = self.last_click_time { + if t.elapsed().as_millis() as u32 <= self.double_click_interval { + self.multiple_click += 1; + } else { + self.multiple_click = 1; + } + } + self.last_click_time = Some(now); + let (current_x, current_y) = Self::mouse_location(); + let (button, event_type) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), + _ => unimplemented!(), + }; + let dest = CGPoint::new(current_x as f64, current_y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) { + if self.multiple_click > 1 { + event.set_integer_value_field( + EventField::MOUSE_EVENT_CLICK_STATE, + self.multiple_click, + ); + } + self.post(event); + } + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + let (current_x, current_y) = Self::mouse_location(); + let (button, event_type) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), + _ => unimplemented!(), + }; + let dest = CGPoint::new(current_x as f64, current_y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) { + if self.multiple_click > 1 { + event.set_integer_value_field( + EventField::MOUSE_EVENT_CLICK_STATE, + self.multiple_click, + ); + } + self.post(event); + } + } + } + + fn mouse_click(&mut self, button: MouseButton) { + self.mouse_down(button).ok(); + self.mouse_up(button); + } + + fn mouse_scroll_x(&mut self, length: i32) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let mouse_ev = CGEventCreateScrollWheelEvent( + &src, + ScrollUnit::Line, + 2, // CGWheelCount 1 = y 2 = xy 3 = xyz + 0, + scroll_direction, + ); + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } + + fn mouse_scroll_y(&mut self, length: i32) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let mouse_ev = CGEventCreateScrollWheelEvent( + &src, + ScrollUnit::Line, + 1, // CGWheelCount 1 = y 2 = xy 3 = xyz + scroll_direction, + ); + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } +} + +// https://stackoverflow. +// com/questions/1918841/how-to-convert-ascii-character-to-cgkeycode + +impl KeyboardControllable for Enigo { + fn key_sequence(&mut self, sequence: &str) { + // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 + // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time + // but i am unsure what would happen for grapheme clusters greater than 20 bytes ... + use unicode_segmentation::UnicodeSegmentation; + let clusters = UnicodeSegmentation::graphemes(sequence, true).collect::>(); + for cluster in clusters { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), 0, true) { + event.set_string(cluster); + self.post(event); + } + } + } + } + + fn key_click(&mut self, key: Key) { + let keycode = self.key_to_keycode(key); + if keycode == 0 { + return; + } + + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, true) { + self.post(event); + } + + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, false) { + self.post(event); + } + } + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), true) + { + self.post(event); + } + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false) + { + self.post(event); + } + } + } + + fn get_key_state(&mut self, key: Key) -> bool { + let keycode = self.key_to_keycode(key); + unsafe { CGEventSourceKeyState(1, keycode) } + } +} + +impl Enigo { + fn pressed_buttons() -> usize { + if let Some(ns_event) = Class::get("NSEvent") { + unsafe { msg_send![ns_event, pressedMouseButtons] } + } else { + 0 + } + } + + /// Fetches the `(width, height)` in pixels of the main display + pub fn main_display_size() -> (usize, usize) { + let display_id = unsafe { CGMainDisplayID() }; + let width = unsafe { CGDisplayPixelsWide(display_id) }; + let height = unsafe { CGDisplayPixelsHigh(display_id) }; + (width, height) + } + + /// Returns the current mouse location in Cocoa coordinates which have Y + /// inverted from the Carbon coordinates used in the rest of the API. + /// This function exists so that mouse_move_relative only has to fetch + /// the screen size once. + fn mouse_location_raw_coords() -> (i32, i32) { + if let Some(ns_event) = Class::get("NSEvent") { + let pt: NSPoint = unsafe { msg_send![ns_event, mouseLocation] }; + (pt.x as i32, pt.y as i32) + } else { + (0, 0) + } + } + + /// The mouse coordinates in points, only works on the main display + pub fn mouse_location() -> (i32, i32) { + let (x, y_inv) = Self::mouse_location_raw_coords(); + let (_, display_height) = Self::main_display_size(); + (x, (display_height as i32) - y_inv) + } + + fn key_to_keycode(&mut self, key: Key) -> CGKeyCode { + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + match key { + Key::Alt => kVK_Option, + Key::Backspace => kVK_Delete, + Key::CapsLock => kVK_CapsLock, + Key::Control => kVK_Control, + Key::Delete => kVK_ForwardDelete, + Key::DownArrow => kVK_DownArrow, + Key::End => kVK_End, + Key::Escape => kVK_Escape, + Key::F1 => kVK_F1, + Key::F10 => kVK_F10, + Key::F11 => kVK_F11, + Key::F12 => kVK_F12, + Key::F2 => kVK_F2, + Key::F3 => kVK_F3, + Key::F4 => kVK_F4, + Key::F5 => kVK_F5, + Key::F6 => kVK_F6, + Key::F7 => kVK_F7, + Key::F8 => kVK_F8, + Key::F9 => kVK_F9, + Key::Home => kVK_Home, + Key::LeftArrow => kVK_LeftArrow, + Key::Option => kVK_Option, + Key::PageDown => kVK_PageDown, + Key::PageUp => kVK_PageUp, + Key::Return => kVK_Return, + Key::RightArrow => kVK_RightArrow, + Key::Shift => kVK_Shift, + Key::Space => kVK_Space, + Key::Tab => kVK_Tab, + Key::UpArrow => kVK_UpArrow, + Key::Numpad0 => kVK_ANSI_Keypad0, + Key::Numpad1 => kVK_ANSI_Keypad1, + Key::Numpad2 => kVK_ANSI_Keypad2, + Key::Numpad3 => kVK_ANSI_Keypad3, + Key::Numpad4 => kVK_ANSI_Keypad4, + Key::Numpad5 => kVK_ANSI_Keypad5, + Key::Numpad6 => kVK_ANSI_Keypad6, + Key::Numpad7 => kVK_ANSI_Keypad7, + Key::Numpad8 => kVK_ANSI_Keypad8, + Key::Numpad9 => kVK_ANSI_Keypad9, + Key::Mute => kVK_Mute, + Key::VolumeDown => kVK_VolumeUp, + Key::VolumeUp => kVK_VolumeDown, + Key::Help => kVK_Help, + Key::Snapshot => kVK_F13, + Key::Clear => kVK_ANSI_KeypadClear, + Key::Decimal => kVK_ANSI_KeypadDecimal, + Key::Multiply => kVK_ANSI_KeypadMultiply, + Key::Add => kVK_ANSI_KeypadPlus, + Key::Divide => kVK_ANSI_KeypadDivide, + Key::NumpadEnter => kVK_ANSI_KeypadEnter, + Key::Subtract => kVK_ANSI_KeypadMinus, + Key::Equals => kVK_ANSI_KeypadEquals, + Key::NumLock => kVK_ANSI_KeypadClear, + + Key::Raw(raw_keycode) => raw_keycode, + Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()), + + Key::Super | Key::Command | Key::Windows | Key::Meta => kVK_Command, + _ => 0, + } + } + + fn get_layoutdependent_keycode(&mut self, string: String) -> CGKeyCode { + if self.keycode_to_string_map.is_empty() { + self.init_map(); + } + *self.keycode_to_string_map.get(&string).unwrap_or(&0) + } + + fn init_map(&mut self) { + self.keycode_to_string_map.insert("".to_owned(), 0); + // loop through every keycode (0 - 127) + for keycode in 0..128 { + // no modifier + if let Some(key_string) = self.keycode_to_string(keycode, 0x100) { + self.keycode_to_string_map.insert(key_string, keycode); + } + + // shift modifier + if let Some(key_string) = self.keycode_to_string(keycode, 0x20102) { + self.keycode_to_string_map.insert(key_string, keycode); + } + + // alt modifier + // if let Some(string) = self.keycode_to_string(keycode, 0x80120) { + // println!("{:?}", string); + // } + // alt + shift modifier + // if let Some(string) = self.keycode_to_string(keycode, 0xa0122) { + // println!("{:?}", string); + // } + } + } + + fn keycode_to_string(&self, keycode: u16, modifier: u32) -> Option { + let cf_string = self.create_string_for_key(keycode, modifier); + unsafe { + if !cf_string.is_null() { + let mut buf: [i8; 255] = [0; 255]; + let success = CFStringGetCString( + cf_string, + buf.as_mut_ptr(), + buf.len() as _, + kCFStringEncodingUTF8, + ); + if success != 0 { + let name: &CStr = CStr::from_ptr(buf.as_ptr()); + if let Ok(name) = name.to_str() { + return Some(name.to_owned()); + } + } + } + } + + None + } + + fn create_string_for_key(&self, keycode: u16, modifier: u32) -> CFStringRef { + let current_keyboard = unsafe { TISCopyCurrentKeyboardInputSource() }; + let layout_data = unsafe { + TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData) + }; + let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) }; + + let mut keys_down: UInt32 = 0; + // let mut chars: *mut c_void;//[UniChar; 4]; + let mut chars: u16 = 0; + let mut real_length: UniCharCount = 0; + unsafe { + UCKeyTranslate( + keyboard_layout, + keycode, + kUCKeyActionDisplay as u16, + modifier, + LMGetKbdType() as u32, + kUCKeyTranslateNoDeadKeysBit as u32, + &mut keys_down, + 8, // sizeof(chars) / sizeof(chars[0]), + &mut real_length, + &mut chars, + ); + } + + unsafe { CFStringCreateWithCharacters(kCFAllocatorDefault, &chars, 1) } + } +} + +unsafe impl Send for Enigo {} diff --git a/libs/enigo/src/macos/mod.rs b/libs/enigo/src/macos/mod.rs new file mode 100644 index 00000000000..286bd748350 --- /dev/null +++ b/libs/enigo/src/macos/mod.rs @@ -0,0 +1,4 @@ +mod macos_impl; + +pub mod keycodes; +pub use self::macos_impl::Enigo; diff --git a/libs/enigo/src/win/keycodes.rs b/libs/enigo/src/win/keycodes.rs new file mode 100644 index 00000000000..2dc99275aca --- /dev/null +++ b/libs/enigo/src/win/keycodes.rs @@ -0,0 +1,73 @@ +// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731 + +pub const EVK_RETURN: u16 = 0x0D; +pub const EVK_TAB: u16 = 0x09; +pub const EVK_SPACE: u16 = 0x20; +pub const EVK_BACK: u16 = 0x08; +pub const EVK_ESCAPE: u16 = 0x1b; +pub const EVK_LWIN: u16 = 0x5b; +pub const EVK_SHIFT: u16 = 0x10; +pub const EVK_CAPITAL: u16 = 0x14; +pub const EVK_MENU: u16 = 0x12; +pub const EVK_LCONTROL: u16 = 0xa2; +pub const EVK_HOME: u16 = 0x24; +pub const EVK_PRIOR: u16 = 0x21; +pub const EVK_NEXT: u16 = 0x22; +pub const EVK_END: u16 = 0x23; +pub const EVK_LEFT: u16 = 0x25; +pub const EVK_RIGHT: u16 = 0x27; +pub const EVK_UP: u16 = 0x26; +pub const EVK_DOWN: u16 = 0x28; +pub const EVK_DELETE: u16 = 0x2E; +pub const EVK_F1: u16 = 0x70; +pub const EVK_F2: u16 = 0x71; +pub const EVK_F3: u16 = 0x72; +pub const EVK_F4: u16 = 0x73; +pub const EVK_F5: u16 = 0x74; +pub const EVK_F6: u16 = 0x75; +pub const EVK_F7: u16 = 0x76; +pub const EVK_F8: u16 = 0x77; +pub const EVK_F9: u16 = 0x78; +pub const EVK_F10: u16 = 0x79; +pub const EVK_F11: u16 = 0x7a; +pub const EVK_F12: u16 = 0x7b; +pub const EVK_NUMPAD0: u16 = 0x60; +pub const EVK_NUMPAD1: u16 = 0x61; +pub const EVK_NUMPAD2: u16 = 0x62; +pub const EVK_NUMPAD3: u16 = 0x63; +pub const EVK_NUMPAD4: u16 = 0x64; +pub const EVK_NUMPAD5: u16 = 0x65; +pub const EVK_NUMPAD6: u16 = 0x66; +pub const EVK_NUMPAD7: u16 = 0x67; +pub const EVK_NUMPAD8: u16 = 0x68; +pub const EVK_NUMPAD9: u16 = 0x69; +pub const EVK_CANCEL: u16 = 0x03; +pub const EVK_CLEAR: u16 = 0x0C; +pub const EVK_PAUSE: u16 = 0x13; +pub const EVK_KANA: u16 = 0x15; +pub const EVK_HANGUL: u16 = 0x15; +pub const EVK_JUNJA: u16 = 0x17; +pub const EVK_FINAL: u16 = 0x18; +pub const EVK_HANJA: u16 = 0x19; +pub const EVK_KANJI: u16 = 0x19; +pub const EVK_CONVERT: u16 = 0x1C; +pub const EVK_SELECT: u16 = 0x29; +pub const EVK_PRINT: u16 = 0x2A; +pub const EVK_EXECUTE: u16 = 0x2B; +pub const EVK_SNAPSHOT: u16 = 0x2C; +pub const EVK_INSERT: u16 = 0x2D; +pub const EVK_HELP: u16 = 0x2F; +pub const EVK_SLEEP: u16 = 0x5F; +pub const EVK_SEPARATOR: u16 = 0x6C; +pub const EVK_VOLUME_MUTE: u16 = 0xAD; +pub const EVK_VOLUME_DOWN: u16 = 0xAE; +pub const EVK_VOLUME_UP: u16 = 0xAF; +pub const EVK_NUMLOCK: u16 = 0x90; +pub const EVK_SCROLL: u16 = 0x91; +pub const EVK_RWIN: u16 = 0x5C; +pub const EVK_APPS: u16 = 0x5D; +pub const EVK_ADD: u16 = 0x6B; +pub const EVK_MULTIPLY: u16 = 0x6A; +pub const EVK_SUBTRACT: u16 = 0x6D; +pub const EVK_DECIMAL: u16 = 0x6E; +pub const EVK_DIVIDE: u16 = 0x6F; diff --git a/libs/enigo/src/win/mod.rs b/libs/enigo/src/win/mod.rs new file mode 100644 index 00000000000..024d7a3fd44 --- /dev/null +++ b/libs/enigo/src/win/mod.rs @@ -0,0 +1,4 @@ +mod win_impl; + +pub mod keycodes; +pub use self::win_impl::Enigo; diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs new file mode 100644 index 00000000000..df636e97803 --- /dev/null +++ b/libs/enigo/src/win/win_impl.rs @@ -0,0 +1,366 @@ +use winapi; + +use self::winapi::ctypes::c_int; +use self::winapi::shared::{minwindef::*, windef::*}; +use self::winapi::um::winbase::*; +use self::winapi::um::winuser::*; + +use crate::win::keycodes::*; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use std::mem::*; + +extern "system" { + pub fn GetLastError() -> DWORD; +} + +/// The main struct for handling the event emitting +#[derive(Default)] +pub struct Enigo; + +fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { + let mut input = INPUT { + type_: INPUT_MOUSE, + u: unsafe { + transmute(MOUSEINPUT { + dx, + dy, + mouseData: data, + dwFlags: flags, + time: 0, + dwExtraInfo: 0, + }) + }, + }; + unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } +} + +fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { + let mut input = INPUT { + type_: INPUT_KEYBOARD, + u: unsafe { + transmute_copy(&KEYBDINPUT { + wVk: vk, + wScan: scan, + dwFlags: flags, + time: 0, + dwExtraInfo: 0, + }) + }, + }; + unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } +} + +fn get_error() -> String { + unsafe { + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let errno = GetLastError(); + let chars_copied = FormatMessageW( + FORMAT_MESSAGE_IGNORE_INSERTS + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + std::ptr::null(), + errno, + 0, + buff.as_mut_ptr(), + (buff_size + 1) as u32, + std::ptr::null_mut(), + ); + if chars_copied == 0 { + return "".to_owned(); + } + let mut curr_char: usize = chars_copied as usize; + while curr_char > 0 { + let ch = buff[curr_char]; + + if ch >= ' ' as u16 { + break; + } + curr_char -= 1; + } + let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char); + let err_msg = String::from_utf16(sl); + return err_msg.unwrap_or("".to_owned()); + } +} + +impl MouseControllable for Enigo { + fn mouse_move_to(&mut self, x: i32, y: i32) { + mouse_event( + MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, + 0, + (x - unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) }) * 65535 + / unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) }, + (y - unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) }) * 65535 + / unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) }, + ); + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + mouse_event(MOUSEEVENTF_MOVE, 0, x, y); + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + let res = mouse_event( + match button { + MouseButton::Left => MOUSEEVENTF_LEFTDOWN, + MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, + MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, + _ => unimplemented!(), + }, + 0, + 0, + 0, + ); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + mouse_event( + match button { + MouseButton::Left => MOUSEEVENTF_LEFTUP, + MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, + MouseButton::Right => MOUSEEVENTF_RIGHTUP, + _ => unimplemented!(), + }, + 0, + 0, + 0, + ); + } + + fn mouse_click(&mut self, button: MouseButton) { + self.mouse_down(button).ok(); + self.mouse_up(button); + } + + fn mouse_scroll_x(&mut self, length: i32) { + mouse_event(MOUSEEVENTF_HWHEEL, unsafe { transmute(length * 120) }, 0, 0); + } + + fn mouse_scroll_y(&mut self, length: i32) { + mouse_event(MOUSEEVENTF_WHEEL, unsafe { transmute(length * 120) }, 0, 0); + } +} + +impl KeyboardControllable for Enigo { + fn key_sequence(&mut self, sequence: &str) { + let mut buffer = [0; 2]; + + for c in sequence.chars() { + // Windows uses uft-16 encoding. We need to check + // for variable length characters. As such some + // characters can be 32 bit long and those are + // encoded in such called hight and low surrogates + // each 16 bit wide that needs to be send after + // another to the SendInput function without + // being interrupted by "keyup" + let result = c.encode_utf16(&mut buffer); + if result.len() == 1 { + self.unicode_key_click(result[0]); + } else { + for utf16_surrogate in result { + self.unicode_key_down(utf16_surrogate.clone()); + } + // do i need to produce a keyup? + // self.unicode_key_up(0); + } + } + } + + fn key_click(&mut self, key: Key) { + let scancode = self.key_to_scancode(key); + keybd_event(KEYEVENTF_SCANCODE, 0, scancode); + keybd_event(KEYEVENTF_KEYUP | KEYEVENTF_SCANCODE, 0, scancode); + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + let res = keybd_event(KEYEVENTF_SCANCODE, 0, self.key_to_scancode(key)); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + keybd_event( + KEYEVENTF_KEYUP | KEYEVENTF_SCANCODE, + 0, + self.key_to_scancode(key), + ); + } + + fn get_key_state(&mut self, key: Key) -> bool { + let keycode = self.key_to_keycode(key); + let x = unsafe { GetKeyState(keycode as _) }; + if key == Key::CapsLock || key == Key::NumLock || key == Key::Scroll { + return (x & 0x1) == 0x1; + } + return (x as u16 & 0x8000) == 0x8000; + } +} + +impl Enigo { + /// Gets the (width, height) of the main display in screen coordinates (pixels). + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut size = Enigo::main_display_size(); + /// ``` + pub fn main_display_size() -> (usize, usize) { + let w = unsafe { GetSystemMetrics(SM_CXSCREEN) as usize }; + let h = unsafe { GetSystemMetrics(SM_CYSCREEN) as usize }; + (w, h) + } + + /// Gets the location of mouse in screen coordinates (pixels). + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut location = Enigo::mouse_location(); + /// ``` + pub fn mouse_location() -> (i32, i32) { + let mut point = POINT { x: 0, y: 0 }; + let result = unsafe { GetCursorPos(&mut point) }; + if result != 0 { + (point.x, point.y) + } else { + (0, 0) + } + } + + fn unicode_key_click(&self, unicode_char: u16) { + self.unicode_key_down(unicode_char); + self.unicode_key_up(unicode_char); + } + + fn unicode_key_down(&self, unicode_char: u16) { + keybd_event(KEYEVENTF_UNICODE, 0, unicode_char); + } + + fn unicode_key_up(&self, unicode_char: u16) { + keybd_event(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, unicode_char); + } + + fn key_to_keycode(&self, key: Key) -> u16 { + // do not use the codes from crate winapi they're + // wrongly typed with i32 instead of i16 use the + // ones provided by win/keycodes.rs that are prefixed + // with an 'E' infront of the original name + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + match key { + Key::Alt => EVK_MENU, + Key::Backspace => EVK_BACK, + Key::CapsLock => EVK_CAPITAL, + Key::Control => EVK_LCONTROL, + Key::Delete => EVK_DELETE, + Key::DownArrow => EVK_DOWN, + Key::End => EVK_END, + Key::Escape => EVK_ESCAPE, + Key::F1 => EVK_F1, + Key::F10 => EVK_F10, + Key::F11 => EVK_F11, + Key::F12 => EVK_F12, + Key::F2 => EVK_F2, + Key::F3 => EVK_F3, + Key::F4 => EVK_F4, + Key::F5 => EVK_F5, + Key::F6 => EVK_F6, + Key::F7 => EVK_F7, + Key::F8 => EVK_F8, + Key::F9 => EVK_F9, + Key::Home => EVK_HOME, + Key::LeftArrow => EVK_LEFT, + Key::Option => EVK_MENU, + Key::PageDown => EVK_NEXT, + Key::PageUp => EVK_PRIOR, + Key::Return => EVK_RETURN, + Key::RightArrow => EVK_RIGHT, + Key::Shift => EVK_SHIFT, + Key::Space => EVK_SPACE, + Key::Tab => EVK_TAB, + Key::UpArrow => EVK_UP, + Key::Numpad0 => EVK_NUMPAD0, + Key::Numpad1 => EVK_NUMPAD1, + Key::Numpad2 => EVK_NUMPAD2, + Key::Numpad3 => EVK_NUMPAD3, + Key::Numpad4 => EVK_NUMPAD4, + Key::Numpad5 => EVK_NUMPAD5, + Key::Numpad6 => EVK_NUMPAD6, + Key::Numpad7 => EVK_NUMPAD7, + Key::Numpad8 => EVK_NUMPAD8, + Key::Numpad9 => EVK_NUMPAD9, + Key::Cancel => EVK_CANCEL, + Key::Clear => EVK_CLEAR, + Key::Menu => EVK_MENU, + Key::Pause => EVK_PAUSE, + Key::Kana => EVK_KANA, + Key::Hangul => EVK_HANGUL, + Key::Junja => EVK_JUNJA, + Key::Final => EVK_FINAL, + Key::Hanja => EVK_HANJA, + Key::Kanji => EVK_KANJI, + Key::Convert => EVK_CONVERT, + Key::Select => EVK_SELECT, + Key::Print => EVK_PRINT, + Key::Execute => EVK_EXECUTE, + Key::Snapshot => EVK_SNAPSHOT, + Key::Insert => EVK_INSERT, + Key::Help => EVK_HELP, + Key::Sleep => EVK_SLEEP, + Key::Separator => EVK_SEPARATOR, + Key::Mute => EVK_VOLUME_MUTE, + Key::VolumeDown => EVK_VOLUME_DOWN, + Key::VolumeUp => EVK_VOLUME_UP, + Key::Scroll => EVK_SCROLL, + Key::NumLock => EVK_NUMLOCK, + Key::RWin => EVK_RWIN, + Key::Apps => EVK_APPS, + Key::Add => EVK_ADD, + Key::Multiply => EVK_MULTIPLY, + Key::Decimal => EVK_DECIMAL, + Key::Subtract => EVK_SUBTRACT, + Key::Divide => EVK_DIVIDE, + Key::NumpadEnter => EVK_RETURN, + Key::Equals => '=' as _, + + Key::Raw(raw_keycode) => raw_keycode, + Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()), + Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN, + } + } + + fn key_to_scancode(&self, key: Key) -> u16 { + let keycode = self.key_to_keycode(key); + unsafe { MapVirtualKeyW(keycode as u32, 0) as u16 } + } + + fn get_layoutdependent_keycode(&self, string: String) -> u16 { + // get the first char from the string ignore the rest + // ensure its not a multybyte char + if let Some(chr) = string.chars().nth(0) { + // NOTE VkKeyScanW uses the current keyboard layout + // to specify a layout use VkKeyScanExW and GetKeyboardLayout + // or load one with LoadKeyboardLayoutW + let keycode_and_shiftstate = unsafe { VkKeyScanW(chr as _) }; + keycode_and_shiftstate as _ + } else { + 0 + } + } +} diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore new file mode 100644 index 00000000000..b1cf151e391 --- /dev/null +++ b/libs/hbb_common/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +src/protos/ diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml new file mode 100644 index 00000000000..6dc5319c824 --- /dev/null +++ b/libs/hbb_common/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "hbb_common" +version = "0.1.0" +authors = ["rustdesk"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +protobuf = { version = "3.0.0-pre", git = "https://github.com/stepancheg/rust-protobuf" } +tokio = { version = "0.2", features = ["full"] } +tokio-util = { version = "0.3", features = ["full"] } +futures = "0.3" +bytes = "0.5" +log = "0.4" +env_logger = "0.8" +socket2 = { version = "0.3", features = ["reuseport"] } +zstd = "0.5" +quinn = {version = "0.6", optional = true } +anyhow = "1.0" +futures-util = "0.3" +directories-next = "2.0" +rand = "0.7" +serde_derive = "1.0" +serde = "1.0" +lazy_static = "1.4" +confy = { git = "https://github.com/open-trade/confy" } +dirs-next = "2.0" +filetime = "0.2" +sodiumoxide = "0.2" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +mac_address = "1.1" + +[features] +quic = ["quinn"] + +[build-dependencies] +protobuf-codegen-pure = { version = "3.0.0-pre", git = "https://github.com/stepancheg/rust-protobuf" } + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser"] } + +[dev-dependencies] +toml = "0.5" +serde_json = "1.0" diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs new file mode 100644 index 00000000000..99dacb7ec09 --- /dev/null +++ b/libs/hbb_common/build.rs @@ -0,0 +1,9 @@ +fn main() { + std::fs::create_dir_all("src/protos").unwrap(); + protobuf_codegen_pure::Codegen::new() + .out_dir("src/protos") + .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .include("protos") + .run() + .expect("Codegen failed."); +} diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto new file mode 100644 index 00000000000..6a2a0329966 --- /dev/null +++ b/libs/hbb_common/protos/message.proto @@ -0,0 +1,401 @@ +syntax = "proto3"; +package hbb; + +message VP9 { + bytes data = 1; + bool key = 2; + int64 pts = 3; +} + +message VP9s { repeated VP9 frames = 1; } + +message RGB { bool compress = 1; } + +// planes data send directly in binary for better use arraybuffer on web +message YUV { + bool compress = 1; + int32 stride = 2; +} + +message VideoFrame { + oneof union { + VP9s vp9s = 6; + RGB rgb = 7; + YUV yuv = 8; + } +} + +message DisplayInfo { + sint32 x = 1; + sint32 y = 2; + int32 width = 3; + int32 height = 4; + string name = 5; + bool online = 6; +} + +message PortForward { + string host = 1; + int32 port = 2; +} + +message FileTransfer { + string dir = 1; + bool show_hidden = 2; +} + +message LoginRequest { + string username = 1; + bytes password = 2; + string my_id = 4; + string my_name = 5; + OptionMessage option = 6; + oneof union { + FileTransfer file_transfer = 7; + PortForward port_forward = 8; + } +} + +message ChatMessage { string text = 1; } + +message PeerInfo { + string username = 1; + string hostname = 2; + string platform = 3; + repeated DisplayInfo displays = 4; + int32 current_display = 5; + bool sas_enabled = 6; + string version = 7; +} + +message LoginResponse { + oneof union { + string error = 1; + PeerInfo peer_info = 2; + } +} + +message MouseEvent { + int32 mask = 1; + sint32 x = 2; + sint32 y = 3; + repeated ControlKey modifiers = 4; +} + +enum ControlKey { + Alt = 1; + Backspace = 2; + CapsLock = 3; + Control = 4; + Delete = 5; + DownArrow = 6; + End = 7; + Escape = 8; + F1 = 9; + F10 = 10; + F11 = 11; + F12 = 12; + F2 = 13; + F3 = 14; + F4 = 15; + F5 = 16; + F6 = 17; + F7 = 18; + F8 = 19; + F9 = 20; + Home = 21; + LeftArrow = 22; + /// meta key (also known as "windows"; "super"; and "command") + Meta = 23; + /// option key on macOS (alt key on Linux and Windows) + Option = 24; + PageDown = 25; + PageUp = 26; + Return = 27; + RightArrow = 28; + Shift = 29; + Space = 30; + Tab = 31; + UpArrow = 32; + Numpad0 = 33; + Numpad1 = 34; + Numpad2 = 35; + Numpad3 = 36; + Numpad4 = 37; + Numpad5 = 38; + Numpad6 = 39; + Numpad7 = 40; + Numpad8 = 41; + Numpad9 = 42; + Cancel = 43; + Clear = 44; + Menu = 45; + Pause = 46; + Kana = 47; + Hangul = 48; + Junja = 49; + Final = 50; + Hanja = 51; + Kanji = 52; + Convert = 53; + Select = 54; + Print = 55; + Execute = 56; + Snapshot = 57; + Insert = 58; + Help = 59; + Sleep = 60; + Separator = 61; + Scroll = 62; + NumLock = 63; + RWin = 64; + Apps = 65; + Multiply = 66; + Add = 67; + Subtract = 68; + Decimal = 69; + Divide = 70; + Equals = 71; + NumpadEnter = 72; + CtrlAltDel = 100; + LockScreen = 101; +} + +message KeyEvent { + bool down = 1; + bool press = 2; + oneof union { + ControlKey control_key = 3; + uint32 chr = 4; + uint32 unicode = 5; + string seq = 6; + } + repeated ControlKey modifiers = 8; +} + +message CursorData { + uint64 id = 1; + sint32 hotx = 2; + sint32 hoty = 3; + int32 width = 4; + int32 height = 5; + bytes colors = 6; +} + +message CursorPosition { + sint32 x = 1; + sint32 y = 2; +} + +message Hash { + string salt = 1; + string challenge = 2; +}; + +message Clipboard { + bool compress = 1; + bytes content = 2; +}; + +enum FileType { + Dir = 1; + DirLink = 2; + DirDrive = 3; + File = 4; + FileLink = 5; +} + +message FileEntry { + FileType entry_type = 1; + string name = 2; + bool is_hidden = 3; + uint64 size = 4; + uint64 modified_time = 5; +} + +message FileDirectory { + int32 id = 1; + string path = 2; + repeated FileEntry entries = 3; +} + +message ReadDir { + string path = 1; + bool include_hidden = 2; +} + +message ReadAllFiles { + int32 id = 1; + string path = 2; + bool include_hidden = 3; +} + +message FileAction { + oneof union { + ReadDir read_dir = 1; + FileTransferSendRequest send = 2; + FileTransferReceiveRequest receive = 3; + FileDirCreate create = 4; + FileRemoveDir remove_dir = 5; + FileRemoveFile remove_file = 6; + ReadAllFiles all_files = 7; + FileTransferCancel cancel = 8; + } +} + +message FileTransferCancel { int32 id = 1; } + +message FileResponse { + oneof union { + FileDirectory dir = 1; + FileTransferBlock block = 2; + FileTransferError error = 3; + FileTransferDone done = 4; + } +} + +message FileTransferBlock { + int32 id = 1; + sint32 file_num = 2; + bytes data = 3; + bool compressed = 4; +} + +message FileTransferError { + int32 id = 1; + string error = 2; + sint32 file_num = 3; +} + +message FileTransferSendRequest { + int32 id = 1; + string path = 2; + bool include_hidden = 3; +} + +message FileTransferDone { + int32 id = 1; + sint32 file_num = 2; +} + +message FileTransferReceiveRequest { + int32 id = 1; + string path = 2; // path written to + repeated FileEntry files = 3; +} + +message FileRemoveDir { + int32 id = 1; + string path = 2; + bool recursive = 3; +} + +message FileRemoveFile { + int32 id = 1; + string path = 2; + sint32 file_num = 3; +} + +message FileDirCreate { + int32 id = 1; + string path = 2; +} + +message SwitchDisplay { + int32 display = 1; + sint32 x = 2; + sint32 y = 3; + int32 width = 4; + int32 height = 5; +} + +enum Permission { + Keyboard = 1; + Clipboard = 2; + Audio = 3; +} + +message PermissionInfo { + Permission permission = 1; + bool enabled = 2; +} + +enum ImageQuality { + NotSet = 0; + Low = 2; + Balanced = 3; + Best = 4; +} + +enum BoolOption { + NotSet = 0; + No = 1; + Yes = 2; +} + +message OptionMessage { + ImageQuality image_quality = 1; + BoolOption lock_after_session_end = 2; + BoolOption show_remote_cursor = 3; + BoolOption privacy_mode = 4; + BoolOption block_input = 5; + int32 custom_image_quality = 6; + BoolOption disable_audio = 7; + BoolOption disable_clipboard = 8; +} + +message TestDelay { + int64 time = 1; + bool from_client = 2; +} + +message PublicKey { + bytes asymmetric_value = 1; + bytes symmetric_value = 2; +} + +message SignedId { + bytes id = 1; + bytes pk = 2; +} + +message AudioFormat { + uint32 sample_rate = 1; + uint32 channels = 2; +} + +message AudioFrame { bytes data = 1; } + +message Misc { + oneof union { + ChatMessage chat_message = 4; + SwitchDisplay switch_display = 5; + PermissionInfo permission_info = 6; + OptionMessage option = 7; + AudioFormat audio_format = 8; + string close_reason = 9; + bool refresh_video = 10; + } +} + +message Message { + oneof union { + SignedId signed_id = 3; + PublicKey public_key = 4; + TestDelay test_delay = 5; + VideoFrame video_frame = 6; + LoginRequest login_request = 7; + LoginResponse login_response = 8; + Hash hash = 9; + MouseEvent mouse_event = 10; + AudioFrame audio_frame = 11; + CursorData cursor_data = 12; + CursorPosition cursor_position = 13; + uint64 cursor_id = 14; + KeyEvent key_event = 15; + Clipboard clipboard = 16; + FileAction file_action = 17; + FileResponse file_response = 18; + Misc misc = 19; + } +} diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto new file mode 100644 index 00000000000..cd920bb3ad9 --- /dev/null +++ b/libs/hbb_common/protos/rendezvous.proto @@ -0,0 +1,133 @@ +syntax = "proto3"; +package hbb; + +message RegisterPeer { + string id = 1; + int32 serial = 2; +} + +message RegisterPeerResponse { bool request_pk = 2; } + +message PunchHoleRequest { + string id = 1; + NatType nat_type = 2; +} + +message PunchHole { + bytes socket_addr = 1; + string relay_server = 2; + NatType nat_type = 3; +} + +message TestNatRequest { + int32 serial = 1; +} + +// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative +message TestNatResponse { + int32 port = 1; + ConfigUpdate cu = 2; // for mobile +} + +enum NatType { + UNKNOWN_NAT = 0; + ASYMMETRIC = 1; + SYMMETRIC = 2; +} + +message PunchHoleSent { + bytes socket_addr = 1; + string id = 2; + string relay_server = 3; + NatType nat_type = 4; +} + +message RegisterPk { + string id = 1; + bytes uuid = 2; + bytes pk = 3; +} + +message RegisterPkResponse { + enum Result { + OK = 1; + UUID_MISMATCH = 2; + } + Result result = 1; +} + +message PunchHoleResponse { + bytes socket_addr = 1; + bytes pk = 2; + enum Failure { + ID_NOT_EXIST = 1; + OFFLINE = 2; + } + Failure failure = 3; + string relay_server = 4; + oneof union { + NatType nat_type = 5; + bool is_local = 6; + } +} + +message ConfigUpdate { + int32 serial = 1; + repeated string rendezvous_servers = 2; +} + +message RequestRelay { + string id = 1; + string uuid = 2; + bytes socket_addr = 3; + string relay_server = 4; + bool secure = 5; +} + +message RelayResponse { + bytes socket_addr = 1; + string uuid = 2; + string relay_server = 3; + oneof union { + string id = 4; + bytes pk = 5; + } +} + +message SoftwareUpdate { string url = 1; } + +// if in same intranet, punch hole won't work both for udp and tcp, +// even some router has below connection error if we connect itself, +// { kind: Other, error: "could not resolve to any address" }, +// so we request local address to connect. +message FetchLocalAddr { + bytes socket_addr = 1; + string relay_server = 2; +} + +message LocalAddr { + bytes socket_addr = 1; + bytes local_addr = 2; + string relay_server = 3; +} + +message RendezvousMessage { + oneof union { + RegisterPeer register_peer = 6; + RegisterPeerResponse register_peer_response = 7; + PunchHoleRequest punch_hole_request = 8; + PunchHole punch_hole = 9; + PunchHoleSent punch_hole_sent = 10; + PunchHoleResponse punch_hole_response = 11; + FetchLocalAddr fetch_local_addr = 12; + LocalAddr local_addr = 13; + ConfigUpdate configure_update = 14; + RegisterPk register_pk = 15; + RegisterPkResponse register_pk_response = 16; + SoftwareUpdate software_update = 17; + RequestRelay request_relay = 18; + RelayResponse relay_response = 19; + TestNatRequest test_nat_request = 20; + TestNatResponse test_nat_response = 21; + } +} diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs new file mode 100644 index 00000000000..e029f1cc02b --- /dev/null +++ b/libs/hbb_common/src/bytes_codec.rs @@ -0,0 +1,274 @@ +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use std::io; +use tokio_util::codec::{Decoder, Encoder}; + +#[derive(Debug, Clone, Copy)] +pub struct BytesCodec { + state: DecodeState, + raw: bool, + max_packet_length: usize, +} + +#[derive(Debug, Clone, Copy)] +enum DecodeState { + Head, + Data(usize), +} + +impl BytesCodec { + pub fn new() -> Self { + Self { + state: DecodeState::Head, + raw: false, + max_packet_length: usize::MAX, + } + } + + pub fn set_raw(&mut self) { + self.raw = true; + } + + pub fn set_max_packet_length(&mut self, n: usize) { + self.max_packet_length = n; + } + + fn decode_head(&mut self, src: &mut BytesMut) -> io::Result> { + if src.is_empty() { + return Ok(None); + } + let head_len = ((src[0] & 0x3) + 1) as usize; + if src.len() < head_len { + return Ok(None); + } + let mut n = src[0] as usize; + if head_len > 1 { + n |= (src[1] as usize) << 8; + } + if head_len > 2 { + n |= (src[2] as usize) << 16; + } + if head_len > 3 { + n |= (src[3] as usize) << 24; + } + n >>= 2; + if n > self.max_packet_length { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet")); + } + src.advance(head_len); + src.reserve(n); + return Ok(Some(n)); + } + + fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { + if src.len() < n { + return Ok(None); + } + Ok(Some(src.split_to(n))) + } +} + +impl Decoder for BytesCodec { + type Item = BytesMut; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, io::Error> { + if self.raw { + if !src.is_empty() { + let len = src.len(); + return Ok(Some(src.split_to(len))); + } else { + return Ok(None); + } + } + let n = match self.state { + DecodeState::Head => match self.decode_head(src)? { + Some(n) => { + self.state = DecodeState::Data(n); + n + } + None => return Ok(None), + }, + DecodeState::Data(n) => n, + }; + + match self.decode_data(n, src)? { + Some(data) => { + self.state = DecodeState::Head; + Ok(Some(data)) + } + None => Ok(None), + } + } +} + +impl Encoder for BytesCodec { + type Error = io::Error; + + fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> { + if self.raw { + buf.reserve(data.len()); + buf.put(data); + return Ok(()); + } + if data.len() <= 0x3F { + buf.put_u8((data.len() << 2) as u8); + } else if data.len() <= 0x3FFF { + buf.put_u16_le((data.len() << 2) as u16 | 0x1); + } else if data.len() <= 0x3FFFFF { + let h = (data.len() << 2) as u32 | 0x2; + buf.put_u16_le((h & 0xFFFF) as u16); + buf.put_u8((h >> 16) as u8); + } else if data.len() <= 0x3FFFFFFF { + buf.put_u32_le((data.len() << 2) as u32 | 0x3); + } else { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow")); + } + buf.extend(data); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_codec1() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3F, 1); + assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + let buf_saved = buf.clone(); + assert_eq!(buf.len(), 0x3F + 1); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3F); + assert_eq!(res[0], 1); + } else { + assert!(false); + } + let mut codec2 = BytesCodec::new(); + let mut buf2 = BytesMut::new(); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + assert!(false); + } + buf2.extend(&buf_saved[0..1]); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + assert!(false); + } + buf2.extend(&buf_saved[1..]); + if let Ok(Some(res)) = codec2.decode(&mut buf2) { + assert_eq!(res.len(), 0x3F); + assert_eq!(res[0], 1); + } else { + assert!(false); + } + } + + #[test] + fn test_codec2() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + assert!(!codec.encode("".into(), &mut buf).is_err()); + assert_eq!(buf.len(), 1); + bytes.resize(0x3F + 1, 2); + assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert_eq!(buf.len(), 0x3F + 2 + 2); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0); + } else { + assert!(false); + } + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3F + 1); + assert_eq!(res[0], 2); + } else { + assert!(false); + } + } + + #[test] + fn test_codec3() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3F - 1, 3); + assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert_eq!(buf.len(), 0x3F + 1 - 1); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3F - 1); + assert_eq!(res[0], 3); + } else { + assert!(false); + } + } + #[test] + fn test_codec4() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3FFF, 4); + assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert_eq!(buf.len(), 0x3FFF + 2); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3FFF); + assert_eq!(res[0], 4); + } else { + assert!(false); + } + } + + #[test] + fn test_codec5() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3FFFFF, 5); + assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert_eq!(buf.len(), 0x3FFFFF + 3); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3FFFFF); + assert_eq!(res[0], 5); + } else { + assert!(false); + } + } + + #[test] + fn test_codec6() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3FFFFF + 1, 6); + assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + let buf_saved = buf.clone(); + assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3FFFFF + 1); + assert_eq!(res[0], 6); + } else { + assert!(false); + } + let mut codec2 = BytesCodec::new(); + let mut buf2 = BytesMut::new(); + buf2.extend(&buf_saved[0..1]); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + assert!(false); + } + buf2.extend(&buf_saved[1..6]); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + assert!(false); + } + buf2.extend(&buf_saved[6..]); + if let Ok(Some(res)) = codec2.decode(&mut buf2) { + assert_eq!(res.len(), 0x3FFFFF + 1); + assert_eq!(res[0], 6); + } else { + assert!(false); + } + } +} diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs new file mode 100644 index 00000000000..a969ccf8634 --- /dev/null +++ b/libs/hbb_common/src/compress.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use zstd::block::{Compressor, Decompressor}; + +thread_local! { + static COMPRESSOR: RefCell = RefCell::new(Compressor::new()); + static DECOMPRESSOR: RefCell = RefCell::new(Decompressor::new()); +} + +/// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), +/// which is currently 22. Levels >= 20 +/// Default level is ZSTD_CLEVEL_DEFAULT==3. +/// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT +pub fn compress(data: &[u8], level: i32) -> Vec { + let mut out = Vec::new(); + COMPRESSOR.with(|c| { + if let Ok(mut c) = c.try_borrow_mut() { + match c.compress(data, level) { + Ok(res) => out = res, + Err(err) => { + crate::log::debug!("Failed to compress: {}", err); + } + } + } + }); + out +} + +pub fn decompress(data: &[u8]) -> Vec { + let mut out = Vec::new(); + DECOMPRESSOR.with(|d| { + if let Ok(mut d) = d.try_borrow_mut() { + const MAX: usize = 1024 * 1024 * 64; + const MIN: usize = 1024 * 1024; + let mut n = 30 * data.len(); + if n > MAX { + n = MAX; + } + if n < MIN { + n = MIN; + } + match d.decompress(data, n) { + Ok(res) => out = res, + Err(err) => { + crate::log::debug!("Failed to decompress: {}", err); + } + } + } + }); + out +} diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs new file mode 100644 index 00000000000..f98be41621a --- /dev/null +++ b/libs/hbb_common/src/config.rs @@ -0,0 +1,688 @@ +use crate::log; +use directories_next::ProjectDirs; +use rand::Rng; +use serde_derive::{Deserialize, Serialize}; +use sodiumoxide::crypto::sign; +use std::{ + collections::HashMap, + fs, + net::SocketAddr, + path::{Path, PathBuf}, + sync::{Arc, Mutex, RwLock}, + time::SystemTime, +}; + +pub const APP_NAME: &str = "RustDesk"; +pub const BIND_INTERFACE: &str = "0.0.0.0"; +pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; +pub const CONNECT_TIMEOUT: u64 = 18_000; +pub const COMPRESS_LEVEL: i32 = 3; +const SERIAL: i32 = 0; +// 128x128 +#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding +pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII= +"; +#[cfg(windows)] // windows, 32x32, bigger very ugly after shrink +pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAolBMVEUAAAAAcf8Acf8Acf8AcP8Acf8Acf8Acf8AcP8Acf/////9/v/7/f+gyv9wr/8Ld/8GdP/A3P+nzv+Qwf9mqv8lhv8UfP+Kvv88k/8zjv/x+P/Y6f/U5//J4f/F3/+x1P+pz/+izP+Cuv9Zov9Qnv9FmP8gg//f7f+42P96tv9fpv8ui//1+f/q8//o8v/Q5f+ax/+Zxv+VxP+Fu/9rrf8rif+x1o3FAAAACXRSTlMAv/RPTPKHioRsIqhAAAABNklEQVQ4y4WT6XaCQAyFB7S2GUD2RaxSKGDV7sv7v1on4djEluL9Ncn5biaZk1FKzSwbRmVbM2V0DRNaGD9Maq6sacBS9jRwpUTw1Ww7SEOvkwQDaeVofQeJ1nrXjgD3pTaqCDCg/xs4OBrlwUGTHvxzoHAx69Y9+Hk5oGdAsCNXSoHfEN1JYEme4KcfvO9WAmtMBNx6jIaCgSPGLbAyvKRhAEcrAxAKTSZiACu+gNSbyWwZeKdQKj91yRXWIOWZTMgATun6EtjgczNQaKMEWD0+xJ6B4AnvECVqdKwYoJ50hIPyw25AANkzpsLB00cYOIUEoHU0uVaQVcMxBwlgVZJ3Orz+3ahH6gP2tBgfYzsZmdo1fGIzRx5Irn2WxKaHeJnKtb/4cS5/PbWACd0oo/n/39/4vwGFYSxtSYV4OAAAAABJRU5ErkJggg== +"; +#[cfg(target_os = "linux")] // 128x128 no padding +pub const ICON: &str = "data:image/png;base64,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 +"; +#[cfg(target_os = "macos")] +pub const ORG: &str = "com.carriez"; + +type Size = (i32, i32, i32, i32); + +lazy_static::lazy_static! { + static ref CONFIG: Arc> = Arc::new(RwLock::new(Config::load())); + static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); + pub static ref ONLINE: Arc>> = Default::default(); +} +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref APP_DIR: Arc> = Default::default(); +} +const CHARS: &'static [char] = &[ + '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', +]; + +pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ + "rs-sg.rustdesk.com", + "rs-cn.rustdesk.com", +]; +pub const RENDEZVOUS_PORT: i32 = 21116; +pub const RELAY_PORT: i32 = 21117; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Config { + #[serde(default)] + id: String, + #[serde(default)] + password: String, + #[serde(default)] + salt: String, + #[serde(default)] + key_pair: (Vec, Vec), // sk, pk + #[serde(default)] + key_confirmed: bool, + #[serde(default)] + keys_confirmed: HashMap, +} + +// more variable configs +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Config2 { + #[serde(default)] + remote_id: String, // latest used one + #[serde(default)] + size: Size, + #[serde(default)] + rendezvous_server: String, + #[serde(default)] + nat_type: i32, + #[serde(default)] + serial: i32, + + // the other scalar value must before this + #[serde(default)] + pub options: HashMap, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct PeerConfig { + #[serde(default)] + pub password: Vec, + #[serde(default)] + pub size: Size, + #[serde(default)] + pub size_ft: Size, + #[serde(default)] + pub size_pf: Size, + #[serde(default)] + pub view_style: String, // original (default), scale + #[serde(default)] + pub image_quality: String, + #[serde(default)] + pub custom_image_quality: Vec, + #[serde(default)] + pub show_remote_cursor: bool, + #[serde(default)] + pub lock_after_session_end: bool, + #[serde(default)] + pub privacy_mode: bool, + #[serde(default)] + pub port_forwards: Vec<(i32, String, i32)>, + #[serde(default)] + pub direct_failures: i32, + #[serde(default)] + pub disable_audio: bool, + #[serde(default)] + pub disable_clipboard: bool, + + // the other scalar value must before this + #[serde(default)] + pub options: HashMap, + #[serde(default)] + pub info: PeerInfoSerde, +} + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] +pub struct PeerInfoSerde { + #[serde(default)] + pub username: String, + #[serde(default)] + pub hostname: String, + #[serde(default)] + pub platform: String, +} + +fn patch(path: PathBuf) -> PathBuf { + if let Some(_tmp) = path.to_str() { + #[cfg(windows)] + return _tmp + .replace( + "system32\\config\\systemprofile", + "ServiceProfiles\\LocalService", + ) + .into(); + #[cfg(target_os = "macos")] + return _tmp.replace("Application Support", "Preferences").into(); + } + path +} + +impl Config2 { + fn load() -> Config2 { + Config::load_::("2") + } + + fn store(&self) { + Config::store_(self, "2"); + } +} + +impl Config { + fn load_( + suffix: &str, + ) -> T { + let file = Self::file_(suffix); + log::debug!("Configuration path: {}", file.display()); + let cfg = match confy::load_path(&file) { + Ok(config) => config, + Err(err) => { + log::error!("Failed to load config: {}", err); + T::default() + } + }; + if suffix.is_empty() { + log::debug!("{:?}", cfg); + } + cfg + } + + fn store_(config: &T, suffix: &str) { + let file = Self::file_(suffix); + if let Err(err) = confy::store_path(file, config) { + log::error!("Failed to store config: {}", err); + } + } + + fn load() -> Config { + Config::load_::("") + } + + fn store(&self) { + Config::store_(self, ""); + } + + pub fn file() -> PathBuf { + Self::file_("") + } + + pub fn import(from: &str) { + log::info!("import {}", from); + // load first to create path + Self::load(); + crate::allow_err!(std::fs::copy(from, Self::file())); + crate::allow_err!(std::fs::copy( + from.replace(".toml", "2.toml"), + Self::file_("2") + )); + } + + pub fn save_tmp() -> String { + let _lock = CONFIG.read().unwrap(); // do not use let _, which will be dropped immediately + let path = Self::file_("2").to_str().unwrap_or("").to_owned(); + let path2 = format!("{}_tmp", path); + crate::allow_err!(std::fs::copy(&path, &path2)); + let path = Self::file().to_str().unwrap_or("").to_owned(); + let path2 = format!("{}_tmp", path); + crate::allow_err!(std::fs::copy(&path, &path2)); + path2 + } + + fn file_(suffix: &str) -> PathBuf { + let name = format!("{}{}", APP_NAME, suffix); + Self::path(name).with_extension("toml") + } + + pub fn get_home() -> PathBuf { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Self::path(""); + if let Some(path) = dirs_next::home_dir() { + patch(path) + } else if let Ok(path) = std::env::current_dir() { + path + } else { + std::env::temp_dir() + } + } + + fn path>(p: P) -> PathBuf { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let mut path: PathBuf = APP_DIR.read().unwrap().clone().into(); + path.push(p); + return path; + } + #[cfg(not(target_os = "macos"))] + let org = ""; + #[cfg(target_os = "macos")] + let org = ORG; + // /var/root for root + if let Some(project) = ProjectDirs::from("", org, APP_NAME) { + let mut path = patch(project.config_dir().to_path_buf()); + path.push(p); + return path; + } + return "".into(); + } + + pub fn log_path() -> PathBuf { + #[cfg(target_os = "macos")] + { + if let Some(path) = dirs_next::home_dir().as_mut() { + path.push(format!("Library/Logs/{}", APP_NAME)); + return path.clone(); + } + } + #[cfg(target_os = "linux")] + { + if let Some(path) = dirs_next::home_dir().as_mut() { + path.push(format!(".local/share/logs/{}", APP_NAME)); + std::fs::create_dir_all(&path).ok(); + return path.clone(); + } + } + if let Some(path) = Self::path("").parent() { + let mut path: PathBuf = path.into(); + path.push("log"); + return path; + } + "".into() + } + + pub fn ipc_path(postfix: &str) -> String { + #[cfg(windows)] + { + // \\ServerName\pipe\PipeName + // where ServerName is either the name of a remote computer or a period, to specify the local computer. + // https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names + format!("\\\\.\\pipe\\{}\\query{}", APP_NAME, postfix) + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + let mut path: PathBuf = format!("/tmp/{}", APP_NAME).into(); + fs::create_dir(&path).ok(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); + path.push(format!("ipc{}", postfix)); + path.to_str().unwrap_or("").to_owned() + } + } + + pub fn icon_path() -> PathBuf { + let mut path = Self::path("icons"); + if fs::create_dir_all(&path).is_err() { + path = std::env::temp_dir(); + } + path + } + + #[inline] + pub fn get_any_listen_addr() -> SocketAddr { + format!("{}:0", BIND_INTERFACE).parse().unwrap() + } + + pub fn get_rendezvous_server() -> SocketAddr { + let mut rendezvous_server = Self::get_option("custom-rendezvous-server"); + if rendezvous_server.is_empty() { + rendezvous_server = CONFIG2.write().unwrap().rendezvous_server.clone(); + } + if rendezvous_server.is_empty() { + rendezvous_server = Self::get_rendezvous_servers() + .drain(..) + .next() + .unwrap_or("".to_owned()); + } + if !rendezvous_server.contains(":") { + rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); + } + if let Ok(addr) = crate::to_socket_addr(&rendezvous_server) { + addr + } else { + Self::get_any_listen_addr() + } + } + + pub fn get_rendezvous_servers() -> Vec { + let s = Self::get_option("custom-rendezvous-server"); + if !s.is_empty() { + return vec![s]; + } + let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; + if serial_obsolute { + let ss: Vec = Self::get_option("rendezvous-servers") + .split(",") + .filter(|x| x.contains(".")) + .map(|x| x.to_owned()) + .collect(); + if !ss.is_empty() { + return ss; + } + } + return RENDEZVOUS_SERVERS.iter().map(|x| x.to_string()).collect(); + } + + pub fn reset_online() { + *ONLINE.lock().unwrap() = Default::default(); + } + + pub fn update_latency(host: &str, latency: i64) { + ONLINE.lock().unwrap().insert(host.to_owned(), latency); + let mut host = "".to_owned(); + let mut delay = i64::MAX; + for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { + if tmp_delay > &0 && tmp_delay < &delay { + delay = tmp_delay.clone(); + host = tmp_host.to_string(); + } + } + if !host.is_empty() { + let mut config = CONFIG2.write().unwrap(); + if host != config.rendezvous_server { + log::debug!("Update rendezvous_server in config to {}", host); + log::debug!("{:?}", *ONLINE.lock().unwrap()); + config.rendezvous_server = host; + config.store(); + } + } + } + + pub fn set_id(id: &str) { + let mut config = CONFIG.write().unwrap(); + if id == config.id { + return; + } + config.id = id.into(); + config.store(); + } + + pub fn set_nat_type(nat_type: i32) { + let mut config = CONFIG2.write().unwrap(); + if nat_type == config.nat_type { + return; + } + config.nat_type = nat_type; + config.store(); + } + + pub fn get_nat_type() -> i32 { + CONFIG2.read().unwrap().nat_type + } + + pub fn set_serial(serial: i32) { + let mut config = CONFIG2.write().unwrap(); + if serial == config.serial { + return; + } + config.serial = serial; + config.store(); + } + + pub fn get_serial() -> i32 { + std::cmp::max(CONFIG2.read().unwrap().serial, SERIAL) + } + + fn get_auto_id() -> Option { + #[cfg(any(target_os = "android", target_os = "ios"))] + return None; + let mut id = 0u32; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(Some(ma)) = mac_address::get_mac_address() { + for x in &ma.bytes()[2..] { + id = (id << 8) | (*x as u32); + } + id = id & 0x1FFFFFFF; + Some(id.to_string()) + } else { + None + } + } + + pub fn get_auto_password() -> String { + let mut rng = rand::thread_rng(); + (0..6) + .map(|_| CHARS[rng.gen::() % CHARS.len()]) + .collect() + } + + pub fn get_key_confirmed() -> bool { + CONFIG.read().unwrap().key_confirmed + } + + pub fn set_key_confirmed(v: bool) { + let mut config = CONFIG.write().unwrap(); + if config.key_confirmed == v { + return; + } + config.key_confirmed = v; + if !v { + config.keys_confirmed = Default::default(); + } + config.store(); + } + + pub fn get_host_key_confirmed(host: &str) -> bool { + if let Some(true) = CONFIG.read().unwrap().keys_confirmed.get(host) { + true + } else { + false + } + } + + pub fn set_host_key_confirmed(host: &str, v: bool) { + if Self::get_host_key_confirmed(host) == v { + return; + } + let mut config = CONFIG.write().unwrap(); + config.keys_confirmed.insert(host.to_owned(), v); + config.store(); + } + + pub fn set_key_pair(pair: (Vec, Vec)) { + let mut config = CONFIG.write().unwrap(); + config.key_pair = pair; + config.store(); + } + + pub fn get_key_pair() -> (Vec, Vec) { + // lock here to make sure no gen_keypair more than once + let mut config = CONFIG.write().unwrap(); + if config.key_pair.0.is_empty() { + let (pk, sk) = sign::gen_keypair(); + config.key_pair = (sk.0.to_vec(), pk.0.into()); + config.store(); + } + config.key_pair.clone() + } + + pub fn get_id() -> String { + let mut id = CONFIG.read().unwrap().id.clone(); + if id.is_empty() { + if let Some(tmp) = Config::get_auto_id() { + id = tmp; + Config::set_id(&id); + } + } + id + } + + pub fn get_options() -> HashMap { + CONFIG2.read().unwrap().options.clone() + } + + pub fn set_options(v: HashMap) { + let mut config = CONFIG2.write().unwrap(); + config.options = v; + config.store(); + } + + pub fn get_option(k: &str) -> String { + if let Some(v) = CONFIG2.read().unwrap().options.get(k) { + v.clone() + } else { + "".to_owned() + } + } + + pub fn set_option(k: String, v: String) { + let mut config = CONFIG2.write().unwrap(); + if k == "custom-rendezvous-server" { + config.rendezvous_server = "".to_owned(); + } + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.options.get(&k) { + if v2.is_none() { + config.options.remove(&k); + } else { + config.options.insert(k, v); + } + config.store(); + } + } + + pub fn update_id() { + // to-do: how about if one ip register a lot of ids? + let id = Self::get_id(); + let mut rng = rand::thread_rng(); + let new_id = rng.gen_range(1_000_000_000, 2_000_000_000).to_string(); + Config::set_id(&new_id); + log::info!("id updated from {} to {}", id, new_id); + } + + pub fn set_password(password: &str) { + let mut config = CONFIG.write().unwrap(); + if password == config.password { + return; + } + config.password = password.into(); + config.store(); + } + + pub fn get_password() -> String { + let mut password = CONFIG.read().unwrap().password.clone(); + if password.is_empty() { + password = Config::get_auto_password(); + Config::set_password(&password); + } + password + } + + pub fn set_salt(salt: &str) { + let mut config = CONFIG.write().unwrap(); + if salt == config.salt { + return; + } + config.salt = salt.into(); + config.store(); + } + + pub fn get_salt() -> String { + let mut salt = CONFIG.read().unwrap().salt.clone(); + if salt.is_empty() { + salt = Config::get_auto_password(); + Config::set_salt(&salt); + } + salt + } + + pub fn get_size() -> Size { + CONFIG2.read().unwrap().size + } + + pub fn set_size(x: i32, y: i32, w: i32, h: i32) { + let mut config = CONFIG2.write().unwrap(); + let size = (x, y, w, h); + if size == config.size || size.2 < 300 || size.3 < 300 { + return; + } + config.size = size; + config.store(); + } + + pub fn set_remote_id(remote_id: &str) { + let mut config = CONFIG2.write().unwrap(); + if remote_id == config.remote_id { + return; + } + config.remote_id = remote_id.into(); + config.store(); + } + + pub fn get_remote_id() -> String { + CONFIG2.read().unwrap().remote_id.clone() + } +} + +const PEERS: &str = "peers"; + +impl PeerConfig { + pub fn load(id: &str) -> PeerConfig { + let _ = CONFIG.read().unwrap(); // for lock + match confy::load_path(&Self::path(id)) { + Ok(config) => config, + Err(err) => { + log::error!("Failed to load config: {}", err); + Default::default() + } + } + } + + pub fn store(&self, id: &str) { + let _ = CONFIG.read().unwrap(); // for lock + if let Err(err) = confy::store_path(Self::path(id), self) { + log::error!("Failed to store config: {}", err); + } + } + + pub fn remove(id: &str) { + fs::remove_file(&Self::path(id)).ok(); + } + + fn path(id: &str) -> PathBuf { + let path: PathBuf = [PEERS, id].iter().collect(); + Config::path(path).with_extension("toml") + } + + pub fn peers() -> Vec<(String, SystemTime, PeerInfoSerde)> { + if let Ok(peers) = Config::path(PEERS).read_dir() { + if let Ok(peers) = peers + .map(|res| res.map(|e| e.path())) + .collect::, _>>() + { + let mut peers: Vec<_> = peers + .iter() + .filter(|p| { + p.is_file() + && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") + }) + .map(|p| { + let t = fs::metadata(p) + .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH)) + .unwrap_or(SystemTime::UNIX_EPOCH); + let id = p + .file_stem() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + let info = PeerConfig::load(&id).info; + if info.platform.is_empty() { + fs::remove_file(&p).ok(); + } + (id, t, info) + }) + .filter(|p| !p.2.platform.is_empty()) + .collect(); + peers.sort_unstable_by(|a, b| b.1.cmp(&a.1)); + return peers; + } + } + Default::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_serialize() { + let cfg: Config = Default::default(); + let res = toml::to_string_pretty(&cfg); + assert!(res.is_ok()); + let cfg: PeerConfig = Default::default(); + let res = toml::to_string_pretty(&cfg); + assert!(res.is_ok()); + } +} diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs new file mode 100644 index 00000000000..1aad666ecb0 --- /dev/null +++ b/libs/hbb_common/src/fs.rs @@ -0,0 +1,554 @@ +use crate::{bail, message_proto::*, ResultType}; +use std::path::{Path, PathBuf}; +// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html +use crate::{ + compress::{compress, decompress}, + config::{Config, COMPRESS_LEVEL}, +}; +#[cfg(windows)] +use std::os::windows::prelude::*; +use tokio::{fs::File, prelude::*}; + +pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType { + let mut dir = FileDirectory { + path: get_string(&path), + ..Default::default() + }; + #[cfg(windows)] + if "/" == &get_string(&path) { + let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; + for i in 0..32 { + if drives & (1 << i) != 0 { + let name = format!( + "{}:", + std::char::from_u32('A' as u32 + i as u32).unwrap_or('A') + ); + dir.entries.push(FileEntry { + name, + entry_type: FileType::DirDrive.into(), + ..Default::default() + }); + } + } + return Ok(dir); + } + for entry in path.read_dir()? { + if let Ok(entry) = entry { + let p = entry.path(); + let name = p + .file_name() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + if name.is_empty() { + continue; + } + let mut is_hidden = false; + let meta; + if let Ok(tmp) = std::fs::symlink_metadata(&p) { + meta = tmp; + } else { + continue; + } + // docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + #[cfg(windows)] + if meta.file_attributes() & 0x2 != 0 { + is_hidden = true; + } + #[cfg(not(windows))] + if name.find('.').unwrap_or(usize::MAX) == 0 { + is_hidden = true; + } + if is_hidden && !include_hidden { + continue; + } + let (entry_type, size) = { + if p.is_dir() { + if meta.file_type().is_symlink() { + (FileType::DirLink.into(), 0) + } else { + (FileType::Dir.into(), 0) + } + } else { + if meta.file_type().is_symlink() { + (FileType::FileLink.into(), 0) + } else { + (FileType::File.into(), meta.len()) + } + } + }; + let modified_time = meta + .modified() + .map(|x| { + x.duration_since(std::time::SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0) + }) + .unwrap_or(0) as u64; + dir.entries.push(FileEntry { + name: get_file_name(&p), + entry_type, + is_hidden, + size, + modified_time, + ..Default::default() + }); + } + } + Ok(dir) +} + +#[inline] +pub fn get_file_name(p: &PathBuf) -> String { + p.file_name() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned() +} + +#[inline] +pub fn get_string(path: &PathBuf) -> String { + path.to_str().unwrap_or("").to_owned() +} + +#[inline] +pub fn get_path(path: &str) -> PathBuf { + Path::new(path).to_path_buf() +} + +#[inline] +pub fn get_home_as_string() -> String { + get_string(&Config::get_home()) +} + +fn read_dir_recursive( + path: &PathBuf, + prefix: &PathBuf, + include_hidden: bool, +) -> ResultType> { + let mut files = Vec::new(); + if path.is_dir() { + // to-do: symbol link handling, cp the link rather than the content + // to-do: file mode, for unix + let fd = read_dir(&path, include_hidden)?; + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::File) => { + let mut entry = entry.clone(); + entry.name = get_string(&prefix.join(entry.name)); + files.push(entry); + } + Ok(FileType::Dir) => { + if let Ok(mut tmp) = read_dir_recursive( + &path.join(&entry.name), + &prefix.join(&entry.name), + include_hidden, + ) { + for entry in tmp.drain(0..) { + files.push(entry); + } + } + } + _ => {} + } + } + Ok(files) + } else if path.is_file() { + let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) { + ( + meta.len(), + meta.modified() + .map(|x| { + x.duration_since(std::time::SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0) + }) + .unwrap_or(0) as u64, + ) + } else { + (0, 0) + }; + files.push(FileEntry { + entry_type: FileType::File.into(), + size, + modified_time, + ..Default::default() + }); + Ok(files) + } else { + bail!("Not exists"); + } +} + +pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType> { + read_dir_recursive(&get_path(path), &get_path(""), include_hidden) +} + +#[derive(Default)] +pub struct TransferJob { + id: i32, + path: PathBuf, + files: Vec, + file_num: i32, + file: Option, + total_size: u64, + finished_size: u64, + transfered: u64, +} + +#[inline] +fn get_ext(name: &str) -> &str { + if let Some(i) = name.rfind(".") { + return &name[i + 1..]; + } + "" +} + +#[inline] +fn is_compressed_file(name: &str) -> bool { + let ext = get_ext(name); + ext == "xz" + || ext == "gz" + || ext == "zip" + || ext == "7z" + || ext == "rar" + || ext == "bz2" + || ext == "tgz" + || ext == "png" + || ext == "jpg" +} + +impl TransferJob { + pub fn new_write(id: i32, path: String, files: Vec) -> Self { + let total_size = files.iter().map(|x| x.size as u64).sum(); + Self { + id, + path: get_path(&path), + files, + total_size, + ..Default::default() + } + } + + pub fn new_read(id: i32, path: String, include_hidden: bool) -> ResultType { + let files = get_recursive_files(&path, include_hidden)?; + let total_size = files.iter().map(|x| x.size as u64).sum(); + Ok(Self { + id, + path: get_path(&path), + files, + total_size, + ..Default::default() + }) + } + + #[inline] + pub fn files(&self) -> &Vec { + &self.files + } + + #[inline] + pub fn set_files(&mut self, files: Vec) { + self.files = files; + } + + #[inline] + pub fn id(&self) -> i32 { + self.id + } + + #[inline] + pub fn total_size(&self) -> u64 { + self.total_size + } + + #[inline] + pub fn finished_size(&self) -> u64 { + self.finished_size + } + + #[inline] + pub fn transfered(&self) -> u64 { + self.transfered + } + + #[inline] + pub fn file_num(&self) -> i32 { + self.file_num + } + + pub fn modify_time(&self) { + let file_num = self.file_num as usize; + if file_num < self.files.len() { + let entry = &self.files[file_num]; + let path = self.join(&entry.name); + let download_path = format!("{}.download", get_string(&path)); + std::fs::rename(&download_path, &path).ok(); + filetime::set_file_mtime( + &path, + filetime::FileTime::from_unix_time(entry.modified_time as _, 0), + ) + .ok(); + } + } + + pub fn remove_download_file(&self) { + let file_num = self.file_num as usize; + if file_num < self.files.len() { + let entry = &self.files[file_num]; + let path = self.join(&entry.name); + let download_path = format!("{}.download", get_string(&path)); + std::fs::remove_file(&download_path).ok(); + } + } + + pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { + if block.id != self.id { + bail!("Wrong id"); + } + let file_num = block.file_num as usize; + if file_num >= self.files.len() { + bail!("Wrong file number"); + } + if file_num != self.file_num as usize || self.file.is_none() { + self.modify_time(); + if let Some(file) = self.file.as_mut() { + file.sync_all().await?; + } + self.file_num = block.file_num; + let entry = &self.files[file_num]; + let path = self.join(&entry.name); + if let Some(p) = path.parent() { + std::fs::create_dir_all(p).ok(); + } + let path = format!("{}.download", get_string(&path)); + self.file = Some(File::create(&path).await?); + } + if block.compressed { + let tmp = decompress(&block.data); + self.file.as_mut().unwrap().write_all(&tmp).await?; + self.finished_size += tmp.len() as u64; + } else { + self.file.as_mut().unwrap().write_all(&block.data).await?; + self.finished_size += block.data.len() as u64; + } + self.transfered += block.data.len() as u64; + Ok(()) + } + + #[inline] + fn join(&self, name: &str) -> PathBuf { + if name.is_empty() { + self.path.clone() + } else { + self.path.join(name) + } + } + + pub async fn read(&mut self) -> ResultType> { + let file_num = self.file_num as usize; + if file_num >= self.files.len() { + self.file.take(); + return Ok(None); + } + let name = &self.files[file_num].name; + if self.file.is_none() { + match File::open(self.join(&name)).await { + Ok(file) => { + self.file = Some(file); + } + Err(err) => { + self.file_num += 1; + return Err(err.into()); + } + } + } + const BUF_SIZE: usize = 128 * 1024; + let mut buf: Vec = Vec::with_capacity(BUF_SIZE); + unsafe { + buf.set_len(BUF_SIZE); + } + let mut compressed = false; + let mut offset: usize = 0; + loop { + match self.file.as_mut().unwrap().read(&mut buf[offset..]).await { + Err(err) => { + self.file_num += 1; + self.file = None; + return Err(err.into()); + } + Ok(n) => { + offset += n; + if n == 0 || offset == BUF_SIZE { + break; + } + } + } + } + unsafe { buf.set_len(offset) }; + if offset == 0 { + self.file_num += 1; + self.file = None; + } else { + self.finished_size += offset as u64; + if !is_compressed_file(name) { + let tmp = compress(&buf, COMPRESS_LEVEL); + if tmp.len() < buf.len() { + buf = tmp; + compressed = true; + } + } + self.transfered += buf.len() as u64; + } + Ok(Some(FileTransferBlock { + id: self.id, + file_num: file_num as _, + data: buf.into(), + compressed, + ..Default::default() + })) + } +} + +#[inline] +pub fn new_error(id: i32, err: T, file_num: i32) -> Message { + let mut resp = FileResponse::new(); + resp.set_error(FileTransferError { + id, + error: err.to_string(), + file_num, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn new_dir(id: i32, files: Vec) -> Message { + let mut resp = FileResponse::new(); + resp.set_dir(FileDirectory { + id, + entries: files.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn new_block(block: FileTransferBlock) -> Message { + let mut resp = FileResponse::new(); + resp.set_block(block); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn new_receive(id: i32, path: String, files: Vec) -> Message { + let mut action = FileAction::new(); + action.set_receive(FileTransferReceiveRequest { + id, + path, + files: files.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_action(action); + msg_out +} + +#[inline] +pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message { + let mut action = FileAction::new(); + action.set_send(FileTransferSendRequest { + id, + path, + include_hidden, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_action(action); + msg_out +} + +#[inline] +pub fn new_done(id: i32, file_num: i32) -> Message { + let mut resp = FileResponse::new(); + resp.set_done(FileTransferDone { + id, + file_num, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn remove_job(id: i32, jobs: &mut Vec) { + *jobs = jobs.drain(0..).filter(|x| x.id() != id).collect(); +} + +#[inline] +pub fn get_job(id: i32, jobs: &mut Vec) -> Option<&mut TransferJob> { + jobs.iter_mut().filter(|x| x.id() == id).next() +} + +pub async fn handle_read_jobs( + jobs: &mut Vec, + stream: &mut crate::Stream, +) -> ResultType<()> { + let mut finished = Vec::new(); + for job in jobs.iter_mut() { + match job.read().await { + Err(err) => { + stream + .send(&new_error(job.id(), err, job.file_num())) + .await?; + } + Ok(Some(block)) => { + stream.send(&new_block(block)).await?; + } + Ok(None) => { + finished.push(job.id()); + stream.send(&new_done(job.id(), job.file_num())).await?; + } + } + } + for id in finished { + remove_job(id, jobs); + } + Ok(()) +} + +pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { + let fd = read_dir(path, true)?; + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::Dir) => { + remove_all_empty_dir(&path.join(&entry.name)).ok(); + } + Ok(FileType::DirLink) | Ok(FileType::FileLink) => { + std::fs::remove_file(&path.join(&entry.name)).ok(); + } + _ => {} + } + } + std::fs::remove_dir(path).ok(); + Ok(()) +} + +#[inline] +pub fn remove_file(file: &str) -> ResultType<()> { + std::fs::remove_file(get_path(file))?; + Ok(()) +} + +#[inline] +pub fn create_dir(dir: &str) -> ResultType<()> { + std::fs::create_dir_all(get_path(dir))?; + Ok(()) +} diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs new file mode 100644 index 00000000000..4eb1db20d98 --- /dev/null +++ b/libs/hbb_common/src/lib.rs @@ -0,0 +1,215 @@ +pub mod compress; +#[path = "./protos/message.rs"] +pub mod message_proto; +#[path = "./protos/rendezvous.rs"] +pub mod rendezvous_proto; +pub use bytes; +pub use futures; +pub use protobuf; +use socket2::{Domain, Socket, Type}; +use std::{ + fs::File, + io::{self, BufRead}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs}, + path::Path, + time::{self, SystemTime, UNIX_EPOCH}, +}; +pub use tokio; +pub use tokio_util; +pub mod tcp; +pub mod udp; +pub use env_logger; +pub use log; +pub mod bytes_codec; +#[cfg(feature = "quic")] +pub mod quic; +pub use anyhow::{self, bail}; +pub use futures_util; +pub mod config; +pub mod fs; +pub use sodiumoxide; + +#[cfg(feature = "quic")] +pub type Stream = quic::Connection; +#[cfg(not(feature = "quic"))] +pub type Stream = tcp::FramedStream; + +#[inline] +pub async fn sleep(sec: f32) { + tokio::time::delay_for(time::Duration::from_secs_f32(sec)).await; +} + +#[macro_export] +macro_rules! allow_err { + ($e:expr) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}:{}:{}:{}", + err, + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; +} + +#[inline] +pub fn timeout(ms: u64, future: T) -> tokio::time::Timeout { + tokio::time::timeout(std::time::Duration::from_millis(ms), future) +} + +fn new_socket(addr: SocketAddr, tcp: bool, reuse: bool) -> Result { + let stype = { + if tcp { + Type::stream() + } else { + Type::dgram() + } + }; + let socket = match addr { + SocketAddr::V4(..) => Socket::new(Domain::ipv4(), stype, None), + SocketAddr::V6(..) => Socket::new(Domain::ipv6(), stype, None), + }?; + if reuse { + // windows has no reuse_port, but it's reuse_address + // almost equals to unix's reuse_port + reuse_address, + // though may introduce nondeterministic bahavior + #[cfg(unix)] + socket.set_reuse_port(true)?; + socket.set_reuse_address(true)?; + } + socket.bind(&addr.into())?; + Ok(socket) +} + +pub type ResultType = anyhow::Result; + +/// Certain router and firewalls scan the packet and if they +/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address + +pub struct AddrMangle(); + +impl AddrMangle { + pub fn encode(addr: SocketAddr) -> Vec { + match addr { + SocketAddr::V4(addr_v4) => { + let tm = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_micros() as u32) as u128; + let ip = u32::from_ne_bytes(addr_v4.ip().octets()) as u128; + let port = addr.port() as u128; + let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF)); + let bytes = v.to_ne_bytes(); + let mut n_padding = 0; + for i in bytes.iter().rev() { + if i == &0u8 { + n_padding += 1; + } else { + break; + } + } + bytes[..(16 - n_padding)].to_vec() + } + _ => { + panic!("Only support ipv4"); + } + } + } + + pub fn decode(bytes: &[u8]) -> SocketAddr { + let mut padded = [0u8; 16]; + padded[..bytes.len()].copy_from_slice(&bytes); + let number = u128::from_ne_bytes(padded); + let tm = (number >> 17) & (u32::max_value() as u128); + let ip = (((number >> 49) - tm) as u32).to_ne_bytes(); + let port = (number & 0xFFFFFF) - (tm & 0xFFFF); + SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), + port as u16, + )) + } +} + +pub fn get_version_from_url(url: &str) -> String { + let n = url.chars().count(); + let a = url + .chars() + .rev() + .enumerate() + .filter(|(_, x)| x == &'-') + .next() + .map(|(i, _)| i); + if let Some(a) = a { + let b = url + .chars() + .rev() + .enumerate() + .filter(|(_, x)| x == &'.') + .next() + .map(|(i, _)| i); + if let Some(b) = b { + if a > b { + if url + .chars() + .skip(n - b) + .collect::() + .parse::() + .is_ok() + { + return url.chars().skip(n - a).collect(); + } else { + return url.chars().skip(n - a).take(a - b - 1).collect(); + } + } else { + return url.chars().skip(n - a).collect(); + } + } + } + "".to_owned() +} + +pub fn to_socket_addr(host: &str) -> ResultType { + let addrs: Vec = host.to_socket_addrs()?.collect(); + if addrs.is_empty() { + bail!("Failed to solve {}", host); + } + Ok(addrs[0]) +} + +pub fn gen_version() { + let mut file = File::create("./src/version.rs").unwrap(); + for line in read_lines("Cargo.toml").unwrap() { + if let Ok(line) = line { + let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect(); + if ab.len() == 2 && ab[0] == "version" { + use std::io::prelude::*; + file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes()) + .ok(); + file.sync_all().ok(); + break; + } + } + } +} + +fn read_lines

(filename: P) -> io::Result>> +where + P: AsRef, +{ + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_mangle() { + let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + } +} diff --git a/libs/hbb_common/src/quic.rs b/libs/hbb_common/src/quic.rs new file mode 100644 index 00000000000..ada2acd63c9 --- /dev/null +++ b/libs/hbb_common/src/quic.rs @@ -0,0 +1,135 @@ +use crate::{allow_err, anyhow::anyhow, ResultType}; +use protobuf::Message; +use std::{net::SocketAddr, sync::Arc}; +use tokio::{self, stream::StreamExt, sync::mpsc}; + +const QUIC_HBB: &[&[u8]] = &[b"hbb"]; +const SERVER_NAME: &str = "hbb"; + +type Sender = mpsc::UnboundedSender; +type Receiver = mpsc::UnboundedReceiver; + +pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> { + let mut transport_config = quinn::TransportConfig::default(); + transport_config.stream_window_uni(0); + let mut server_config = quinn::ServerConfig::default(); + server_config.transport = Arc::new(transport_config); + let mut server_config = quinn::ServerConfigBuilder::new(server_config); + server_config.protocols(QUIC_HBB); + // server_config.enable_keylog(); + // server_config.use_stateless_retry(true); + let mut endpoint = quinn::Endpoint::builder(); + endpoint.listen(server_config.build()); + let (end, incoming) = endpoint.with_socket(socket)?; + Ok((Server { incoming }, end.local_addr()?)) +} + +pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType { + let mut endpoint = quinn::Endpoint::builder(); + let mut client_config = quinn::ClientConfigBuilder::default(); + client_config.protocols(QUIC_HBB); + //client_config.enable_keylog(); + endpoint.default_client_config(client_config.build()); + let (endpoint, _) = endpoint.bind(local_addr)?; + let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?; + Connection::new_for_client(new_conn.connection).await +} + +pub struct Server { + incoming: quinn::Incoming, +} + +impl Server { + #[inline] + pub async fn next(&mut self) -> ResultType> { + Connection::new_for_server(&mut self.incoming).await + } +} + +pub struct Connection { + conn: quinn::Connection, + tx: quinn::SendStream, + rx: Receiver, +} + +type Value = ResultType>; + +impl Connection { + async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType> { + if let Some(conn) = incoming.next().await { + let quinn::NewConnection { + connection: conn, + // uni_streams, + mut bi_streams, + .. + } = conn.await?; + let (tx, rx) = mpsc::unbounded_channel::(); + tokio::spawn(async move { + loop { + let stream = bi_streams.next().await; + if let Some(stream) = stream { + let stream = match stream { + Err(e) => { + tx.send(Err(e.into())).ok(); + break; + } + Ok(s) => s, + }; + let cloned = tx.clone(); + tokio::spawn(async move { + allow_err!(handle_request(stream.1, cloned).await); + }); + } else { + tx.send(Err(anyhow!("Reset by the peer"))).ok(); + break; + } + } + log::info!("Exit connection outer loop"); + }); + let tx = conn.open_uni().await?; + Ok(Some(Self { conn, tx, rx })) + } else { + Ok(None) + } + } + + async fn new_for_client(conn: quinn::Connection) -> ResultType { + let (tx, rx_quic) = conn.open_bi().await?; + let (tx_mpsc, rx) = mpsc::unbounded_channel::(); + tokio::spawn(async move { + allow_err!(handle_request(rx_quic, tx_mpsc).await); + }); + Ok(Self { conn, tx, rx }) + } + + #[inline] + pub async fn next(&mut self) -> Option { + // None is returned when all Sender halves have dropped, + // indicating that no further values can be sent on the channel. + self.rx.recv().await + } + + #[inline] + pub fn remote_address(&self) -> SocketAddr { + self.conn.remote_address() + } + + #[inline] + pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> { + self.tx.write_all(bytes).await?; + Ok(()) + } + + #[inline] + pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> { + match msg.write_to_bytes() { + Ok(bytes) => self.send_raw(&bytes).await?, + err => allow_err!(err), + } + Ok(()) + } +} + +async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> { + Ok(()) +} diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs new file mode 100644 index 00000000000..cc8432e24dd --- /dev/null +++ b/libs/hbb_common/src/tcp.rs @@ -0,0 +1,146 @@ +use crate::{bail, bytes_codec::BytesCodec, ResultType}; +use bytes::{BufMut, Bytes, BytesMut}; +use futures::SinkExt; +use protobuf::Message; +use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; +use std::{ + io::{Error, ErrorKind}, + ops::{Deref, DerefMut}, +}; +use tokio::{ + net::{TcpListener, TcpStream, ToSocketAddrs}, + stream::StreamExt, +}; +use tokio_util::codec::Framed; + +pub struct FramedStream(Framed, Option<(Key, u64, u64)>); + +impl Deref for FramedStream { + type Target = Framed; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FramedStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FramedStream { + pub async fn new( + remote_addr: T, + local_addr: T2, + ms_timeout: u64, + ) -> ResultType { + for local_addr in local_addr.to_socket_addrs().await? { + for remote_addr in remote_addr.to_socket_addrs().await? { + if let Ok(stream) = super::timeout( + ms_timeout, + TcpStream::connect_std( + super::new_socket(local_addr, true, true)?.into_tcp_stream(), + &remote_addr, + ), + ) + .await? + { + return Ok(Self(Framed::new(stream, BytesCodec::new()), None)); + } + } + } + bail!("could not resolve to any address"); + } + + pub fn from(stream: TcpStream) -> Self { + Self(Framed::new(stream, BytesCodec::new()), None) + } + + pub fn set_raw(&mut self) { + self.0.codec_mut().set_raw(); + self.1 = None; + } + + pub fn is_secured(&self) -> bool { + self.1.is_some() + } + + #[inline] + pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { + self.send_raw(msg.write_to_bytes()?).await + } + + #[inline] + pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { + let mut msg = msg; + if let Some(key) = self.1.as_mut() { + key.1 += 1; + let nonce = Self::get_nonce(key.1); + msg = secretbox::seal(&msg, &nonce, &key.0); + } + self.0.send(bytes::Bytes::from(msg)).await?; + Ok(()) + } + + pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { + self.0.send(bytes).await?; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + let mut res = self.0.next().await; + if let Some(key) = self.1.as_mut() { + if let Some(Ok(bytes)) = res.as_mut() { + key.2 += 1; + let nonce = Self::get_nonce(key.2); + match secretbox::open(&bytes, &nonce, &key.0) { + Ok(res) => { + bytes.clear(); + bytes.put_slice(&res); + } + Err(()) => { + return Some(Err(Error::new(ErrorKind::Other, "decryption error"))); + } + } + } + } + res + } + + #[inline] + pub async fn next_timeout(&mut self, ms: u64) -> Option> { + if let Ok(res) = super::timeout(ms, self.next()).await { + res + } else { + None + } + } + + pub fn set_key(&mut self, key: Key) { + self.1 = Some((key, 0, 0)); + } + + fn get_nonce(seqnum: u64) -> Nonce { + let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]); + nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_ne_bytes()); + nonce + } +} + +const DEFAULT_BACKLOG: i32 = 128; + +#[allow(clippy::never_loop)] +pub async fn new_listener(addr: T, reuse: bool) -> ResultType { + if !reuse { + Ok(TcpListener::bind(addr).await?) + } else { + for addr in addr.to_socket_addrs().await? { + let socket = super::new_socket(addr, true, true)?; + socket.listen(DEFAULT_BACKLOG)?; + return Ok(TcpListener::from_std(socket.into_tcp_listener())?); + } + bail!("could not resolve to any address"); + } +} diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs new file mode 100644 index 00000000000..0a60c7a56ce --- /dev/null +++ b/libs/hbb_common/src/udp.rs @@ -0,0 +1,75 @@ +use crate::{bail, ResultType}; +use bytes::BytesMut; +use futures::SinkExt; +use protobuf::Message; +use std::{ + io::Error, + net::SocketAddr, + ops::{Deref, DerefMut}, +}; +use tokio::{net::ToSocketAddrs, net::UdpSocket, stream::StreamExt}; +use tokio_util::{codec::BytesCodec, udp::UdpFramed}; + +pub struct FramedSocket(UdpFramed); + +impl Deref for FramedSocket { + type Target = UdpFramed; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FramedSocket { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FramedSocket { + pub async fn new(addr: T) -> ResultType { + let socket = UdpSocket::bind(addr).await?; + Ok(Self(UdpFramed::new(socket, BytesCodec::new()))) + } + + #[allow(clippy::never_loop)] + pub async fn new_reuse(addr: T) -> ResultType { + for addr in addr.to_socket_addrs().await? { + return Ok(Self(UdpFramed::new( + UdpSocket::from_std(super::new_socket(addr, false, true)?.into_udp_socket())?, + BytesCodec::new(), + ))); + } + bail!("could not resolve to any address"); + } + + #[inline] + pub async fn send(&mut self, msg: &impl Message, addr: SocketAddr) -> ResultType<()> { + self.0 + .send((bytes::Bytes::from(msg.write_to_bytes().unwrap()), addr)) + .await?; + Ok(()) + } + + #[inline] + pub async fn send_raw(&mut self, msg: &'static [u8], addr: SocketAddr) -> ResultType<()> { + self.0.send((bytes::Bytes::from(msg), addr)).await?; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + self.0.next().await + } + + #[inline] + pub async fn next_timeout(&mut self, ms: u64) -> Option> { + if let Ok(res) = + tokio::time::timeout(std::time::Duration::from_millis(ms), self.0.next()).await + { + res + } else { + None + } + } +} diff --git a/libs/magnum-opus/.gitignore b/libs/magnum-opus/.gitignore new file mode 100644 index 00000000000..a9d37c560c6 --- /dev/null +++ b/libs/magnum-opus/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/libs/magnum-opus/Cargo.toml b/libs/magnum-opus/Cargo.toml new file mode 100644 index 00000000000..3fa1cefbb40 --- /dev/null +++ b/libs/magnum-opus/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "magnum-opus" +version = "0.3.4" +authors = ["Tad Hardesty ", "Sergey Duck "] +edition = "2018" +description = "Safe Rust bindings for libopus" +readme = "README.md" +license = "MIT/Apache-2.0" +keywords = ["opus", "codec", "voice", "sound", "audio"] +categories = ["api-bindings", "encoding", "compression", + "multimedia::audio", "multimedia::encoding"] + +repository = "https://github.com/DuckerMan/magnum-opus" +documentation = "https://docs.rs/magnum-opus" + +[build-dependencies] +target_build_utils = "0.3" +bindgen = "0.53" diff --git a/libs/magnum-opus/LICENSE-APACHE b/libs/magnum-opus/LICENSE-APACHE new file mode 100644 index 00000000000..16fe87b06e8 --- /dev/null +++ b/libs/magnum-opus/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/libs/magnum-opus/LICENSE-MIT b/libs/magnum-opus/LICENSE-MIT new file mode 100644 index 00000000000..610d1f72e10 --- /dev/null +++ b/libs/magnum-opus/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2016 Tad Hardesty + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/libs/magnum-opus/README.md b/libs/magnum-opus/README.md new file mode 100644 index 00000000000..d26f517717a --- /dev/null +++ b/libs/magnum-opus/README.md @@ -0,0 +1,22 @@ +# magnum-opus [![](https://meritbadge.herokuapp.com/magnum-opus)](https://crates.io/crates/magnum-opus) [![](https://img.shields.io/badge/docs-online-2020ff.svg)](https://docs.rs/magnum-opus) + +### This is the fork of @SpaceManiac repo, which now is abandoned + +Safe Rust bindings for libopus. The rustdoc (available through `cargo doc`) +includes brief descriptions for methods, and detailed API information can be +found at the [libopus documentation](https://opus-codec.org/docs/opus_api-1.1.2/). + +## License + +Licensed under either of + + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. diff --git a/libs/magnum-opus/build.rs b/libs/magnum-opus/build.rs new file mode 100644 index 00000000000..1522604e259 --- /dev/null +++ b/libs/magnum-opus/build.rs @@ -0,0 +1,89 @@ +use std::{ + env, + path::{Path, PathBuf}, +}; + +fn find_package(name: &str) -> Vec { + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: PathBuf = vcpkg_root.into(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let target = if target_os == "macos" { + "x64-osx".to_owned() + } else if target_os == "windows" { + "x64-windows-static".to_owned() + } else if target_os == "android" { + format!("{}-android-static", target_arch) + } else { + "x64-linux".to_owned() + }; + println!("cargo:info={}", target); + path.push("installed"); + path.push(target); + println!( + "{}", + format!("cargo:rustc-link-lib={}", name.trim_start_matches("lib")) + ); + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + let include = path.join("include"); + println!("{}", format!("cargo:include={}", include.to_str().unwrap())); + vec![include] +} + +fn generate_bindings(ffi_header: &Path, include_paths: &[PathBuf], ffi_rs: &Path) { + #[derive(Debug)] + struct ParseCallbacks; + impl bindgen::callbacks::ParseCallbacks for ParseCallbacks { + fn int_macro(&self, name: &str, _value: i64) -> Option { + if name.starts_with("OPUS") { + Some(bindgen::callbacks::IntKind::Int) + } else { + None + } + } + } + let mut b = bindgen::Builder::default() + .header(ffi_header.to_str().unwrap()) + .parse_callbacks(Box::new(ParseCallbacks)) + .generate_comments(false); + + for dir in include_paths { + b = b.clang_arg(format!("-I{}", dir.display())); + } + + b.generate().unwrap().write_to_file(ffi_rs).unwrap(); +} + +fn gen_opus() { + let includes = find_package("opus"); + let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); + let src_dir = Path::new(&src_dir); + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + let ffi_header = src_dir.join("opus_ffi.h"); + println!("rerun-if-changed={}", ffi_header.display()); + for dir in &includes { + println!("rerun-if-changed={}", dir.display()); + } + + let ffi_rs = out_dir.join("opus_ffi.rs"); + generate_bindings(&ffi_header, &includes, &ffi_rs); +} + +fn main() { + gen_opus() +} diff --git a/libs/magnum-opus/opus_ffi.h b/libs/magnum-opus/opus_ffi.h new file mode 100644 index 00000000000..f6d3aba34d0 --- /dev/null +++ b/libs/magnum-opus/opus_ffi.h @@ -0,0 +1 @@ +#include diff --git a/libs/magnum-opus/src/lib.rs b/libs/magnum-opus/src/lib.rs new file mode 100644 index 00000000000..9fb2e11ec53 --- /dev/null +++ b/libs/magnum-opus/src/lib.rs @@ -0,0 +1,843 @@ +// Copyright 2016 Tad Hardesty +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! High-level bindings for libopus. +//! +//! Only brief descriptions are included here. For detailed information, consult +//! the [libopus documentation](https://opus-codec.org/docs/opus_api-1.1.2/). +#![warn(missing_docs)] + +mod opus_ffi; +use opus_ffi as ffi; + + +use std::ffi::CStr; +use std::marker::PhantomData; + +use std::os::raw::c_int; + +// ============================================================================ +// Constants + +// Generic CTLs +const OPUS_RESET_STATE: c_int = 4028; // void +const OPUS_GET_FINAL_RANGE: c_int = 4031; // out *u32 +const OPUS_GET_BANDWIDTH: c_int = 4009; // out *i32 +const OPUS_GET_SAMPLE_RATE: c_int = 4029; // out *i32 +// Encoder CTLs +const OPUS_SET_BITRATE: c_int = 4002; // in i32 +const OPUS_GET_BITRATE: c_int = 4003; // out *i32 +const OPUS_SET_VBR: c_int = 4006; // in i32 +const OPUS_GET_VBR: c_int = 4007; // out *i32 +const OPUS_SET_VBR_CONSTRAINT: c_int = 4020; // in i32 +const OPUS_GET_VBR_CONSTRAINT: c_int = 4021; // out *i32 +const OPUS_SET_INBAND_FEC: c_int = 4012; // in i32 +const OPUS_GET_INBAND_FEC: c_int = 4013; // out *i32 +const OPUS_SET_PACKET_LOSS_PERC: c_int = 4014; // in i32 +const OPUS_GET_PACKET_LOSS_PERC: c_int = 4015; // out *i32 +const OPUS_GET_LOOKAHEAD: c_int = 4027; // out *i32 +// Decoder CTLs +const OPUS_SET_GAIN: c_int = 4034; // in i32 +const OPUS_GET_GAIN: c_int = 4045; // out *i32 +const OPUS_GET_LAST_PACKET_DURATION: c_int = 4039; // out *i32 +const OPUS_GET_PITCH: c_int = 4033; // out *i32 + +// Bitrate +const OPUS_AUTO: c_int = -1000; +const OPUS_BITRATE_MAX: c_int = -1; + +/// The possible applications for the codec. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Application { + /// Best for most VoIP/videoconference applications where listening quality + /// and intelligibility matter most. + Voip = 2048, + /// Best for broadcast/high-fidelity application where the decoded audio + /// should be as close as possible to the input. + Audio = 2049, + /// Only use when lowest-achievable latency is what matters most. + LowDelay = 2051, +} + +/// The available channel setings. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Channels { + /// One channel. + Mono = 1, + /// Two channels, left and right. + Stereo = 2, +} + +/// The available bandwidth level settings. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Bandwidth { + /// Auto/default setting. + Auto = -1000, + /// 4kHz bandpass. + Narrowband = 1101, + /// 6kHz bandpass. + Mediumband = 1102, + /// 8kHz bandpass. + Wideband = 1103, + /// 12kHz bandpass. + Superwideband = 1104, + /// 20kHz bandpass. + Fullband = 1105, +} + +impl Bandwidth { + fn from_int(value: i32) -> Option { + Some(match value { + -1000 => Bandwidth::Auto, + 1101 => Bandwidth::Narrowband, + 1102 => Bandwidth::Mediumband, + 1103 => Bandwidth::Wideband, + 1104 => Bandwidth::Superwideband, + 1105 => Bandwidth::Fullband, + _ => return None, + }) + } + + fn decode(value: i32, what: &'static str) -> Result { + match Bandwidth::from_int(value) { + Some(bandwidth) => Ok(bandwidth), + None => Err(Error::bad_arg(what)), + } + } +} + +/// Possible error codes. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ErrorCode { + /// One or more invalid/out of range arguments. + BadArg = -1, + /// Not enough bytes allocated in the buffer. + BufferTooSmall = -2, + /// An internal error was detected. + InternalError = -3, + /// The compressed data passed is corrupted. + InvalidPacket = -4, + /// Invalid/unsupported request number. + Unimplemented = -5, + /// An encoder or decoder structure is invalid or already freed. + InvalidState = -6, + /// Memory allocation has failed. + AllocFail = -7, + /// An unknown failure. + Unknown = -8, +} + +impl ErrorCode { + fn from_int(value: c_int) -> ErrorCode { + use ErrorCode::*; + match value { + ffi::OPUS_BAD_ARG => BadArg, + ffi::OPUS_BUFFER_TOO_SMALL => BufferTooSmall, + ffi::OPUS_INTERNAL_ERROR => InternalError, + ffi::OPUS_INVALID_PACKET => InvalidPacket, + ffi::OPUS_UNIMPLEMENTED => Unimplemented, + ffi::OPUS_INVALID_STATE => InvalidState, + ffi::OPUS_ALLOC_FAIL => AllocFail, + _ => Unknown, + } + } + + /// Get a human-readable error string for this error code. + pub fn description(self) -> &'static str { + // should always be ASCII and non-null for any input + unsafe { CStr::from_ptr(ffi::opus_strerror(self as c_int)) }.to_str().unwrap() + } +} + +/// Possible bitrates. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Bitrate { + /// Explicit bitrate choice (in bits/second). + Bits(i32), + /// Maximum bitrate allowed (up to maximum number of bytes for the packet). + Max, + /// Default bitrate decided by the encoder (not recommended). + Auto, +} + +/// Get the libopus version string. +/// +/// Applications may look for the substring "-fixed" in the version string to +/// determine whether they have a fixed-point or floating-point build at +/// runtime. +pub fn version() -> &'static str { + // verison string should always be ASCII + unsafe { CStr::from_ptr(ffi::opus_get_version_string()) }.to_str().unwrap() +} + +macro_rules! ffi { + ($f:ident $(, $rest:expr)*) => { + match unsafe { ffi::$f($($rest),*) } { + code if code < 0 => return Err(Error::from_code(stringify!($f), code)), + code => code, + } + } +} + +macro_rules! ctl { + ($f:ident, $this:ident, $ctl:ident, $($rest:expr),*) => { + match unsafe { ffi::$f($this.ptr, $ctl, $($rest),*) } { + code if code < 0 => return Err(Error::from_code( + concat!(stringify!($f), "(", stringify!($ctl), ")"), + code, + )), + _ => (), + } + } +} + +// ============================================================================ +// Encoder + +macro_rules! enc_ctl { + ($this:ident, $ctl:ident $(, $rest:expr)*) => { + ctl!(opus_encoder_ctl, $this, $ctl, $($rest),*) + } +} + +/// An Opus encoder with associated state. +#[derive(Debug)] +pub struct Encoder { + ptr: *mut ffi::OpusEncoder, + channels: Channels, +} + +impl Encoder { + /// Create and initialize an encoder. + pub fn new(sample_rate: u32, channels: Channels, mode: Application) -> Result { + let mut error = 0; + let ptr = unsafe { ffi::opus_encoder_create( + sample_rate as i32, + channels as c_int, + mode as c_int, + &mut error) }; + if error != ffi::OPUS_OK || ptr.is_null() { + Err(Error::from_code("opus_encoder_create", error)) + } else { + Ok(Encoder { ptr: ptr, channels: channels }) + } + } + + /// Encode an Opus frame. + pub fn encode(&mut self, input: &[i16], output: &mut [u8]) -> Result { + let len = ffi!(opus_encode, self.ptr, + input.as_ptr(), len(input) / self.channels as c_int, + output.as_mut_ptr(), len(output)); + Ok(len as usize) + } + + /// Encode an Opus frame from floating point input. + pub fn encode_float(&mut self, input: &[f32], output: &mut [u8]) -> Result { + let len = ffi!(opus_encode_float, self.ptr, + input.as_ptr(), len(input) / self.channels as c_int, + output.as_mut_ptr(), len(output)); + Ok(len as usize) + } + + /// Encode an Opus frame to a new buffer. + pub fn encode_vec(&mut self, input: &[i16], max_size: usize) -> Result> { + let mut output: Vec = vec![0; max_size]; + let result = self.encode(input, output.as_mut_slice())?; + output.truncate(result); + Ok(output) + } + + /// Encode an Opus frame from floating point input to a new buffer. + pub fn encode_vec_float(&mut self, input: &[f32], max_size: usize) -> Result> { + let mut output: Vec = vec![0; max_size]; + let result = self.encode_float(input, output.as_mut_slice())?; + output.truncate(result); + Ok(output) + } + + // ------------ + // Generic CTLs + + /// Reset the codec state to be equivalent to a freshly initialized state. + pub fn reset_state(&mut self) -> Result<()> { + enc_ctl!(self, OPUS_RESET_STATE); + Ok(()) + } + + /// Get the final range of the codec's entropy coder. + pub fn get_final_range(&mut self) -> Result { + let mut value: u32 = 0; + enc_ctl!(self, OPUS_GET_FINAL_RANGE, &mut value); + Ok(value) + } + + /// Get the encoder's configured bandpass. + pub fn get_bandwidth(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_BANDWIDTH, &mut value); + Bandwidth::decode(value, "opus_encoder_ctl(OPUS_GET_BANDWIDTH)") + } + + /// Get the samping rate the encoder was intialized with. + pub fn get_sample_rate(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_SAMPLE_RATE, &mut value); + Ok(value as u32) + } + + // ------------ + // Encoder CTLs + + /// Set the encoder's bitrate. + pub fn set_bitrate(&mut self, value: Bitrate) -> Result<()> { + let value: i32 = match value { + Bitrate::Auto => OPUS_AUTO, + Bitrate::Max => OPUS_BITRATE_MAX, + Bitrate::Bits(b) => b, + }; + enc_ctl!(self, OPUS_SET_BITRATE, value); + Ok(()) + } + + /// Get the encoder's bitrate. + pub fn get_bitrate(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_BITRATE, &mut value); + Ok(match value { + OPUS_AUTO => Bitrate::Auto, + OPUS_BITRATE_MAX => Bitrate::Max, + _ => Bitrate::Bits(value), + }) + } + + /// Enable or disable variable bitrate. + pub fn set_vbr(&mut self, vbr: bool) -> Result<()> { + let value: i32 = if vbr { 1 } else { 0 }; + enc_ctl!(self, OPUS_SET_VBR, value); + Ok(()) + } + + /// Determine if variable bitrate is enabled. + pub fn get_vbr(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_VBR, &mut value); + Ok(value != 0) + } + + /// Enable or disable constrained VBR. + pub fn set_vbr_constraint(&mut self, vbr: bool) -> Result<()> { + let value: i32 = if vbr { 1 } else { 0 }; + enc_ctl!(self, OPUS_SET_VBR_CONSTRAINT, value); + Ok(()) + } + + /// Determine if constrained VBR is enabled. + pub fn get_vbr_constraint(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_VBR_CONSTRAINT, &mut value); + Ok(value != 0) + } + + /// Configures the encoder's use of inband forward error correction (FEC). + pub fn set_inband_fec(&mut self, value: bool) -> Result<()> { + let value: i32 = if value { 1 } else { 0 }; + enc_ctl!(self, OPUS_SET_INBAND_FEC, value); + Ok(()) + } + + /// Gets encoder's configured use of inband forward error correction. + pub fn get_inband_fec(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_INBAND_FEC, &mut value); + Ok(value != 0) + } + + /// Sets the encoder's expected packet loss percentage. + pub fn set_packet_loss_perc(&mut self, value: i32) -> Result<()> { + enc_ctl!(self, OPUS_SET_PACKET_LOSS_PERC, value); + Ok(()) + } + + /// Gets the encoder's expected packet loss percentage. + pub fn get_packet_loss_perc(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_PACKET_LOSS_PERC, &mut value); + Ok(value) + } + + /// Gets the total samples of delay added by the entire codec. + pub fn get_lookahead(&mut self) -> Result { + let mut value: i32 = 0; + enc_ctl!(self, OPUS_GET_LOOKAHEAD, &mut value); + Ok(value) + } + + // TODO: Encoder-specific CTLs +} + +impl Drop for Encoder { + fn drop(&mut self) { + unsafe { ffi::opus_encoder_destroy(self.ptr) } + } +} + +// "A single codec state may only be accessed from a single thread at +// a time and any required locking must be performed by the caller. Separate +// streams must be decoded with separate decoder states and can be decoded +// in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK +// defined." +// +// In other words, opus states may be moved between threads at will. A special +// compilation mode intended for embedded platforms forbids multithreaded use +// of the library as a whole rather than on a per-state basis, but the opus-sys +// crate does not use this mode. +unsafe impl Send for Encoder {} + +// ============================================================================ +// Decoder + +macro_rules! dec_ctl { + ($this:ident, $ctl:ident $(, $rest:expr)*) => { + ctl!(opus_decoder_ctl, $this, $ctl, $($rest),*) + } +} + +/// An Opus decoder with associated state. +#[derive(Debug)] +pub struct Decoder { + ptr: *mut ffi::OpusDecoder, + channels: Channels, +} + +impl Decoder { + /// Create and initialize a decoder. + pub fn new(sample_rate: u32, channels: Channels) -> Result { + let mut error = 0; + let ptr = unsafe { ffi::opus_decoder_create( + sample_rate as i32, + channels as c_int, + &mut error) }; + if error != ffi::OPUS_OK || ptr.is_null() { + Err(Error::from_code("opus_decoder_create", error)) + } else { + Ok(Decoder { ptr: ptr, channels: channels }) + } + } + + /// Decode an Opus packet. + pub fn decode(&mut self, input: &[u8], output: &mut [i16], fec: bool) -> Result { + let ptr = match input.len() { + 0 => std::ptr::null(), + _ => input.as_ptr(), + }; + let len = ffi!(opus_decode, self.ptr, + ptr, len(input), + output.as_mut_ptr(), len(output) / self.channels as c_int, + fec as c_int); + Ok(len as usize) + } + + /// Decode an Opus packet with floating point output. + pub fn decode_float(&mut self, input: &[u8], output: &mut [f32], fec: bool) -> Result { + let ptr = match input.len() { + 0 => std::ptr::null(), + _ => input.as_ptr(), + }; + let len = ffi!(opus_decode_float, self.ptr, + ptr, len(input), + output.as_mut_ptr(), len(output) / self.channels as c_int, + fec as c_int); + Ok(len as usize) + } + + /// Get the number of samples of an Opus packet. + pub fn get_nb_samples(&self, packet: &[u8]) -> Result { + let len = ffi!(opus_decoder_get_nb_samples, self.ptr, + packet.as_ptr(), packet.len() as i32); + Ok(len as usize) + } + + // ------------ + // Generic CTLs + + /// Reset the codec state to be equivalent to a freshly initialized state. + pub fn reset_state(&mut self) -> Result<()> { + dec_ctl!(self, OPUS_RESET_STATE); + Ok(()) + } + + /// Get the final range of the codec's entropy coder. + pub fn get_final_range(&mut self) -> Result { + let mut value: u32 = 0; + dec_ctl!(self, OPUS_GET_FINAL_RANGE, &mut value); + Ok(value) + } + + /// Get the decoder's last bandpass. + pub fn get_bandwidth(&mut self) -> Result { + let mut value: i32 = 0; + dec_ctl!(self, OPUS_GET_BANDWIDTH, &mut value); + Bandwidth::decode(value, "opus_decoder_ctl(OPUS_GET_BANDWIDTH)") + } + + /// Get the samping rate the decoder was intialized with. + pub fn get_sample_rate(&mut self) -> Result { + let mut value: i32 = 0; + dec_ctl!(self, OPUS_GET_SAMPLE_RATE, &mut value); + Ok(value as u32) + } + + // ------------ + // Decoder CTLs + + /// Configures decoder gain adjustment. + /// + /// Scales the decoded output by a factor specified in Q8 dB units. This has + /// a maximum range of -32768 to 32768 inclusive, and returns `BadArg` + /// otherwise. The default is zero indicating no adjustment. This setting + /// survives decoder reset. + /// + /// `gain = pow(10, x / (20.0 * 256))` + pub fn set_gain(&mut self, gain: i32) -> Result<()> { + dec_ctl!(self, OPUS_SET_GAIN, gain); + Ok(()) + } + + /// Gets the decoder's configured gain adjustment. + pub fn get_gain(&mut self) -> Result { + let mut value: i32 = 0; + dec_ctl!(self, OPUS_GET_GAIN, &mut value); + Ok(value) + } + + /// Gets the duration (in samples) of the last packet successfully decoded + /// or concealed. + pub fn get_last_packet_duration(&mut self) -> Result { + let mut value: i32 = 0; + dec_ctl!(self, OPUS_GET_LAST_PACKET_DURATION, &mut value); + Ok(value as u32) + } + + /// Gets the pitch of the last decoded frame, if available. + /// + /// This can be used for any post-processing algorithm requiring the use of + /// pitch, e.g. time stretching/shortening. If the last frame was not + /// voiced, or if the pitch was not coded in the frame, then zero is + /// returned. + pub fn get_pitch(&mut self) -> Result { + let mut value: i32 = 0; + dec_ctl!(self, OPUS_GET_PITCH, &mut value); + Ok(value) + } +} + +impl Drop for Decoder { + fn drop(&mut self) { + unsafe { ffi::opus_decoder_destroy(self.ptr) } + } +} + +// See `unsafe impl Send for Encoder`. +unsafe impl Send for Decoder {} + +// ============================================================================ +// Packet Analysis + +/// Analyze raw Opus packets. +pub mod packet { + use super::*; + use super::ffi; + use std::{ptr, slice}; + + /// Get the bandwidth of an Opus packet. + pub fn get_bandwidth(packet: &[u8]) -> Result { + if packet.len() < 1 { + return Err(Error::bad_arg("opus_packet_get_bandwidth")); + } + let bandwidth = ffi!(opus_packet_get_bandwidth, packet.as_ptr()); + Bandwidth::decode(bandwidth, "opus_packet_get_bandwidth") + } + + /// Get the number of channels from an Opus packet. + pub fn get_nb_channels(packet: &[u8]) -> Result { + if packet.len() < 1 { + return Err(Error::bad_arg("opus_packet_get_nb_channels")); + } + let channels = ffi!(opus_packet_get_nb_channels, packet.as_ptr()); + match channels { + 1 => Ok(Channels::Mono), + 2 => Ok(Channels::Stereo), + _ => Err(Error::bad_arg("opus_packet_get_nb_channels")), + } + } + + /// Get the number of frames in an Opus packet. + pub fn get_nb_frames(packet: &[u8]) -> Result { + let frames = ffi!(opus_packet_get_nb_frames, packet.as_ptr(), len(packet)); + Ok(frames as usize) + } + + /// Get the number of samples of an Opus packet. + pub fn get_nb_samples(packet: &[u8], sample_rate: u32) -> Result { + let frames = ffi!(opus_packet_get_nb_samples, + packet.as_ptr(), len(packet), + sample_rate as c_int); + Ok(frames as usize) + } + + /// Get the number of samples per frame from an Opus packet. + pub fn get_samples_per_frame(packet: &[u8], sample_rate: u32) -> Result { + if packet.len() < 1 { + return Err(Error::bad_arg("opus_packet_get_samples_per_frame")) + } + let samples = ffi!(opus_packet_get_samples_per_frame, + packet.as_ptr(), sample_rate as c_int); + Ok(samples as usize) + } + + /// Parse an Opus packet into one or more frames. + pub fn parse(packet: &[u8]) -> Result { + let mut toc: u8 = 0; + let mut frames = [ptr::null(); 48]; + let mut sizes = [0i16; 48]; + let mut payload_offset: i32 = 0; + let num_frames = ffi!(opus_packet_parse, + packet.as_ptr(), len(packet), + &mut toc, frames.as_mut_ptr(), + sizes.as_mut_ptr(), &mut payload_offset); + + let mut frames_vec = Vec::with_capacity(num_frames as usize); + for i in 0..num_frames as usize { + frames_vec.push(unsafe { slice::from_raw_parts(frames[i], sizes[i] as usize) }); + } + + Ok(Packet { + toc: toc, + frames: frames_vec, + payload_offset: payload_offset as usize, + }) + } + + /// A parsed Opus packet, retuned from `parse`. + #[derive(Debug)] + pub struct Packet<'a> { + /// The TOC byte of the packet. + pub toc: u8, + /// The frames contained in the packet. + pub frames: Vec<&'a [u8]>, + /// The offset into the packet at which the payload is located. + pub payload_offset: usize, + } + + /// Pad a given Opus packet to a larger size. + /// + /// The packet will be extended from the first `prev_len` bytes of the + /// buffer into the rest of the available space. + pub fn pad(packet: &mut [u8], prev_len: usize) -> Result { + let result = ffi!(opus_packet_pad, packet.as_mut_ptr(), + check_len(prev_len), len(packet)); + Ok(result as usize) + } + + /// Remove all padding from a given Opus packet and rewrite the TOC sequence + /// to minimize space usage. + pub fn unpad(packet: &mut [u8]) -> Result { + let result = ffi!(opus_packet_unpad, packet.as_mut_ptr(), len(packet)); + Ok(result as usize) + } +} + +// ============================================================================ +// Float Soft Clipping + +/// Soft-clipping to bring a float signal within the [-1,1] range. +#[derive(Debug)] +pub struct SoftClip { + channels: Channels, + memory: [f32; 2], +} + +impl SoftClip { + /// Initialize a new soft-clipping state. + pub fn new(channels: Channels) -> SoftClip { + SoftClip { channels: channels, memory: [0.0; 2] } + } + + /// Apply soft-clipping to a float signal. + pub fn apply(&mut self, signal: &mut [f32]) { + unsafe { ffi::opus_pcm_soft_clip( + signal.as_mut_ptr(), + len(signal) / self.channels as c_int, + self.channels as c_int, + self.memory.as_mut_ptr()) }; + } +} + +// ============================================================================ +// Repacketizer + +/// A repacketizer used to merge together or split apart multiple Opus packets. +#[derive(Debug)] +pub struct Repacketizer { + ptr: *mut ffi::OpusRepacketizer, +} + +impl Repacketizer { + /// Create and initialize a repacketizer. + pub fn new() -> Result { + let ptr = unsafe { ffi::opus_repacketizer_create() }; + if ptr.is_null() { + Err(Error::from_code("opus_repacketizer_create", ffi::OPUS_ALLOC_FAIL)) + } else { + Ok(Repacketizer { ptr: ptr }) + } + } + + /// Shortcut to combine several smaller packets into one larger one. + pub fn combine(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result { + let mut state = self.begin(); + for &packet in input { + state.cat(packet)?; + } + state.out(output) + } + + /// Begin using the repacketizer. + pub fn begin<'rp, 'buf>(&'rp mut self) -> RepacketizerState<'rp, 'buf> { + unsafe { ffi::opus_repacketizer_init(self.ptr); } + RepacketizerState { rp: self, phantom: PhantomData } + } +} + +impl Drop for Repacketizer { + fn drop(&mut self) { + unsafe { ffi::opus_repacketizer_destroy(self.ptr) } + } +} + +// See `unsafe impl Send for Encoder`. +unsafe impl Send for Repacketizer {} + +// To understand why these lifetime bounds are needed, imagine that the +// repacketizer keeps an internal Vec<&'buf [u8]>, which is added to by cat() +// and accessed by get_nb_frames(), out(), and out_range(). To prove that these +// lifetime bounds are correct, a dummy implementation with the same signatures +// but a real Vec<&'buf [u8]> rather than unsafe blocks may be substituted. + +/// An in-progress repacketization. +#[derive(Debug)] +pub struct RepacketizerState<'rp, 'buf> { + rp: &'rp mut Repacketizer, + phantom: PhantomData<&'buf [u8]>, +} + +impl<'rp, 'buf> RepacketizerState<'rp, 'buf> { + /// Add a packet to the current repacketizer state. + pub fn cat(&mut self, packet: &'buf [u8]) -> Result<()> { + ffi!(opus_repacketizer_cat, self.rp.ptr, + packet.as_ptr(), len(packet)); + Ok(()) + } + + /// Add a packet to the current repacketizer state, moving it. + #[inline] + pub fn cat_move<'b2>(self, packet: &'b2 [u8]) -> Result> where 'buf: 'b2 { + let mut shorter = self; + shorter.cat(packet)?; + Ok(shorter) + } + + /// Get the total number of frames contained in packet data submitted so + /// far via `cat`. + pub fn get_nb_frames(&mut self) -> usize { + unsafe { ffi::opus_repacketizer_get_nb_frames(self.rp.ptr) as usize } + } + + /// Construct a new packet from data previously submitted via `cat`. + /// + /// All previously submitted frames are used. + pub fn out(&mut self, buffer: &mut [u8]) -> Result { + let result = ffi!(opus_repacketizer_out, self.rp.ptr, + buffer.as_mut_ptr(), len(buffer)); + Ok(result as usize) + } + + /// Construct a new packet from data previously submitted via `cat`, with + /// a manually specified subrange. + /// + /// The `end` index should not exceed the value of `get_nb_frames()`. + pub fn out_range(&mut self, begin: usize, end: usize, buffer: &mut [u8]) -> Result { + let result = ffi!(opus_repacketizer_out_range, self.rp.ptr, + check_len(begin), check_len(end), + buffer.as_mut_ptr(), len(buffer)); + Ok(result as usize) + } +} + +// ============================================================================ +// TODO: Multistream API + +// ============================================================================ +// Error Handling + +/// Opus error Result alias. +pub type Result = std::result::Result; + +/// An error generated by the Opus library. +#[derive(Debug)] +pub struct Error { + function: &'static str, + code: ErrorCode, +} + +impl Error { + fn bad_arg(what: &'static str) -> Error { + Error { function: what, code: ErrorCode::BadArg } + } + + fn from_code(what: &'static str, code: c_int) -> Error { + Error { function: what, code: ErrorCode::from_int(code) } + } + + /// Get the name of the Opus function from which the error originated. + #[inline] + pub fn function(&self) -> &'static str { self.function } + + /// Get a textual description of the error provided by Opus. + #[inline] + pub fn description(&self) -> &'static str { self.code.description() } + + /// Get the Opus error code of the error. + #[inline] + pub fn code(&self) -> ErrorCode { self.code } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}: {}", self.function, self.description()) + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + self.code.description() + } +} + +fn check_len(val: usize) -> c_int { + let len = val as c_int; + if len as usize != val { + panic!("length out of range: {}", val); + } + len +} + +#[inline] +fn len(slice: &[T]) -> c_int { + check_len(slice.len()) +} diff --git a/libs/magnum-opus/src/opus_ffi.rs b/libs/magnum-opus/src/opus_ffi.rs new file mode 100644 index 00000000000..0953b539d63 --- /dev/null +++ b/libs/magnum-opus/src/opus_ffi.rs @@ -0,0 +1,6 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +include!(concat!(env!("OUT_DIR"), "/opus_ffi.rs")); diff --git a/libs/magnum-opus/tests/compile-fail/repacketize.rs b/libs/magnum-opus/tests/compile-fail/repacketize.rs new file mode 100644 index 00000000000..e8e9c1711dc --- /dev/null +++ b/libs/magnum-opus/tests/compile-fail/repacketize.rs @@ -0,0 +1,10 @@ +extern crate opus; + +fn main() { + let mut rp = opus::Repacketizer::new().unwrap(); + let mut wip = rp.begin().cat_move( + &[1, 2, 3] + //~^ ERROR borrowed value does not live long enough + ).unwrap(); + wip.out(&mut []); +} diff --git a/libs/magnum-opus/tests/fec.rs b/libs/magnum-opus/tests/fec.rs new file mode 100644 index 00000000000..5f660cfa9a4 --- /dev/null +++ b/libs/magnum-opus/tests/fec.rs @@ -0,0 +1,13 @@ +//! Test that supplying empty packets does forward error correction. + +extern crate magnum_opus; +use magnum_opus::*; + +#[test] +fn blah() { + let mut magnum_opus = Decoder::new(48000, Channels::Mono).unwrap(); + + let mut output = vec![0i16; 5760]; + let size = magnum_opus.decode(&[], &mut output[..], true).unwrap(); + assert_eq!(size, 5760); +} \ No newline at end of file diff --git a/libs/magnum-opus/tests/opus-padding.rs b/libs/magnum-opus/tests/opus-padding.rs new file mode 100644 index 00000000000..65dd0a8562e --- /dev/null +++ b/libs/magnum-opus/tests/opus-padding.rs @@ -0,0 +1,29 @@ +// Based on libmagnum_opus/tests/test_magnum_opus_padding.c + +/* Check for overflow in reading the padding length. + * http://lists.xiph.org/pipermail/magnum_opus/2012-November/001834.html + */ + +extern crate magnum_opus; + +#[test] +fn test_overflow() { + const PACKETSIZE: usize = 16909318; + const CHANNELS: magnum_opus::Channels = magnum_opus::Channels::Stereo; + const FRAMESIZE: usize = 5760; + + let mut input = vec![0xff; PACKETSIZE]; + let mut output = vec![0i16; FRAMESIZE * 2]; + + input[0] = 0xff; + input[1] = 0x41; + *input.last_mut().unwrap() = 0x0b; + + let mut decoder = magnum_opus::Decoder::new(48000, CHANNELS).unwrap(); + let result = decoder.decode(&input[..], &mut output[..], false); + drop(decoder); + drop(input); + drop(output); + + assert_eq!(result.unwrap_err().code(), magnum_opus::ErrorCode::InvalidPacket); +} diff --git a/libs/magnum-opus/tests/tests.rs b/libs/magnum-opus/tests/tests.rs new file mode 100644 index 00000000000..4b5783a0102 --- /dev/null +++ b/libs/magnum-opus/tests/tests.rs @@ -0,0 +1,127 @@ +extern crate magnum_opus; + +fn check_ascii(s: &str) -> &str { + for &b in s.as_bytes() { + assert!(b < 0x80, "Non-ASCII character in string"); + assert!(b > 0x00, "NUL in string") + } + std::str::from_utf8(s.as_bytes()).unwrap() +} + +#[test] +fn strings_ascii() { + use magnum_opus::ErrorCode::*; + + println!("\nVersion: {}", check_ascii(magnum_opus::version())); + + let codes = [BadArg, BufferTooSmall, InternalError, InvalidPacket, + Unimplemented, InvalidState, AllocFail, Unknown]; + for &code in codes.iter() { + println!("{:?}: {}", code, check_ascii(code.description())); + } +} + +// 48000Hz * 1 channel * 20 ms / 1000 +const MONO_20MS: usize = 48000 * 1 * 20 / 1000; + +#[test] +fn encode_mono() { + let mut encoder = magnum_opus::Encoder::new(48000, magnum_opus::Channels::Mono, magnum_opus::Application::Audio).unwrap(); + + let mut output = [0; 256]; + let len = encoder.encode(&[0_i16; MONO_20MS], &mut output).unwrap(); + assert_eq!(&output[..len], &[248, 255, 254]); + + let len = encoder.encode(&[0_i16; MONO_20MS], &mut output).unwrap(); + assert_eq!(&output[..len], &[248, 255, 254]); + + let len = encoder.encode(&[1_i16; MONO_20MS], &mut output).unwrap(); + assert!(len > 190 && len < 220); + + let len = encoder.encode(&[0_i16; MONO_20MS], &mut output).unwrap(); + assert!(len > 170 && len < 190); + + let myvec = encoder.encode_vec(&[1_i16; MONO_20MS], output.len()).unwrap(); + assert!(myvec.len() > 120 && myvec.len() < 140); +} + +#[test] +fn encode_stereo() { + let mut encoder = magnum_opus::Encoder::new(48000, magnum_opus::Channels::Stereo, magnum_opus::Application::Audio).unwrap(); + + let mut output = [0; 512]; + let len = encoder.encode(&[0_i16; 2 * MONO_20MS], &mut output).unwrap(); + assert_eq!(&output[..len], &[252, 255, 254]); + + let len = encoder.encode(&[0_i16; 4 * MONO_20MS], &mut output).unwrap(); + assert_eq!(&output[..len], &[253, 255, 254, 255, 254]); + + let len = encoder.encode(&[17_i16; 2 * MONO_20MS], &mut output).unwrap(); + assert!(len > 240); + + let len = encoder.encode(&[0_i16; 2 * MONO_20MS], &mut output).unwrap(); + assert!(len > 240); + + // Very small buffer should still succeed + let len = encoder.encode(&[95_i16; 2 * MONO_20MS], &mut [0; 20]).unwrap(); + assert!(len <= 20); + + let myvec = encoder.encode_vec(&[95_i16; 2 * MONO_20MS], 20).unwrap(); + assert!(myvec.len() <= 20); +} + +#[test] +fn encode_bad_rate() { + match magnum_opus::Encoder::new(48001, magnum_opus::Channels::Mono, magnum_opus::Application::Audio) { + Ok(_) => panic!("Encoder::new did not return BadArg"), + Err(err) => assert_eq!(err.code(), magnum_opus::ErrorCode::BadArg), + } +} + +#[test] +fn encode_bad_buffer() { + let mut encoder = magnum_opus::Encoder::new(48000, magnum_opus::Channels::Stereo, magnum_opus::Application::Audio).unwrap(); + match encoder.encode(&[1_i16; 2 * MONO_20MS], &mut [0; 0]) { + Ok(_) => panic!("encode with 0-length buffer did not return BadArg"), + Err(err) => assert_eq!(err.code(), magnum_opus::ErrorCode::BadArg), + } +} + +#[test] +fn repacketizer() { + let mut rp = magnum_opus::Repacketizer::new().unwrap(); + let mut out = [0; 256]; + + for _ in 0..2 { + let packet1 = [249, 255, 254, 255, 254]; + let packet2 = [248, 255, 254]; + + let mut state = rp.begin(); + state.cat(&packet1).unwrap(); + state.cat(&packet2).unwrap(); + let len = state.out(&mut out).unwrap(); + assert_eq!(&out[..len], &[251, 3, 255, 254, 255, 254, 255, 254]); + } + for _ in 0..2 { + let packet = [248, 255, 254]; + let state = rp.begin().cat_move(&packet).unwrap(); + let packet = [249, 255, 254, 255, 254]; + let state = state.cat_move(&packet).unwrap(); + let len = {state}.out(&mut out).unwrap(); + assert_eq!(&out[..len], &[251, 3, 255, 254, 255, 254, 255, 254]); + } + for _ in 0..2 { + let len = rp.combine(&[ + &[249, 255, 254, 255, 254], + &[248, 255, 254], + ], &mut out).unwrap(); + assert_eq!(&out[..len], &[251, 3, 255, 254, 255, 254, 255, 254]); + } + for _ in 0..2 { + let len = rp.begin() + .cat_move(&[248, 255, 254]).unwrap() + .cat_move(&[248, 71, 71]).unwrap() + .out(&mut out).unwrap(); + assert_eq!(&out[..len], &[249, 255, 254, 71, 71]); + } +} diff --git a/libs/parity-tokio-ipc/.gitignore b/libs/parity-tokio-ipc/.gitignore new file mode 100644 index 00000000000..2da6cdebaac --- /dev/null +++ b/libs/parity-tokio-ipc/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +.idea \ No newline at end of file diff --git a/libs/parity-tokio-ipc/.travis.yml b/libs/parity-tokio-ipc/.travis.yml new file mode 100644 index 00000000000..c0fff4dea39 --- /dev/null +++ b/libs/parity-tokio-ipc/.travis.yml @@ -0,0 +1,20 @@ +language: rust +rust: +- stable +- beta +- nightly +matrix: + allow_failures: + - rust: nightly +after_success: +- |- + [ $TRAVIS_BRANCH = master ] && + [ $TRAVIS_PULL_REQUEST = false ] && + cargo doc --all --no-deps && + echo '' > target/doc/index.html && + pip install --user ghp-import && + /home/travis/.local/bin/ghp-import -n target/doc && + git push -fq https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages +env: + global: + secure: eUHPLjVMSVclRcEgwgybIKZQJTBW8QDjcjgIsEhOHZn7Kpzw0+rwJVoKkvr/uqyJwyY7pHy36mWX31J8YDSbSiM3W8jXeI97sk+FTUkleqUffPzXnbnR1D4kHZlKndFIbcuO5Z+rtVgsAv5E/u1w9XR+mvgK2lfaIEay+26gBl6dl/1TxWvrwDBeMvfq1JGVDQH4Etubncpi3LSWhbRkie1AKnVnsDIY9sUYVKSnIqjxx0qW6Z7EiCdwZ8gf04LNnqyIoKDpyldotL+nJ67ZlVI2O2DrbOOt55nliFHsH4BcWZIZOyIAM4PxIwhDl8g9E55FLkkUX9VUpVtqjTu9RWkVl7rzyrSxLoBUEjguIPrpFWBwLo0FSDvplB2XCXt8x035Io5PEg6m5dVxx5iytTIbI6HQwcA0ESTuDPuAdRJMNvJS/9e2UzPukdYYaaxF6g8wSmiIQjLuZU/nGBdmAl7Uw6cFlQnyLc/GXQg0oZ+B/J8sc4W2C/Z64oB8jK72RLNTKeeWs/XSOt8NxQiNkWeFIhGqiYOPJgjBiTCLSKJPY3CUTiBT8QpAcpj1x1gsWi+5fRoXYxNig/CmeTwZjuxKNxfQIu3J+lJbNdt44x7whnwhZ/AKVuLFPNNiC2OBNpa738UY60VYDoNZyhomWSdBnz3E6i1VtdiSnujFFnc= diff --git a/libs/parity-tokio-ipc/Cargo.toml b/libs/parity-tokio-ipc/Cargo.toml new file mode 100644 index 00000000000..2993686f3f1 --- /dev/null +++ b/libs/parity-tokio-ipc/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "parity-tokio-ipc" +version = "0.7.2" +edition = "2018" +authors = ["NikVolf "] +license = "MIT/Apache-2.0" +readme = "README.md" +repository = "https://github.com/nikvolf/parity-tokio-ipc" +homepage = "https://github.com/nikvolf/parity-tokio-ipc" +description = """ +Interprocess communication library for tokio. +""" + +[dependencies] +futures = "0.3" +log = "0.4" +mio-named-pipes = "0.1" +miow = "0.3" +rand = "0.7" +tokio = { version = "0.2", features = ["io-driver", "io-util", "uds", "stream", "rt-core", "macros", "time"] } +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["winbase", "winnt", "accctrl", "aclapi", "securitybaseapi", "minwinbase", "winbase"] } diff --git a/libs/parity-tokio-ipc/LICENSE-APACHE b/libs/parity-tokio-ipc/LICENSE-APACHE new file mode 100644 index 00000000000..16fe87b06e8 --- /dev/null +++ b/libs/parity-tokio-ipc/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/libs/parity-tokio-ipc/LICENSE-MIT b/libs/parity-tokio-ipc/LICENSE-MIT new file mode 100644 index 00000000000..5dd158b8a3d --- /dev/null +++ b/libs/parity-tokio-ipc/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2017 Nikolay Volf + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/libs/parity-tokio-ipc/README.md b/libs/parity-tokio-ipc/README.md new file mode 100644 index 00000000000..709267939f7 --- /dev/null +++ b/libs/parity-tokio-ipc/README.md @@ -0,0 +1,28 @@ +# parity-tokio-ipc + +[![Build Status](https://travis-ci.org/NikVolf/parity-tokio-ipc.svg?branch=master)](https://travis-ci.org/NikVolf/parity-tokio-ipc) + +[Documentation](https://nikvolf.github.io/parity-tokio-ipc) + +This crate abstracts interprocess transport for UNIX/Windows. On UNIX it utilizes unix sockets (`tokio_uds` crate) and named pipe on windows (experimental `tokio-named-pipes` crate). + +Endpoint is transport-agnostic interface for incoming connections: +```rust + let endpoint = Endpoint::new(endpoint_addr, handle).unwrap(); + endpoint.incoming().for_each(|_| println!("Connection received!")); +``` + +And IpcStream is transport-agnostic io: +```rust + let endpoint = Endpoint::new(endpoint_addr, handle).unwrap(); + endpoint.incoming().for_each(|(ipc_stream: IpcStream, _)| io::write_all(ipc_stream, b"Hello!")); +``` + + +# License + +`parity-tokio-ipc` is primarily distributed under the terms of both the MIT +license and the Apache License (Version 2.0), with portions covered by various +BSD-like licenses. + +See LICENSE-APACHE, and LICENSE-MIT for details. diff --git a/libs/parity-tokio-ipc/appveyor.yml b/libs/parity-tokio-ipc/appveyor.yml new file mode 100644 index 00000000000..0379e2b2079 --- /dev/null +++ b/libs/parity-tokio-ipc/appveyor.yml @@ -0,0 +1,16 @@ +environment: + matrix: + - TARGET: x86_64-pc-windows-msvc + +install: + - curl -sSf -o rustup-init.exe https://win.rustup.rs/ + - rustup-init.exe -y --default-host %TARGET% + - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin;C:\MinGW\bin + + - rustc -vV + - cargo -vV + +build: false + +test_script: + - cargo test diff --git a/libs/parity-tokio-ipc/examples/client.rs b/libs/parity-tokio-ipc/examples/client.rs new file mode 100644 index 00000000000..418461d3474 --- /dev/null +++ b/libs/parity-tokio-ipc/examples/client.rs @@ -0,0 +1,24 @@ +use tokio::{self, prelude::*}; +use parity_tokio_ipc::Endpoint; + +#[tokio::main] +async fn main() { + let path = std::env::args().nth(1).expect("Run it with server path to connect as argument"); + + let mut client = Endpoint::connect(&path).await + .expect("Failed to connect client."); + + loop { + let mut buf = [0u8; 4]; + println!("SEND: PING"); + client.write_all(b"ping").await.expect("Unable to write message to client"); + client.read_exact(&mut buf[..]).await.expect("Unable to read buffer"); + if let Ok("pong") = std::str::from_utf8(&buf[..]) { + println!("RECEIVED: PONG"); + } else { + break; + } + + tokio::time::delay_for(std::time::Duration::from_secs(2)).await; + } +} diff --git a/libs/parity-tokio-ipc/examples/server.rs b/libs/parity-tokio-ipc/examples/server.rs new file mode 100644 index 00000000000..9c53ab3778b --- /dev/null +++ b/libs/parity-tokio-ipc/examples/server.rs @@ -0,0 +1,47 @@ +use futures::StreamExt as _; +use tokio::{ + prelude::*, + self, + io::split, +}; + +use parity_tokio_ipc::{Endpoint, SecurityAttributes}; + +async fn run_server(path: String) { + let mut endpoint = Endpoint::new(path); + endpoint.set_security_attributes(SecurityAttributes::allow_everyone_create().unwrap()); + + let mut incoming = endpoint.incoming().expect("failed to open new socket"); + + while let Some(result) = incoming.next().await + { + match result { + Ok(stream) => { + let (mut reader, mut writer) = split(stream); + + tokio::spawn(async move { + loop { + let mut buf = [0u8; 4]; + let pong_buf = b"pong"; + if let Err(_) = reader.read_exact(&mut buf).await { + println!("Closing socket"); + break; + } + if let Ok("ping") = std::str::from_utf8(&buf[..]) { + println!("RECIEVED: PING"); + writer.write_all(pong_buf).await.expect("unable to write to socket"); + println!("SEND: PONG"); + } + } + }); + } + _ => unreachable!("ideally") + } + }; +} + +#[tokio::main] +async fn main() { + let path = std::env::args().nth(1).expect("Run it with server path as argument"); + run_server(path).await +} \ No newline at end of file diff --git a/libs/parity-tokio-ipc/examples/spam-clients.sh b/libs/parity-tokio-ipc/examples/spam-clients.sh new file mode 100755 index 00000000000..7e8d23e3e06 --- /dev/null +++ b/libs/parity-tokio-ipc/examples/spam-clients.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo "Spawning 100 processes" +for i in {1..100} ; +do + ( cargo run --example client -- /tmp/test.ipc & ) +done \ No newline at end of file diff --git a/libs/parity-tokio-ipc/src/lib.rs b/libs/parity-tokio-ipc/src/lib.rs new file mode 100644 index 00000000000..0627287e7cb --- /dev/null +++ b/libs/parity-tokio-ipc/src/lib.rs @@ -0,0 +1,154 @@ +//! Tokio IPC transport. Under the hood uses Unix Domain Sockets for Linux/Mac +//! and Named Pipes for Windows. + +//#![warn(missing_docs)] +//#![deny(rust_2018_idioms)] + +#[cfg(windows)] +mod win; +#[cfg(not(windows))] +mod unix; + +/// Endpoint for IPC transport +/// +/// # Examples +/// +/// ```ignore +/// use parity_tokio_ipc::{Endpoint, dummy_endpoint}; +/// use futures::{future, Future, Stream, StreamExt}; +/// use tokio::runtime::Runtime; +/// +/// fn main() { +/// let mut runtime = Runtime::new().unwrap(); +/// let mut endpoint = Endpoint::new(dummy_endpoint()); +/// let server = endpoint.incoming() +/// .expect("failed to open up a new pipe/socket") +/// .for_each(|_stream| { +/// println!("Connection received"); +/// futures::future::ready(()) +/// }); +/// runtime.block_on(server) +/// } +///``` +#[cfg(windows)] +pub use win::{SecurityAttributes, Endpoint, Connection, Incoming}; +#[cfg(unix)] +pub use unix::{SecurityAttributes, Endpoint, Connection, Incoming}; + +/// For testing/examples +pub fn dummy_endpoint() -> String { + let num: u64 = rand::Rng::gen(&mut rand::thread_rng()); + if cfg!(windows) { + format!(r"\\.\pipe\my-pipe-{}", num) + } else { + format!(r"/tmp/my-uds-{}", num) + } +} + +#[cfg(test)] +mod tests { + use tokio::prelude::*; + use futures::{channel::oneshot, StreamExt as _, FutureExt as _}; + use std::time::Duration; + use tokio::{ + self, + io::split, + }; + + use super::{dummy_endpoint, Endpoint, SecurityAttributes}; + use std::path::Path; + use futures::future::{Either, select, ready}; + + async fn run_server(path: String) { + let path = path.to_owned(); + let mut endpoint = Endpoint::new(path); + + endpoint.set_security_attributes( + SecurityAttributes::empty() + .set_mode(0o777) + .unwrap() + ); + let mut incoming = endpoint.incoming().expect("failed to open up a new socket"); + + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let (mut reader, mut writer) = split(stream); + let mut buf = [0u8; 5]; + reader.read_exact(&mut buf).await.expect("unable to read from socket"); + writer.write_all(&buf[..]).await.expect("unable to write to socket"); + } + _ => unreachable!("ideally") + } + }; + } + + #[tokio::test] + async fn smoke_test() { + let path = dummy_endpoint(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let server = select(Box::pin(run_server(path.clone())), shutdown_rx) + .then(|either| { + match either { + Either::Right((_, server)) => { + drop(server); + } + _ => unreachable!("also ideally") + }; + ready(()) + }); + tokio::spawn(server); + + tokio::time::delay_for(Duration::from_secs(2)).await; + + println!("Connecting to client 0..."); + let mut client_0 = Endpoint::connect(&path).await + .expect("failed to open client_0"); + tokio::time::delay_for(Duration::from_secs(2)).await; + println!("Connecting to client 1..."); + let mut client_1 = Endpoint::connect(&path).await + .expect("failed to open client_1"); + let msg = b"hello"; + + let mut rx_buf = vec![0u8; msg.len()]; + client_0.write_all(msg).await.expect("Unable to write message to client"); + client_0.read_exact(&mut rx_buf).await.expect("Unable to read message from client"); + + let mut rx_buf2 = vec![0u8; msg.len()]; + client_1.write_all(msg).await.expect("Unable to write message to client"); + client_1.read_exact(&mut rx_buf2).await.expect("Unable to read message from client"); + + assert_eq!(rx_buf, msg); + assert_eq!(rx_buf2, msg); + + // shutdown server + if let Ok(()) = shutdown_tx.send(()) { + // wait one second for the file to be deleted. + tokio::time::delay_for(Duration::from_secs(1)).await; + let path = Path::new(&path); + // assert that it has + assert!(!path.exists()); + } + } + + #[cfg(windows)] + fn create_pipe_with_permissions(attr: SecurityAttributes) -> ::std::io::Result<()> { + let path = dummy_endpoint(); + + let mut endpoint = Endpoint::new(path); + endpoint.set_security_attributes(attr); + endpoint.incoming().map(|_| ()) + } + + #[cfg(windows)] + #[tokio::test] + async fn test_pipe_permissions() { + create_pipe_with_permissions(SecurityAttributes::empty()) + .expect("failed with no attributes"); + create_pipe_with_permissions(SecurityAttributes::allow_everyone_create().unwrap()) + .expect("failed with attributes for creating"); + create_pipe_with_permissions(SecurityAttributes::empty().allow_everyone_connect().unwrap()) + .expect("failed with attributes for connecting"); + } +} diff --git a/libs/parity-tokio-ipc/src/unix.rs b/libs/parity-tokio-ipc/src/unix.rs new file mode 100644 index 00000000000..b4b87399c58 --- /dev/null +++ b/libs/parity-tokio-ipc/src/unix.rs @@ -0,0 +1,163 @@ +use libc::chmod; +use std::ffi::CString; +use std::io::{self, Error}; +use tokio::prelude::*; +use tokio::net::{UnixListener, UnixStream}; +use std::path::Path; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::mem::MaybeUninit; + +/// Socket permissions and ownership on UNIX +pub struct SecurityAttributes { + // read/write permissions for owner, group and others in unix octal. + mode: Option +} + +impl SecurityAttributes { + /// New default security attributes. + pub fn empty() -> Self { + SecurityAttributes { + mode: None + } + } + + /// New security attributes that allow everyone to connect. + pub fn allow_everyone_connect(mut self) -> io::Result { + self.mode = Some(0o777); + Ok(self) + } + + /// Set a custom permission on the socket + pub fn set_mode(mut self, mode: u16) -> io::Result { + self.mode = Some(mode); + Ok(self) + } + + /// New security attributes that allow everyone to create. + pub fn allow_everyone_create() -> io::Result { + Ok(SecurityAttributes { + mode: None + }) + } + + /// called in unix, after server socket has been created + /// will apply security attributes to the socket. + pub(crate) unsafe fn apply_permissions(&self, path: &str) -> io::Result<()> { + let path = CString::new(path.to_owned())?; + if let Some(mode) = self.mode { + if chmod(path.as_ptr(), mode as _) == -1 { + return Err(Error::last_os_error()) + } + } + + Ok(()) + } +} + +/// Endpoint implementation for unix systems +pub struct Endpoint { + path: String, + security_attributes: SecurityAttributes, +} + +pub struct Incoming { + socket: UnixListener, +} + +impl Incoming { + pub async fn next(&mut self) -> Option> { + match self.socket.accept().await { + Ok((stream, _)) => Some(Ok(Connection::wrap(stream))), + Err(err) => Some(Err(err)) + } + } +} + +impl Endpoint { + /// Stream of incoming connections + pub fn incoming(&mut self) -> io::Result { + unsafe { + // the call to bind in `inner()` creates the file + // `apply_permission()` will set the file permissions. + self.security_attributes.apply_permissions(&self.path)?; + }; + let socket = self.inner()?; + Ok(Incoming { socket }) + } + + /// Inner platform-dependant state of the endpoint + fn inner(&self) -> io::Result { + UnixListener::bind(&self.path) + } + + /// Set security attributes for the connection + pub fn set_security_attributes(&mut self, security_attributes: SecurityAttributes) { + self.security_attributes = security_attributes; + } + + /// Make new connection using the provided path and running event pool + pub async fn connect>(path: P) -> io::Result { + Ok(Connection::wrap(UnixStream::connect(path.as_ref()).await?)) + } + + /// Returns the path of the endpoint. + pub fn path(&self) -> &str { + &self.path + } + + /// New IPC endpoint at the given path + pub fn new(path: String) -> Self { + Endpoint { + path, + security_attributes: SecurityAttributes::empty(), + } + } +} + +/// IPC connection. +pub struct Connection { + inner: UnixStream, +} + +impl Connection { + fn wrap(stream: UnixStream) -> Self { + Self { inner: stream } + } +} + +impl AsyncRead for Connection { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [MaybeUninit]) -> bool { + self.inner.prepare_uninitialized_buffer(buf) + } + + fn poll_read( + self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_read(ctx, buf) + } +} + +impl AsyncWrite for Connection { + fn poll_write( + self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_write(ctx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_flush(ctx) + } + + fn poll_shutdown(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_shutdown(ctx) + } +} diff --git a/libs/parity-tokio-ipc/src/win.rs b/libs/parity-tokio-ipc/src/win.rs new file mode 100644 index 00000000000..02af778bd4f --- /dev/null +++ b/libs/parity-tokio-ipc/src/win.rs @@ -0,0 +1,488 @@ +use winapi::shared::winerror::ERROR_SUCCESS; +use winapi::um::accctrl::*; +use winapi::um::aclapi::*; +use winapi::um::minwinbase::{LPTR, PSECURITY_ATTRIBUTES, SECURITY_ATTRIBUTES}; +use winapi::um::securitybaseapi::*; +use winapi::um::winbase::{LocalAlloc, LocalFree}; +use winapi::um::winnt::*; + +use std::io; +use std::marker; +use std::mem; +use std::ptr; +use futures::Stream; +use tokio::prelude::*; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::path::Path; +use std::mem::MaybeUninit; +use tokio::io::PollEvented; + +type NamedPipe = PollEvented; + +const PIPE_AVAILABILITY_TIMEOUT: u64 = 5000; + +/// Endpoint implementation for windows +pub struct Endpoint { + path: String, + security_attributes: SecurityAttributes, +} + +impl Endpoint { + /// Stream of incoming connections + pub fn incoming(mut self) -> io::Result { + let pipe = self.inner()?; + Ok(Incoming { + path: self.path.clone(), + inner: NamedPipeSupport { + path: self.path, + pipe, + security_attributes: self.security_attributes, + }, + }) + } + + /// Inner platform-dependant state of the endpoint + fn inner(&mut self) -> io::Result { + use miow::pipe::NamedPipeBuilder; + use std::os::windows::io::*; + + let raw_handle = unsafe { + NamedPipeBuilder::new(&self.path) + .first(true) + .inbound(true) + .outbound(true) + .out_buffer_size(65536) + .in_buffer_size(65536) + .with_security_attributes(self.security_attributes.as_ptr())? + .into_raw_handle() + }; + + let mio_pipe = unsafe { mio_named_pipes::NamedPipe::from_raw_handle(raw_handle) }; + NamedPipe::new(mio_pipe) + } + + /// Set security attributes for the connection + pub fn set_security_attributes(&mut self, security_attributes: SecurityAttributes) { + self.security_attributes = security_attributes; + } + + /// Returns the path of the endpoint. + pub fn path(&self) -> &str { + &self.path + } + + /// Make new connection using the provided path and running event pool. + pub async fn connect>(path: P) -> io::Result { + Ok(Connection::wrap(Self::connect_inner(path.as_ref())?)) + } + + fn connect_inner(path: &Path) -> io::Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use std::os::windows::io::{FromRawHandle, IntoRawHandle}; + use winapi::um::winbase::FILE_FLAG_OVERLAPPED; + + // Wait for the pipe to become available or fail after 5 seconds. + miow::pipe::NamedPipe::wait( + path, + Some(std::time::Duration::from_millis(PIPE_AVAILABILITY_TIMEOUT)), + )?; + let file = OpenOptions::new() + .read(true) + .write(true) + .custom_flags(FILE_FLAG_OVERLAPPED) + .open(path)?; + let mio_pipe = + unsafe { mio_named_pipes::NamedPipe::from_raw_handle(file.into_raw_handle()) }; + let pipe = NamedPipe::new(mio_pipe)?; + Ok(pipe) + } + + /// New IPC endpoint at the given path + pub fn new(path: String) -> Self { + Endpoint { + path, + security_attributes: SecurityAttributes::empty(), + } + } +} + +struct NamedPipeSupport { + path: String, + pipe: NamedPipe, + security_attributes: SecurityAttributes, +} + +impl NamedPipeSupport { + fn replacement_pipe(&mut self) -> io::Result { + use miow::pipe::NamedPipeBuilder; + use std::os::windows::io::*; + + let raw_handle = unsafe { + NamedPipeBuilder::new(&self.path) + .first(false) + .inbound(true) + .outbound(true) + .out_buffer_size(65536) + .in_buffer_size(65536) + .with_security_attributes(self.security_attributes.as_ptr())? + .into_raw_handle() + }; + + let mio_pipe = unsafe { mio_named_pipes::NamedPipe::from_raw_handle(raw_handle) }; + NamedPipe::new(mio_pipe) + } +} + +/// Stream of incoming connections +pub struct Incoming { + #[allow(dead_code)] + path: String, + inner: NamedPipeSupport, +} + +impl Stream for Incoming { + type Item = tokio::io::Result; + + fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + match self.inner.pipe.get_ref().connect() { + Ok(()) => { + log::trace!("Incoming connection polled successfully"); + let new_listener = self.inner.replacement_pipe()?; + Poll::Ready( + Some(Ok(Connection::wrap(std::mem::replace(&mut self.inner.pipe, new_listener)))) + ) + } + Err(e) => { + if e.kind() == io::ErrorKind::WouldBlock { + self.inner.pipe.clear_write_ready(ctx); + Poll::Pending + } else { + Poll::Ready(Some(Err(e))) + } + } + } + } +} + +/// IPC connection. +pub struct Connection { + inner: NamedPipe, +} + +impl Connection { + pub fn wrap(pipe: NamedPipe) -> Self { + Self { inner: pipe } + } +} + +impl AsyncRead for Connection { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [MaybeUninit]) -> bool { + self.inner.prepare_uninitialized_buffer(buf) + } + + fn poll_read( + self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_read(ctx, buf) + } +} + +impl AsyncWrite for Connection { + fn poll_write( + self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_write(ctx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_flush(ctx) + } + + fn poll_shutdown(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + let this = Pin::into_inner(self); + Pin::new(&mut this.inner).poll_shutdown(ctx) + } +} + +/// Security attributes. +pub struct SecurityAttributes { + attributes: Option, +} + +impl SecurityAttributes { + /// New default security attributes. + pub fn empty() -> SecurityAttributes { + SecurityAttributes { attributes: None } + } + + /// New default security attributes that allow everyone to connect. + pub fn allow_everyone_connect(&self) -> io::Result { + let attributes = Some(InnerAttributes::allow_everyone( + GENERIC_READ | FILE_WRITE_DATA, + )?); + Ok(SecurityAttributes { attributes }) + } + + /// Set a custom permission on the socket + pub fn set_mode(self, _mode: u32) -> io::Result { + // for now, does nothing. + Ok(self) + } + + /// New default security attributes that allow everyone to create. + pub fn allow_everyone_create() -> io::Result { + let attributes = Some(InnerAttributes::allow_everyone( + GENERIC_READ | GENERIC_WRITE, + )?); + Ok(SecurityAttributes { attributes }) + } + + /// Return raw handle of security attributes. + pub(crate) unsafe fn as_ptr(&mut self) -> PSECURITY_ATTRIBUTES { + match self.attributes.as_mut() { + Some(attributes) => attributes.as_ptr(), + None => ptr::null_mut(), + } + } +} + +unsafe impl Send for SecurityAttributes {} + +struct Sid { + sid_ptr: PSID, +} + +impl Sid { + fn everyone_sid() -> io::Result { + let mut sid_ptr = ptr::null_mut(); + let result = unsafe { + AllocateAndInitializeSid( + SECURITY_WORLD_SID_AUTHORITY.as_mut_ptr() as *mut _, + 1, + SECURITY_WORLD_RID, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + &mut sid_ptr, + ) + }; + if result == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(Sid { sid_ptr }) + } + } + + // Unsafe - the returned pointer is only valid for the lifetime of self. + unsafe fn as_ptr(&self) -> PSID { + self.sid_ptr + } +} + +impl Drop for Sid { + fn drop(&mut self) { + if !self.sid_ptr.is_null() { + unsafe { + FreeSid(self.sid_ptr); + } + } + } +} + +struct AceWithSid<'a> { + explicit_access: EXPLICIT_ACCESS_W, + _marker: marker::PhantomData<&'a Sid>, +} + +impl<'a> AceWithSid<'a> { + fn new(sid: &'a Sid, trustee_type: u32) -> AceWithSid<'a> { + let mut explicit_access = unsafe { mem::zeroed::() }; + explicit_access.Trustee.TrusteeForm = TRUSTEE_IS_SID; + explicit_access.Trustee.TrusteeType = trustee_type; + explicit_access.Trustee.ptstrName = unsafe { sid.as_ptr() as *mut _ }; + + AceWithSid { + explicit_access, + _marker: marker::PhantomData, + } + } + + fn set_access_mode(&mut self, access_mode: u32) -> &mut Self { + self.explicit_access.grfAccessMode = access_mode; + self + } + + fn set_access_permissions(&mut self, access_permissions: u32) -> &mut Self { + self.explicit_access.grfAccessPermissions = access_permissions; + self + } + + fn allow_inheritance(&mut self, inheritance_flags: u32) -> &mut Self { + self.explicit_access.grfInheritance = inheritance_flags; + self + } +} + +struct Acl { + acl_ptr: PACL, +} + +impl Acl { + fn empty() -> io::Result { + Self::new(&mut []) + } + + fn new(entries: &mut [AceWithSid<'_>]) -> io::Result { + let mut acl_ptr = ptr::null_mut(); + let result = unsafe { + SetEntriesInAclW( + entries.len() as u32, + entries.as_mut_ptr() as *mut _, + ptr::null_mut(), + &mut acl_ptr, + ) + }; + + if result != ERROR_SUCCESS { + return Err(io::Error::from_raw_os_error(result as i32)); + } + + Ok(Acl { acl_ptr }) + } + + unsafe fn as_ptr(&self) -> PACL { + self.acl_ptr + } +} + +impl Drop for Acl { + fn drop(&mut self) { + if !self.acl_ptr.is_null() { + unsafe { LocalFree(self.acl_ptr as *mut _) }; + } + } +} + +struct SecurityDescriptor { + descriptor_ptr: PSECURITY_DESCRIPTOR, +} + +impl SecurityDescriptor { + fn new() -> io::Result { + let descriptor_ptr = unsafe { LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH) }; + if descriptor_ptr.is_null() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Failed to allocate security descriptor", + )); + } + + if unsafe { + InitializeSecurityDescriptor(descriptor_ptr, SECURITY_DESCRIPTOR_REVISION) == 0 + } { + return Err(io::Error::last_os_error()); + }; + + Ok(SecurityDescriptor { descriptor_ptr }) + } + + fn set_dacl(&mut self, acl: &Acl) -> io::Result<()> { + if unsafe { + SetSecurityDescriptorDacl(self.descriptor_ptr, true as i32, acl.as_ptr(), false as i32) + == 0 + } { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + + unsafe fn as_ptr(&self) -> PSECURITY_DESCRIPTOR { + self.descriptor_ptr + } +} + +impl Drop for SecurityDescriptor { + fn drop(&mut self) { + if !self.descriptor_ptr.is_null() { + unsafe { LocalFree(self.descriptor_ptr) }; + self.descriptor_ptr = ptr::null_mut(); + } + } +} + +struct InnerAttributes { + descriptor: SecurityDescriptor, + acl: Acl, + attrs: SECURITY_ATTRIBUTES, +} + +impl InnerAttributes { + fn empty() -> io::Result { + let descriptor = SecurityDescriptor::new()?; + let mut attrs = unsafe { mem::zeroed::() }; + attrs.nLength = mem::size_of::() as u32; + attrs.lpSecurityDescriptor = unsafe { descriptor.as_ptr() }; + attrs.bInheritHandle = false as i32; + + let acl = Acl::empty().expect("this should never fail"); + + Ok(InnerAttributes { + acl, + descriptor, + attrs, + }) + } + + fn allow_everyone(permissions: u32) -> io::Result { + let mut attributes = Self::empty()?; + let sid = Sid::everyone_sid()?; + + let mut everyone_ace = AceWithSid::new(&sid, TRUSTEE_IS_WELL_KNOWN_GROUP); + everyone_ace + .set_access_mode(SET_ACCESS) + .set_access_permissions(permissions) + .allow_inheritance(false as u32); + + let mut entries = vec![everyone_ace]; + attributes.acl = Acl::new(&mut entries)?; + attributes.descriptor.set_dacl(&attributes.acl)?; + + Ok(attributes) + } + + unsafe fn as_ptr(&mut self) -> PSECURITY_ATTRIBUTES { + &mut self.attrs as *mut _ + } +} + +#[cfg(test)] +mod test { + use super::SecurityAttributes; + + #[test] + fn test_allow_everyone_everything() { + SecurityAttributes::allow_everyone_create() + .expect("failed to create security attributes that allow everyone to create a pipe"); + } + + #[test] + fn test_allow_eveyone_read_write() { + SecurityAttributes::empty() + .allow_everyone_connect() + .expect("failed to create security attributes that allow everyone to read and write to/from a pipe"); + } + +} diff --git a/libs/pulsectl/.gitignore b/libs/pulsectl/.gitignore new file mode 100644 index 00000000000..12dd8a67726 --- /dev/null +++ b/libs/pulsectl/.gitignore @@ -0,0 +1,4 @@ +**/target +/target +**/*.rs.bk +.idea/ diff --git a/libs/pulsectl/Cargo.lock b/libs/pulsectl/Cargo.lock new file mode 100644 index 00000000000..4be1700153b --- /dev/null +++ b/libs/pulsectl/Cargo.lock @@ -0,0 +1,129 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "libc" +version = "0.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" + +[[package]] +name = "libpulse-binding" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe925a4d3a96961316c9c1488f97a95938a6093f0d4691eec888776057ce965e" +dependencies = [ + "libc", + "libpulse-sys", + "num-derive", + "num-traits", + "winapi", +] + +[[package]] +name = "libpulse-sys" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9073c83dda6aff9b611dc368e8db6e0aa29027546d8800a18b4417e182b4d5" +dependencies = [ + "libc", + "num-derive", + "num-traits", + "pkg-config", + "winapi", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rust-pulsectl" +version = "0.2.9" +dependencies = [ + "libpulse-binding", +] + +[[package]] +name = "syn" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4f34193997d92804d359ed09953e25d5138df6bcc055a71bf68ee89fdf9223" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/libs/pulsectl/Cargo.toml b/libs/pulsectl/Cargo.toml new file mode 100644 index 00000000000..e309ca81cdf --- /dev/null +++ b/libs/pulsectl/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rust-pulsectl" +version = "0.2.10" +authors = ["Kristopher Ruzic "] +edition = "2018" +license = "GPL-3.0+" +description = "A higher level API for libpulse_binding" +readme = "README.md" +keywords = ["pulse", "pulseaudio", "binding", "audio", "api"] +categories = ["api-bindings", "multimedia::audio"] +homepage = "https://github.com/krruzic/pulsectl" +repository = "https://github.com/krruzic/pulsectl" + +[lib] +name = "pulsectl" +path = "src/lib.rs" + +[dependencies] +libpulse-binding = "2.21" diff --git a/libs/pulsectl/LICENSE.md b/libs/pulsectl/LICENSE.md new file mode 100644 index 00000000000..c8a32896510 --- /dev/null +++ b/libs/pulsectl/LICENSE.md @@ -0,0 +1,13 @@ +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + diff --git a/libs/pulsectl/README.md b/libs/pulsectl/README.md new file mode 100644 index 00000000000..ca8703b1e33 --- /dev/null +++ b/libs/pulsectl/README.md @@ -0,0 +1,43 @@ +Rust PulsecAudio API +==================== + +`pulsectl-rust` is a API wrapper for `libpulse_binding` to make pulseaudio application development easier. +This is a wrapper around the introspector, and thus this library is only capable of modifying PulseAudio data (changing volume, routing applications and muting right now). + +### Usage + +Add this to your `Cargo.toml`: +```toml +[dependencies] +rust-pulsectl = "0.2.6" +``` + +Then, connect to PulseAudio by creating a `SinkController` for audio playback devices and apps or a `SourceController` for audio recording devices and apps. + +```rust +// Simple application that lists all playback devices and their status +// See examples/change_device_vol.rs for a more complete example +extern crate pulsectl; + +use std::io; + +use pulsectl::controllers::SinkController; +use pulsectl::controllers::DeviceControl; +fn main() { + // create handler that calls functions on playback devices and apps + let mut handler = SinkController::create(); + let devices = handler + .list_devices() + .expect("Could not get list of playback devices"); + println!("Playback Devices"); + for dev in devices.clone() { + println!( + "[{}] {}, [Volume: {}]", + dev.index, + dev.description.as_ref().unwrap(), + dev.volume.print() + ); + } +} +``` + diff --git a/libs/pulsectl/examples/change_device_vol.rs b/libs/pulsectl/examples/change_device_vol.rs new file mode 100644 index 00000000000..49bb3eac903 --- /dev/null +++ b/libs/pulsectl/examples/change_device_vol.rs @@ -0,0 +1,34 @@ +extern crate pulsectl; + +use std::io; + +use pulsectl::controllers::DeviceControl; +use pulsectl::controllers::SinkController; + +fn main() { + // create handler that calls functions on playback devices and apps + let mut handler = SinkController::create().unwrap(); + let devices = handler + .list_devices() + .expect("Could not get list of playback devices"); + + println!("Playback Devices"); + for dev in devices.clone() { + println!( + "[{}] {}, [Volume: {}]", + dev.index, + dev.description.as_ref().unwrap(), + dev.volume.print() + ); + } + let mut selection = String::new(); + + io::stdin() + .read_line(&mut selection) + .expect("error: unable to read user input"); + for dev in devices.clone() { + if let true = selection.trim() == dev.index.to_string() { + handler.increase_device_volume_by_percent(dev.index, 0.05); + } + } +} diff --git a/libs/pulsectl/src/controllers/errors.rs b/libs/pulsectl/src/controllers/errors.rs new file mode 100644 index 00000000000..f4e50292e2c --- /dev/null +++ b/libs/pulsectl/src/controllers/errors.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use crate::PulseCtlError; + +/// if the error occurs within the Mainloop, we bubble up the error with +/// this conversion +impl From for ControllerError { + fn from(error: super::errors::PulseCtlError) -> Self { + ControllerError { + error: ControllerErrorType::PulseCtlError, + message: format!("{:?}", error), + } + } +} + +impl fmt::Debug for ControllerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut error_string = String::new(); + match self.error { + ControllerErrorType::PulseCtlError => { + error_string.push_str("PulseCtlError"); + } + ControllerErrorType::GetInfoError => { + error_string.push_str("GetInfoError"); + } + } + write!(f, "[{}]: {}", error_string, self.message) + } +} + +pub(crate) enum ControllerErrorType { + PulseCtlError, + GetInfoError, +} + +/// Error thrown while fetching data from pulseaudio, +/// has two variants: PulseCtlError for when PulseAudio returns an error code +/// and GetInfoError when a request for data fails for whatever reason +pub struct ControllerError { + error: ControllerErrorType, + message: String, +} + +impl ControllerError { + pub(crate) fn new(err: ControllerErrorType, msg: &str) -> Self { + ControllerError { + error: err, + message: msg.to_string(), + } + } +} diff --git a/libs/pulsectl/src/controllers/mod.rs b/libs/pulsectl/src/controllers/mod.rs new file mode 100644 index 00000000000..2fb1aa92ae3 --- /dev/null +++ b/libs/pulsectl/src/controllers/mod.rs @@ -0,0 +1,595 @@ +/// Source = microphone etc. something that takes in audio +/// Source Output = application consuming that audio +/// +/// Sink = headphones etc. something that plays out audio +/// Sink Input = application producing that audio +/// When you create a `SinkController`, you are working with audio playback devices and applications +/// if you want to manipulate recording devices such as microphone volume, +/// you'll need to use a `SourceController`. Both of these implement the same api, defined by +/// the traits DeviceControl and AppControl +use std::cell::RefCell; +use std::clone::Clone; +use std::rc::Rc; + +use pulse::{ + callbacks::ListResult, + context::introspect, + volume::{ChannelVolumes, Volume}, +}; + +use errors::{ControllerError, ControllerErrorType::*}; +use types::{ApplicationInfo, DeviceInfo, ServerInfo}; + +use crate::Handler; + +pub(crate) mod errors; +pub mod types; + +pub trait DeviceControl { + fn get_default_device(&mut self) -> Result; + fn set_default_device(&mut self, name: &str) -> Result; + + fn list_devices(&mut self) -> Result, ControllerError>; + fn get_device_by_index(&mut self, index: u32) -> Result; + fn get_device_by_name(&mut self, name: &str) -> Result; + fn set_device_volume_by_index(&mut self, index: u32, volume: &ChannelVolumes); + fn set_device_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes); + fn increase_device_volume_by_percent(&mut self, index: u32, delta: f64); + fn decrease_device_volume_by_percent(&mut self, index: u32, delta: f64); +} + +pub trait AppControl { + fn list_applications(&mut self) -> Result, ControllerError>; + + fn get_app_by_index(&mut self, index: u32) -> Result; + fn increase_app_volume_by_percent(&mut self, index: u32, delta: f64); + fn decrease_app_volume_by_percent(&mut self, index: u32, delta: f64); + + fn move_app_by_index( + &mut self, + stream_index: u32, + device_index: u32, + ) -> Result; + fn move_app_by_name( + &mut self, + stream_index: u32, + device_name: &str, + ) -> Result; + fn set_app_mute(&mut self, index: u32, mute: bool) -> Result; +} + +fn volume_from_percent(volume: f64) -> f64 { + (volume * 100.0) * (f64::from(pulse::volume::VOLUME_NORM.0) / 100.0) +} + +pub struct SinkController { + pub handler: Handler, +} + +impl SinkController { + pub fn create() -> Result { + let handler = Handler::connect("SinkController")?; + Ok(SinkController { handler }) + } + + pub fn get_server_info(&mut self) -> Result { + let server = Rc::new(RefCell::new(Some(None))); + let server_ref = server.clone(); + + let op = self.handler.introspect.get_server_info(move |res| { + server_ref + .borrow_mut() + .as_mut() + .unwrap() + .replace(res.into()); + }); + self.handler.wait_for_operation(op)?; + let mut result = server.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting information about the server", + )) + } +} + +impl DeviceControl for SinkController { + fn get_default_device(&mut self) -> Result { + let server_info = self.get_server_info(); + match server_info { + Ok(info) => self.get_device_by_name(info.default_sink_name.unwrap().as_ref()), + Err(e) => Err(e), + } + } + fn set_default_device(&mut self, name: &str) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + + let op = self + .handler + .context + .borrow_mut() + .set_default_sink(name, move |res| success_ref.borrow_mut().clone_from(&res)); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } + + fn list_devices(&mut self) -> Result, ControllerError> { + let list = Rc::new(RefCell::new(Some(Vec::new()))); + let list_ref = list.clone(); + + let op = self.handler.introspect.get_sink_info_list( + move |sink_list: ListResult<&introspect::SinkInfo>| { + if let ListResult::Item(item) = sink_list { + list_ref.borrow_mut().as_mut().unwrap().push(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = list.borrow_mut(); + result.take().ok_or(ControllerError::new( + GetInfoError, + "Error getting device list", + )) + } + fn get_device_by_index(&mut self, index: u32) -> Result { + let device = Rc::new(RefCell::new(Some(None))); + let dev_ref = device.clone(); + let op = self.handler.introspect.get_sink_info_by_index( + index, + move |sink_list: ListResult<&introspect::SinkInfo>| { + if let ListResult::Item(item) = sink_list { + dev_ref.borrow_mut().as_mut().unwrap().replace(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = device.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting requested device", + )) + } + fn get_device_by_name(&mut self, name: &str) -> Result { + let device = Rc::new(RefCell::new(Some(None))); + let dev_ref = device.clone(); + let op = self.handler.introspect.get_sink_info_by_name( + name, + move |sink_list: ListResult<&introspect::SinkInfo>| { + if let ListResult::Item(item) = sink_list { + dev_ref.borrow_mut().as_mut().unwrap().replace(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = device.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting requested device", + )) + } + + fn set_device_volume_by_index(&mut self, index: u32, volume: &ChannelVolumes) { + let op = self + .handler + .introspect + .set_sink_volume_by_index(index, volume, None); + self.handler.wait_for_operation(op).ok(); + } + fn set_device_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes) { + let op = self + .handler + .introspect + .set_sink_volume_by_name(name, volume, None); + self.handler.wait_for_operation(op).ok(); + } + fn increase_device_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut dev_ref) = self.get_device_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = dev_ref.volume.increase(new_vol) { + let op = self + .handler + .introspect + .set_sink_volume_by_index(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } + fn decrease_device_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut dev_ref) = self.get_device_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = dev_ref.volume.decrease(new_vol) { + let op = self + .handler + .introspect + .set_sink_volume_by_index(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } +} + +impl AppControl for SinkController { + fn list_applications(&mut self) -> Result, ControllerError> { + let list = Rc::new(RefCell::new(Some(Vec::new()))); + let list_ref = list.clone(); + + let op = self.handler.introspect.get_sink_input_info_list( + move |sink_list: ListResult<&introspect::SinkInputInfo>| { + if let ListResult::Item(item) = sink_list { + list_ref.borrow_mut().as_mut().unwrap().push(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = list.borrow_mut(); + result.take().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } + + fn get_app_by_index(&mut self, index: u32) -> Result { + let app = Rc::new(RefCell::new(Some(None))); + let app_ref = app.clone(); + let op = self.handler.introspect.get_sink_input_info( + index, + move |sink_list: ListResult<&introspect::SinkInputInfo>| { + if let ListResult::Item(item) = sink_list { + app_ref.borrow_mut().as_mut().unwrap().replace(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = app.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting requested app", + )) + } + + fn increase_app_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut app_ref) = self.get_app_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = app_ref.volume.increase(new_vol) { + let op = self + .handler + .introspect + .set_sink_input_volume(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } + + fn decrease_app_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut app_ref) = self.get_app_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = app_ref.volume.decrease(new_vol) { + let op = self + .handler + .introspect + .set_sink_input_volume(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } + + fn move_app_by_index( + &mut self, + stream_index: u32, + device_index: u32, + ) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + let op = self.handler.introspect.move_sink_input_by_index( + stream_index, + device_index, + Some(Box::new(move |res| { + success_ref.borrow_mut().clone_from(&res) + })), + ); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } + + fn move_app_by_name( + &mut self, + stream_index: u32, + device_name: &str, + ) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + let op = self.handler.introspect.move_sink_input_by_name( + stream_index, + device_name, + Some(Box::new(move |res| { + success_ref.borrow_mut().clone_from(&res) + })), + ); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } + + fn set_app_mute(&mut self, index: u32, mute: bool) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + let op = self.handler.introspect.set_sink_input_mute( + index, + mute, + Some(Box::new(move |res| { + success_ref.borrow_mut().clone_from(&res) + })), + ); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } +} + +pub struct SourceController { + pub handler: Handler, +} + +impl SourceController { + pub fn create() -> Result { + let handler = Handler::connect("SourceController")?; + Ok(SourceController { handler }) + } + + pub fn get_server_info(&mut self) -> Result { + let server = Rc::new(RefCell::new(Some(None))); + let server_ref = server.clone(); + + let op = self.handler.introspect.get_server_info(move |res| { + server_ref + .borrow_mut() + .as_mut() + .unwrap() + .replace(res.into()); + }); + self.handler.wait_for_operation(op)?; + let mut result = server.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } +} + +impl DeviceControl for SourceController { + fn get_default_device(&mut self) -> Result { + let server_info = self.get_server_info(); + match server_info { + Ok(info) => self.get_device_by_name(info.default_sink_name.unwrap().as_ref()), + Err(e) => Err(e), + } + } + fn set_default_device(&mut self, name: &str) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + + let op = self + .handler + .context + .borrow_mut() + .set_default_source(name, move |res| success_ref.borrow_mut().clone_from(&res)); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } + + fn list_devices(&mut self) -> Result, ControllerError> { + let list = Rc::new(RefCell::new(Some(Vec::new()))); + let list_ref = list.clone(); + + let op = self.handler.introspect.get_source_info_list( + move |sink_list: ListResult<&introspect::SourceInfo>| { + if let ListResult::Item(item) = sink_list { + list_ref.borrow_mut().as_mut().unwrap().push(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = list.borrow_mut(); + result.take().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } + fn get_device_by_index(&mut self, index: u32) -> Result { + let device = Rc::new(RefCell::new(Some(None))); + let dev_ref = device.clone(); + let op = self.handler.introspect.get_source_info_by_index( + index, + move |sink_list: ListResult<&introspect::SourceInfo>| { + if let ListResult::Item(item) = sink_list { + dev_ref.borrow_mut().as_mut().unwrap().replace(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = device.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } + fn get_device_by_name(&mut self, name: &str) -> Result { + let device = Rc::new(RefCell::new(Some(None))); + let dev_ref = device.clone(); + let op = self.handler.introspect.get_source_info_by_name( + name, + move |sink_list: ListResult<&introspect::SourceInfo>| { + if let ListResult::Item(item) = sink_list { + dev_ref.borrow_mut().as_mut().unwrap().replace(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = device.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } + + fn set_device_volume_by_index(&mut self, index: u32, volume: &ChannelVolumes) { + let op = self + .handler + .introspect + .set_source_volume_by_index(index, volume, None); + self.handler.wait_for_operation(op).ok(); + } + fn set_device_volume_by_name(&mut self, name: &str, volume: &ChannelVolumes) { + let op = self + .handler + .introspect + .set_source_volume_by_name(name, volume, None); + self.handler.wait_for_operation(op).ok(); + } + fn increase_device_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut dev_ref) = self.get_device_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = dev_ref.volume.increase(new_vol) { + let op = self + .handler + .introspect + .set_source_volume_by_index(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } + fn decrease_device_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut dev_ref) = self.get_device_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = dev_ref.volume.decrease(new_vol) { + let op = self + .handler + .introspect + .set_source_volume_by_index(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } +} + +impl AppControl for SourceController { + fn list_applications(&mut self) -> Result, ControllerError> { + let list = Rc::new(RefCell::new(Some(Vec::new()))); + let list_ref = list.clone(); + + let op = self.handler.introspect.get_source_output_info_list( + move |sink_list: ListResult<&introspect::SourceOutputInfo>| { + if let ListResult::Item(item) = sink_list { + list_ref.borrow_mut().as_mut().unwrap().push(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = list.borrow_mut(); + result.take().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } + + fn get_app_by_index(&mut self, index: u32) -> Result { + let app = Rc::new(RefCell::new(Some(None))); + let app_ref = app.clone(); + let op = self.handler.introspect.get_source_output_info( + index, + move |sink_list: ListResult<&introspect::SourceOutputInfo>| { + if let ListResult::Item(item) = sink_list { + app_ref.borrow_mut().as_mut().unwrap().replace(item.into()); + } + }, + ); + self.handler.wait_for_operation(op)?; + let mut result = app.borrow_mut(); + result.take().unwrap().ok_or(ControllerError::new( + GetInfoError, + "Error getting application list", + )) + } + + fn increase_app_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut app_ref) = self.get_app_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = app_ref.volume.increase(new_vol) { + let op = self + .handler + .introspect + .set_source_output_volume(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } + + fn decrease_app_volume_by_percent(&mut self, index: u32, delta: f64) { + if let Ok(mut app_ref) = self.get_app_by_index(index) { + let new_vol = Volume::from(Volume(volume_from_percent(delta) as u32)); + if let Some(volumes) = app_ref.volume.decrease(new_vol) { + let op = self + .handler + .introspect + .set_source_output_volume(index, &volumes, None); + self.handler.wait_for_operation(op).ok(); + } + } + } + + fn move_app_by_index( + &mut self, + stream_index: u32, + device_index: u32, + ) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + let op = self.handler.introspect.move_source_output_by_index( + stream_index, + device_index, + Some(Box::new(move |res| { + success_ref.borrow_mut().clone_from(&res) + })), + ); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } + + fn move_app_by_name( + &mut self, + stream_index: u32, + device_name: &str, + ) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + let op = self.handler.introspect.move_source_output_by_name( + stream_index, + device_name, + Some(Box::new(move |res| { + success_ref.borrow_mut().clone_from(&res) + })), + ); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } + + fn set_app_mute(&mut self, index: u32, mute: bool) -> Result { + let success = Rc::new(RefCell::new(false)); + let success_ref = success.clone(); + let op = self.handler.introspect.set_source_mute_by_index( + index, + mute, + Some(Box::new(move |res| { + success_ref.borrow_mut().clone_from(&res) + })), + ); + self.handler.wait_for_operation(op)?; + let result = success.borrow_mut().clone(); + Ok(result) + } +} diff --git a/libs/pulsectl/src/controllers/types.rs b/libs/pulsectl/src/controllers/types.rs new file mode 100644 index 00000000000..c61ad2f445a --- /dev/null +++ b/libs/pulsectl/src/controllers/types.rs @@ -0,0 +1,354 @@ +use pulse::{ + channelmap, + context::introspect, + def, + def::PortAvailable, + format, + proplist::Proplist, + sample, + time::MicroSeconds, + volume::{ChannelVolumes, Volume}, +}; + +/// These structs are direct representations of what libpulse_binding gives +/// created to be copyable / cloneable for use in and out of callbacks + +/// This is a wrapper around SinkPortInfo and SourcePortInfo as they have the same members +#[derive(Clone)] +pub struct DevicePortInfo { + /// Name of the sink. + pub name: Option, + /// Description of this sink. + pub description: Option, + /// The higher this value is, the more useful this port is as a default. + pub priority: u32, + /// A flag indicating availability status of this port. + pub available: PortAvailable, +} + +impl<'a> From<&'a Box>> for DevicePortInfo { + fn from(item: &'a Box>) -> Self { + DevicePortInfo { + name: item.name.as_ref().map(|cow| cow.to_string()), + description: item.description.as_ref().map(|cow| cow.to_string()), + priority: item.priority, + available: item.available, + } + } +} + +impl<'a> From<&'a introspect::SinkPortInfo<'a>> for DevicePortInfo { + fn from(item: &'a introspect::SinkPortInfo<'a>) -> Self { + DevicePortInfo { + name: item.name.as_ref().map(|cow| cow.to_string()), + description: item.description.as_ref().map(|cow| cow.to_string()), + priority: item.priority, + available: item.available, + } + } +} + +impl<'a> From<&'a Box>> for DevicePortInfo { + fn from(item: &'a Box>) -> Self { + DevicePortInfo { + name: item.name.as_ref().map(|cow| cow.to_string()), + description: item.description.as_ref().map(|cow| cow.to_string()), + priority: item.priority, + available: item.available, + } + } +} + +impl<'a> From<&'a introspect::SourcePortInfo<'a>> for DevicePortInfo { + fn from(item: &'a introspect::SourcePortInfo<'a>) -> Self { + DevicePortInfo { + name: item.name.as_ref().map(|cow| cow.to_string()), + description: item.description.as_ref().map(|cow| cow.to_string()), + priority: item.priority, + available: item.available, + } + } +} + +/// This is a wrapper around SinkState and SourceState as they have the same values +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DevState { + /// This state is used when the server does not support sink state introspection. + Invalid = -1, + /// Running, sink is playing and used by at least one non-corked sink-input. + Running = 0, + /// When idle, the sink is playing but there is no non-corked sink-input attached to it. + Idle = 1, + /// When suspended, actual sink access can be closed, for instance. + Suspended = 2, +} + +impl<'a> From for DevState { + fn from(s: def::SourceState) -> Self { + match s { + def::SourceState::Idle => DevState::Idle, + def::SourceState::Invalid => DevState::Invalid, + def::SourceState::Running => DevState::Running, + def::SourceState::Suspended => DevState::Suspended, + } + } +} + +impl<'a> From for DevState { + fn from(s: def::SinkState) -> Self { + match s { + def::SinkState::Idle => DevState::Idle, + def::SinkState::Invalid => DevState::Invalid, + def::SinkState::Running => DevState::Running, + def::SinkState::Suspended => DevState::Suspended, + } + } +} + +#[derive(Clone)] +pub enum Flags { + SourceFLags(def::SourceFlagSet), + SinkFlags(def::SinkFlagSet), +} + +#[derive(Clone)] +pub struct DeviceInfo { + /// Index of the sink. + pub index: u32, + /// Name of the sink. + pub name: Option, + /// Description of this sink. + pub description: Option, + /// Sample spec of this sink. + pub sample_spec: sample::Spec, + /// Channel map. + pub channel_map: channelmap::Map, + /// Index of the owning module of this sink, or `None` if is invalid. + pub owner_module: Option, + /// Volume of the sink. + pub volume: ChannelVolumes, + /// Mute switch of the sink. + pub mute: bool, + /// Index of the monitor source connected to this sink. + pub monitor: Option, + /// The name of the monitor source. + pub monitor_name: Option, + /// Length of queued audio in the output buffer. + pub latency: MicroSeconds, + /// Driver name. + pub driver: Option, + /// Flags. + pub flags: Flags, + /// Property list. + pub proplist: Proplist, + /// The latency this device has been configured to. + pub configured_latency: MicroSeconds, + /// Some kind of “base” volume that refers to unamplified/unattenuated volume in the context of + /// the output device. + pub base_volume: Volume, + /// State. + pub state: DevState, + /// Number of volume steps for sinks which do not support arbitrary volumes. + pub n_volume_steps: u32, + /// Card index, or `None` if invalid. + pub card: Option, + /// Set of available ports. + pub ports: Vec, + // Pointer to active port in the set, or None. + pub active_port: Option, + /// Set of formats supported by the sink. + pub formats: Vec, +} + +impl<'a> From<&'a introspect::SinkInfo<'a>> for DeviceInfo { + fn from(item: &'a introspect::SinkInfo<'a>) -> Self { + DeviceInfo { + name: item.name.as_ref().map(|cow| cow.to_string()), + index: item.index, + description: item.description.as_ref().map(|cow| cow.to_string()), + sample_spec: item.sample_spec, + channel_map: item.channel_map, + owner_module: item.owner_module, + volume: item.volume, + mute: item.mute, + monitor: Some(item.monitor_source), + monitor_name: item.monitor_source_name.as_ref().map(|cow| cow.to_string()), + latency: item.latency, + driver: item.driver.as_ref().map(|cow| cow.to_string()), + flags: Flags::SinkFlags(item.flags), + proplist: item.proplist.clone(), + configured_latency: item.configured_latency, + base_volume: item.base_volume, + state: DevState::from(item.state), + n_volume_steps: item.n_volume_steps, + card: item.card, + ports: item.ports.iter().map(From::from).collect(), + active_port: item.active_port.as_ref().map(From::from), + formats: item.formats.clone(), + } + } +} + +impl<'a> From<&'a introspect::SourceInfo<'a>> for DeviceInfo { + fn from(item: &'a introspect::SourceInfo<'a>) -> Self { + DeviceInfo { + name: item.name.as_ref().map(|cow| cow.to_string()), + index: item.index, + description: item.description.as_ref().map(|cow| cow.to_string()), + sample_spec: item.sample_spec, + channel_map: item.channel_map, + owner_module: item.owner_module, + volume: item.volume, + mute: item.mute, + monitor: item.monitor_of_sink, + monitor_name: item + .monitor_of_sink_name + .as_ref() + .map(|cow| cow.to_string()), + latency: item.latency, + driver: item.driver.as_ref().map(|cow| cow.to_string()), + flags: Flags::SourceFLags(item.flags), + proplist: item.proplist.clone(), + configured_latency: item.configured_latency, + base_volume: item.base_volume, + state: DevState::from(item.state), + n_volume_steps: item.n_volume_steps, + card: item.card, + ports: item.ports.iter().map(From::from).collect(), + active_port: item.active_port.as_ref().map(From::from), + formats: item.formats.clone(), + } + } +} + +#[derive(Clone)] +pub struct ApplicationInfo { + /// Index of the sink input. + pub index: u32, + /// Name of the sink input. + pub name: Option, + /// Index of the module this sink input belongs to, or `None` when it does not belong to any + /// module. + pub owner_module: Option, + /// Index of the client this sink input belongs to, or invalid when it does not belong to any + /// client. + pub client: Option, + /// Index of the connected sink/source. + pub connection_id: u32, + /// The sample specification of the sink input. + pub sample_spec: sample::Spec, + /// Channel map. + pub channel_map: channelmap::Map, + /// The volume of this sink input. + pub volume: ChannelVolumes, + /// Latency due to buffering in sink input, see + /// [`def::TimingInfo`](../../def/struct.TimingInfo.html) for details. + pub buffer_usec: MicroSeconds, + /// Latency of the sink device, see + /// [`def::TimingInfo`](../../def/struct.TimingInfo.html) for details. + pub connection_usec: MicroSeconds, + /// The resampling method used by this sink input. + pub resample_method: Option, + /// Driver name. + pub driver: Option, + /// Stream muted. + pub mute: bool, + /// Property list. + pub proplist: Proplist, + /// Stream corked. + pub corked: bool, + /// Stream has volume. If not set, then the meaning of this struct’s volume member is unspecified. + pub has_volume: bool, + /// The volume can be set. If not set, the volume can still change even though clients can’t + /// control the volume. + pub volume_writable: bool, + /// Stream format information. + pub format: format::Info, +} + +impl<'a> From<&'a introspect::SinkInputInfo<'a>> for ApplicationInfo { + fn from(item: &'a introspect::SinkInputInfo<'a>) -> Self { + ApplicationInfo { + index: item.index, + name: item.name.as_ref().map(|cow| cow.to_string()), + owner_module: item.owner_module, + client: item.client, + connection_id: item.sink, + sample_spec: item.sample_spec, + channel_map: item.channel_map, + volume: item.volume, + buffer_usec: item.buffer_usec, + connection_usec: item.sink_usec, + resample_method: item.resample_method.as_ref().map(|cow| cow.to_string()), + driver: item.driver.as_ref().map(|cow| cow.to_string()), + mute: item.mute, + proplist: item.proplist.clone(), + corked: item.corked, + has_volume: item.has_volume, + volume_writable: item.volume_writable, + format: item.format.clone(), + } + } +} + +impl<'a> From<&'a introspect::SourceOutputInfo<'a>> for ApplicationInfo { + fn from(item: &'a introspect::SourceOutputInfo<'a>) -> Self { + ApplicationInfo { + index: item.index, + name: item.name.as_ref().map(|cow| cow.to_string()), + owner_module: item.owner_module, + client: item.client, + connection_id: item.source, + sample_spec: item.sample_spec, + channel_map: item.channel_map, + volume: item.volume, + buffer_usec: item.buffer_usec, + connection_usec: item.source_usec, + resample_method: item.resample_method.as_ref().map(|cow| cow.to_string()), + driver: item.driver.as_ref().map(|cow| cow.to_string()), + mute: item.mute, + proplist: item.proplist.clone(), + corked: item.corked, + has_volume: item.has_volume, + volume_writable: item.volume_writable, + format: item.format.clone(), + } + } +} + +pub struct ServerInfo { + /// User name of the daemon process. + pub user_name: Option, + /// Host name the daemon is running on. + pub host_name: Option, + /// Version string of the daemon. + pub server_version: Option, + /// Server package name (usually “pulseaudio”). + pub server_name: Option, + /// Default sample specification. + pub sample_spec: sample::Spec, + /// Name of default sink. + pub default_sink_name: Option, + /// Name of default source. + pub default_source_name: Option, + /// A random cookie for identifying this instance of PulseAudio. + pub cookie: u32, + /// Default channel map. + pub channel_map: channelmap::Map, +} + +impl<'a> From<&'a introspect::ServerInfo<'a>> for ServerInfo { + fn from(info: &'a introspect::ServerInfo<'a>) -> Self { + ServerInfo { + user_name: info.user_name.as_ref().map(|cow| cow.to_string()), + host_name: info.host_name.as_ref().map(|cow| cow.to_string()), + server_version: info.server_version.as_ref().map(|cow| cow.to_string()), + server_name: info.server_name.as_ref().map(|cow| cow.to_string()), + sample_spec: info.sample_spec, + default_sink_name: info.default_sink_name.as_ref().map(|cow| cow.to_string()), + default_source_name: info.default_source_name.as_ref().map(|cow| cow.to_string()), + cookie: info.cookie, + channel_map: info.channel_map, + } + } +} diff --git a/libs/pulsectl/src/errors.rs b/libs/pulsectl/src/errors.rs new file mode 100644 index 00000000000..b75a8f5335d --- /dev/null +++ b/libs/pulsectl/src/errors.rs @@ -0,0 +1,54 @@ +use std::fmt; + +use pulse::error::{PAErr}; + +impl From for PulseCtlError { + fn from(error: PAErr) -> Self { + PulseCtlError { + error: PulseCtlErrorType::PulseAudioError, + message: format!("PulseAudio returned error: {}", error.to_string().unwrap_or("Unknown".to_owned())), + } + } +} + +impl fmt::Debug for PulseCtlError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut error_string = String::new(); + match self.error { + PulseCtlErrorType::ConnectError => { + error_string.push_str("ConnectError"); + } + PulseCtlErrorType::OperationError => { + error_string.push_str("OperationError"); + } + PulseCtlErrorType::PulseAudioError => { + error_string.push_str("PulseAudioError"); + } + } + write!(f, "[{}]: {}", error_string, self.message) + } +} + +pub(crate) enum PulseCtlErrorType { + ConnectError, + OperationError, + PulseAudioError, +} + +/// Error thrown when PulseAudio throws an error code, there are 3 variants +/// `PulseCtlErrorType::ConnectError` when there's an error establishing a connection +/// `PulseCtlErrorType::OperationError` when the requested operation quis unexpecdatly or is cancelled +/// `PulseCtlErrorType::PulseAudioError` when PulseAudio returns an error code in any circumstance +pub struct PulseCtlError { + error: PulseCtlErrorType, + message: String, +} + +impl PulseCtlError { + pub(crate) fn new(err: PulseCtlErrorType, msg: &str) -> Self { + PulseCtlError { + error: err, + message: msg.to_string(), + } + } +} diff --git a/libs/pulsectl/src/lib.rs b/libs/pulsectl/src/lib.rs new file mode 100644 index 00000000000..073902fd9d3 --- /dev/null +++ b/libs/pulsectl/src/lib.rs @@ -0,0 +1,166 @@ +/// `pulsectl` is a high level wrapper around the PulseAudio bindings supplied by +/// `libpulse_binding`. It provides simple access to sinks, inputs, sources and outputs allowing +/// one to write audio control programs with ease. +/// +/// ## Quick Example +/// +/// The following example demonstrates listing all of the playback devices currently connected +/// +/// See examples/change_device_vol.rs for a more complete example +/// ```no_run +/// extern crate pulsectl; +/// +/// use std::io; +/// +/// use pulsectl::controllers::SinkController; +/// use pulsectl::controllers::DeviceControl; +/// fn main() { +/// // create handler that calls functions on playback devices and apps +/// let mut handler = SinkController::create().unwrap(); +/// let devices = handler +/// .list_devices() +/// .expect("Could not get list of playback devices"); +/// +/// println!("Playback Devices"); +/// for dev in devices.clone() { +/// println!( +/// "[{}] {}, Volume: {}", +/// dev.index, +/// dev.description.as_ref().unwrap(), +/// dev.volume.print() +/// ); +/// } +/// } +/// ``` +extern crate libpulse_binding as pulse; + +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; + +use pulse::{ + context::{introspect, Context}, + mainloop::standard::{IterateResult, Mainloop}, + operation::{Operation, State}, + proplist::Proplist, +}; + +use crate::errors::{PulseCtlError, PulseCtlErrorType::*}; + +pub mod controllers; +mod errors; + +pub struct Handler { + pub mainloop: Rc>, + pub context: Rc>, + pub introspect: introspect::Introspector, +} + +fn connect_error(err: &str) -> PulseCtlError { + PulseCtlError::new(ConnectError, err) +} + +impl Handler { + pub fn connect(name: &str) -> Result { + let mut proplist = Proplist::new().unwrap(); + proplist + .set_str(pulse::proplist::properties::APPLICATION_NAME, name) + .unwrap(); + + let mainloop; + if let Some(m) = Mainloop::new() { + mainloop = Rc::new(RefCell::new(m)); + } else { + return Err(connect_error("Failed to create mainloop")); + } + + let context; + if let Some(c) = + Context::new_with_proplist(mainloop.borrow().deref(), "MainConn", &proplist) + { + context = Rc::new(RefCell::new(c)); + } else { + return Err(connect_error("Failed to create new context")); + } + + context + .borrow_mut() + .connect(None, pulse::context::flags::NOFLAGS, None) + .map_err(|_| connect_error("Failed to connect context"))?; + + loop { + match mainloop.borrow_mut().iterate(false) { + IterateResult::Err(e) => { + eprintln!("iterate state was not success, quitting..."); + return Err(e.into()); + } + IterateResult::Success(_) => {} + IterateResult::Quit(_) => { + eprintln!("iterate state was not success, quitting..."); + return Err(PulseCtlError::new( + ConnectError, + "Iterate state quit without an error", + )); + } + } + + match context.borrow().get_state() { + pulse::context::State::Ready => break, + pulse::context::State::Failed | pulse::context::State::Terminated => { + eprintln!("context state failed/terminated, quitting..."); + return Err(PulseCtlError::new( + ConnectError, + "Context state failed/terminated without an error", + )); + } + _ => {} + } + } + + let introspect = context.borrow_mut().introspect(); + Ok(Handler { + mainloop, + context, + introspect, + }) + } + + // loop until the passed operation is completed + pub fn wait_for_operation( + &mut self, + op: Operation, + ) -> Result<(), errors::PulseCtlError> { + loop { + match self.mainloop.borrow_mut().iterate(false) { + IterateResult::Err(e) => return Err(e.into()), + IterateResult::Success(_) => {} + IterateResult::Quit(_) => { + return Err(PulseCtlError::new( + OperationError, + "Iterate state quit without an error", + )); + } + } + match op.get_state() { + State::Done => { + break; + } + State::Running => {} + State::Cancelled => { + return Err(PulseCtlError::new( + OperationError, + "Operation cancelled without an error", + )); + } + } + } + Ok(()) + } +} + +impl Drop for Handler { + fn drop(&mut self) { + self.context.borrow_mut().disconnect(); + self.mainloop.borrow_mut().quit(pulse::def::Retval(0)); + } +} diff --git a/libs/scrap/.gitignore b/libs/scrap/.gitignore new file mode 100644 index 00000000000..5e10219a107 --- /dev/null +++ b/libs/scrap/.gitignore @@ -0,0 +1,4 @@ +/target/ +**/*.rs.bk +Cargo.lock +generated/ diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml new file mode 100644 index 00000000000..01cb824293c --- /dev/null +++ b/libs/scrap/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "scrap" +description = "Screen capture made easy." +version = "0.5.0" +repository = "https://github.com/quadrupleslap/scrap" +documentation = "https://docs.rs/scrap" +keywords = ["screen", "capture", "record"] +license = "MIT" +authors = ["Ram "] +edition = "2018" + +[dependencies] +block = "0.1" +cfg-if = "1.0" +libc = "0.2" +num_cpus = "1.13" + +[dependencies.winapi] +version = "0.3" +default-features = true +features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11"] + +[dev-dependencies] +repng = "0.2" +docopt = "1.1" +webm = "1.0" +serde = {version="1.0", features=["derive"]} +quest = "0.3" + +[build-dependencies] +target_build_utils = "0.3" +bindgen = "0.53" diff --git a/libs/scrap/README.md b/libs/scrap/README.md new file mode 100644 index 00000000000..7839e34815f --- /dev/null +++ b/libs/scrap/README.md @@ -0,0 +1,60 @@ +# scrap + +Scrap records your screen! At least it does if you're on Windows, macOS, or Linux. + +## Usage + +```toml +[dependencies] +scrap = "0.5" +``` + +Its API is as simple as it gets! + +```rust +struct Display; /// A screen. +struct Frame; /// An array of the pixels that were on-screen. +struct Capturer; /// A recording instance. + +impl Capturer { + /// Begin recording. + pub fn new(display: Display) -> io::Result; + + /// Try to get a frame. + /// Returns WouldBlock if it's not ready yet. + pub fn frame<'a>(&'a mut self) -> io::Result>; + + pub fn width(&self) -> usize; + pub fn height(&self) -> usize; +} + +impl Display { + /// The primary screen. + pub fn primary() -> io::Result; + + /// All the screens. + pub fn all() -> io::Result>; + + pub fn width(&self) -> usize; + pub fn height(&self) -> usize; +} + +impl<'a> ops::Deref for Frame<'a> { + /// A frame is just an array of bytes. + type Target = [u8]; +} +``` + +## The Frame Format + +- The frame format is guaranteed to be **packed BGRA**. +- The width and height are guaranteed to remain constant. +- The stride might be greater than the width, and it may also vary between frames. + +## System Requirements + +OS | Minimum Requirements +--------|--------------------- +macOS | macOS 10.8 +Linux | XCB + SHM + RandR +Windows | DirectX 11.1 diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs new file mode 100644 index 00000000000..c4354076ade --- /dev/null +++ b/libs/scrap/build.rs @@ -0,0 +1,118 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +fn find_package(name: &str) -> Vec { + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: PathBuf = vcpkg_root.into(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let target = if target_os == "macos" { + "x64-osx".to_owned() + } else if target_os == "windows" { + "x64-windows-static".to_owned() + } else if target_os == "android" { + format!("{}-android-static", target_arch) + } else { + "x64-linux".to_owned() + }; + println!("cargo:info={}", target); + path.push("installed"); + path.push(target); + let mut lib = name.trim_start_matches("lib").to_string(); + if lib == "vpx" && target_os == "windows" { + lib = format!("{}mt", lib); + } + println!("{}", format!("cargo:rustc-link-lib={}", lib)); + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + let include = path.join("include"); + println!("{}", format!("cargo:include={}", include.to_str().unwrap())); + vec![include] +} + +fn generate_bindings( + ffi_header: &Path, + include_paths: &[PathBuf], + ffi_rs: &Path, + exact_file: &Path, +) { + let mut b = bindgen::builder() + .header(ffi_header.to_str().unwrap()) + .whitelist_type("^[vV].*") + .whitelist_var("^[vV].*") + .whitelist_function("^[vV].*") + .rustified_enum("^v.*") + .trust_clang_mangling(false) + .layout_tests(false) // breaks 32/64-bit compat + .generate_comments(false); // vpx comments have prefix /*!\ + + for dir in include_paths { + b = b.clang_arg(format!("-I{}", dir.display())); + } + + b.generate().unwrap().write_to_file(ffi_rs).unwrap(); + fs::copy(ffi_rs, exact_file).ok(); // ignore failure +} + +fn gen_vpx() { + let includes = find_package("libvpx"); + let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); + let src_dir = Path::new(&src_dir); + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + let ffi_header = src_dir.join("vpx_ffi.h"); + println!("rerun-if-changed={}", ffi_header.display()); + for dir in &includes { + println!("rerun-if-changed={}", dir.display()); + } + + let ffi_rs = out_dir.join("vpx_ffi.rs"); + let exact_file = src_dir.join("generated").join("vpx_ffi.rs"); + generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file); +} + +fn main() { + // note: all link symbol names in x86 (32-bit) are prefixed wth "_". + // run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc, + // please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc", + // then set x64 to default by "rustup default stable-x86_64-pc-windows-msvc" + let target = target_build_utils::TargetInfo::new(); + if target.unwrap().target_pointer_width() != "64" { + panic!("Only support 64bit system"); + } + env::remove_var("CARGO_CFG_TARGET_FEATURE"); + env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static"); + + find_package("libyuv"); + gen_vpx(); + + // there is problem with cfg(target_os) in build.rs, so use our workaround + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os == "android" || target_os == "ios" { + // nothing + } else if cfg!(windows) { + // The first choice is Windows because DXGI is amazing. + println!("cargo:rustc-cfg=dxgi"); + } else if cfg!(target_os = "macos") { + // Quartz is second because macOS is the (annoying) exception. + println!("cargo:rustc-cfg=quartz"); + } else if cfg!(unix) { + // On UNIX we pray that X11 (with XCB) is available. + println!("cargo:rustc-cfg=x11"); + } +} diff --git a/libs/scrap/examples/ffplay.rs b/libs/scrap/examples/ffplay.rs new file mode 100644 index 00000000000..a4ca1b35bff --- /dev/null +++ b/libs/scrap/examples/ffplay.rs @@ -0,0 +1,51 @@ +extern crate scrap; + +fn main() { + use scrap::{Capturer, Display}; + use std::io::ErrorKind::WouldBlock; + use std::io::Write; + use std::process::{Command, Stdio}; + + let d = Display::primary().unwrap(); + let (w, h) = (d.width(), d.height()); + + let child = Command::new("ffplay") + .args(&[ + "-f", + "rawvideo", + "-pixel_format", + "bgr0", + "-video_size", + &format!("{}x{}", w, h), + "-framerate", + "60", + "-", + ]) + .stdin(Stdio::piped()) + .spawn() + .expect("This example requires ffplay."); + + let mut capturer = Capturer::new(d, false).unwrap(); + let mut out = child.stdin.unwrap(); + + loop { + match capturer.frame(0) { + Ok(frame) => { + // Write the frame, removing end-of-row padding. + let stride = frame.len() / h; + let rowlen = 4 * w; + for row in frame.chunks(stride) { + let row = &row[..rowlen]; + out.write_all(row).unwrap(); + } + } + Err(ref e) if e.kind() == WouldBlock => { + // Wait for the frame. + } + Err(_) => { + // We're done here. + break; + } + } + } +} diff --git a/libs/scrap/examples/list.rs b/libs/scrap/examples/list.rs new file mode 100644 index 00000000000..af5bb4559a6 --- /dev/null +++ b/libs/scrap/examples/list.rs @@ -0,0 +1,16 @@ +extern crate scrap; + +use scrap::Display; + +fn main() { + let displays = Display::all().unwrap(); + + for (i, display) in displays.iter().enumerate() { + println!( + "Display {} [{}x{}]", + i + 1, + display.width(), + display.height() + ); + } +} diff --git a/libs/scrap/examples/record-screen.rs b/libs/scrap/examples/record-screen.rs new file mode 100644 index 00000000000..bd8291d274b --- /dev/null +++ b/libs/scrap/examples/record-screen.rs @@ -0,0 +1,161 @@ +extern crate docopt; +extern crate quest; +extern crate repng; +extern crate scrap; +extern crate serde; +extern crate webm; + +use std::fs::{File, OpenOptions}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use std::{io, thread}; + +use docopt::Docopt; +use webm::mux; +use webm::mux::Track; + +use scrap::codec as vpx_encode; +use scrap::{Capturer, Display, STRIDE_ALIGN}; + +const USAGE: &'static str = " +Simple WebM screen capture. + +Usage: + record-screen [--time=] [--fps=] [--bv=] [--ba=] [--codec CODEC] + record-screen (-h | --help) + +Options: + -h --help Show this screen. + --time= Recording duration in seconds. + --fps= Frames per second [default: 30]. + --bv= Video bitrate in kilobits per second [default: 5000]. + --ba= Audio bitrate in kilobits per second [default: 96]. + --codec CODEC Configure the codec used. [default: vp9] + Valid values: vp8, vp9. +"; + +#[derive(Debug, serde::Deserialize)] +struct Args { + arg_path: PathBuf, + flag_codec: Codec, + flag_time: Option, + flag_fps: u64, + flag_bv: u32, + flag_ba: u32, +} + +#[derive(Debug, serde::Deserialize)] +enum Codec { + Vp8, + Vp9, +} + +fn main() -> io::Result<()> { + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + + let duration = args.flag_time.map(Duration::from_secs); + + let d = Display::primary().unwrap(); + let (width, height) = (d.width() as u32, d.height() as u32); + + // Setup the multiplexer. + + let out = match { + OpenOptions::new() + .write(true) + .create_new(true) + .open(&args.arg_path) + } { + Ok(file) => file, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => { + if loop { + quest::ask("Overwrite the existing file? [y/N] "); + if let Some(b) = quest::yesno(false)? { + break b; + } + } { + File::create(&args.arg_path)? + } else { + return Ok(()); + } + } + Err(e) => return Err(e.into()), + }; + + let mut webm = + mux::Segment::new(mux::Writer::new(out)).expect("Could not initialize the multiplexer."); + + let (vpx_codec, mux_codec) = match args.flag_codec { + Codec::Vp8 => (vpx_encode::VideoCodecId::VP8, mux::VideoCodecId::VP8), + Codec::Vp9 => (vpx_encode::VideoCodecId::VP9, mux::VideoCodecId::VP9), + }; + + let mut vt = webm.add_video_track(width, height, None, mux_codec); + + // Setup the encoder. + + let mut vpx = vpx_encode::Encoder::new( + &vpx_encode::Config { + width, + height, + timebase: [1, 1000], + bitrate: args.flag_bv, + codec: vpx_codec, + rc_min_quantizer: 0, + rc_max_quantizer: 0, + speed: 6, + }, + 0, + ) + .unwrap(); + + // Start recording. + + let start = Instant::now(); + let stop = Arc::new(AtomicBool::new(false)); + + thread::spawn({ + let stop = stop.clone(); + move || { + let _ = quest::ask("Recording! Press ⏎ to stop."); + let _ = quest::text(); + stop.store(true, Ordering::Release); + } + }); + + let spf = Duration::from_nanos(1_000_000_000 / args.flag_fps); + + // Capturer object is expensive, avoiding to create it frequently. + let mut c = Capturer::new(d, true).unwrap(); + while !stop.load(Ordering::Acquire) { + let now = Instant::now(); + let time = now - start; + + if Some(true) == duration.map(|d| time > d) { + break; + } + + if let Ok(frame) = c.frame(0) { + let ms = time.as_secs() * 1000 + time.subsec_millis() as u64; + + for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() { + vt.add_frame(frame.data, frame.pts as u64 * 1_000_000, frame.key); + } + } + + let dt = now.elapsed(); + if dt < spf { + thread::sleep(spf - dt); + } + } + + // End things. + + let _ = webm.finalize(None); + + Ok(()) +} diff --git a/libs/scrap/examples/screenshot.rs b/libs/scrap/examples/screenshot.rs new file mode 100644 index 00000000000..dee410b0dd4 --- /dev/null +++ b/libs/scrap/examples/screenshot.rs @@ -0,0 +1,122 @@ +extern crate repng; +extern crate scrap; + +use std::fs::File; +use std::io::ErrorKind::WouldBlock; +use std::thread; +use std::time::Duration; + +use scrap::{i420_to_rgb, Capturer, Display}; + +fn main() { + let n = Display::all().unwrap().len(); + for i in 0..n { + record(i); + } +} + +fn get_display(i: usize) -> Display { + Display::all().unwrap().remove(i) +} + +fn record(i: usize) { + let one_second = Duration::new(1, 0); + let one_frame = one_second / 60; + + let display = get_display(i); + let mut capturer = Capturer::new(display, false).expect("Couldn't begin capture."); + let (w, h) = (capturer.width(), capturer.height()); + + loop { + // Wait until there's a frame. + + let buffer = match capturer.frame(0) { + Ok(buffer) => buffer, + Err(error) => { + if error.kind() == WouldBlock { + // Keep spinning. + thread::sleep(one_frame); + continue; + } else { + panic!("Error: {}", error); + } + } + }; + + println!("Captured! Saving..."); + + // Flip the BGRA image into a RGBA image. + + let mut bitflipped = Vec::with_capacity(w * h * 4); + let stride = buffer.len() / h; + + for y in 0..h { + for x in 0..w { + let i = stride * y + 4 * x; + bitflipped.extend_from_slice(&[buffer[i + 2], buffer[i + 1], buffer[i], 255]); + } + } + + // Save the image. + + let name = format!("screenshot{}_1.png", i); + repng::encode( + File::create(name.clone()).unwrap(), + w as u32, + h as u32, + &bitflipped, + ) + .unwrap(); + + println!("Image saved to `{}`.", name); + break; + } + + drop(capturer); + let display = get_display(i); + let mut capturer = Capturer::new(display, true).expect("Couldn't begin capture."); + let (w, h) = (capturer.width(), capturer.height()); + + loop { + // Wait until there's a frame. + + let buffer = match capturer.frame(0) { + Ok(buffer) => buffer, + Err(error) => { + if error.kind() == WouldBlock { + // Keep spinning. + thread::sleep(one_frame); + continue; + } else { + panic!("Error: {}", error); + } + } + }; + + println!("Captured! Saving..."); + + let mut frame = Default::default(); + i420_to_rgb(w, h, &buffer, &mut frame); + + let mut bitflipped = Vec::with_capacity(w * h * 4); + let stride = frame.len() / h; + + for y in 0..h { + for x in 0..w { + let i = stride * y + 3 * x; + bitflipped.extend_from_slice(&[frame[i], frame[i + 1], frame[i + 2], 255]); + } + } + let name = format!("screenshot{}_2.png", i); + repng::encode( + File::create(name.clone()).unwrap(), + w as u32, + h as u32, + &bitflipped, + ) + .unwrap(); + + println!("Image saved to `{}`.", name); + break; + } +} diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs new file mode 100644 index 00000000000..6977f51b10b --- /dev/null +++ b/libs/scrap/src/common/codec.rs @@ -0,0 +1,536 @@ +// https://github.com/astraw/vpx-encode +// https://github.com/astraw/env-libvpx-sys +// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs + +use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; +use std::os::raw::{c_int, c_uint}; +use std::{ptr, slice}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum VideoCodecId { + VP8, + VP9, +} + +impl Default for VideoCodecId { + fn default() -> VideoCodecId { + VideoCodecId::VP9 + } +} + +pub struct Encoder { + ctx: vpx_codec_ctx_t, + width: usize, + height: usize, +} + +pub struct Decoder { + ctx: vpx_codec_ctx_t, +} + +#[derive(Debug)] +pub enum Error { + FailedCall(String), + BadPtr(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +macro_rules! call_vpx { + ($x:expr) => {{ + let result = unsafe { $x }; // original expression + let result_int = unsafe { std::mem::transmute::<_, i32>(result) }; + if result_int != 0 { + return Err(Error::FailedCall(format!( + "errcode={} {}:{}:{}:{}", + result_int, + module_path!(), + file!(), + line!(), + column!() + )) + .into()); + } + result + }}; +} + +macro_rules! call_vpx_ptr { + ($x:expr) => {{ + let result = unsafe { $x }; // original expression + let result_int = unsafe { std::mem::transmute::<_, i64>(result) }; + if result_int == 0 { + return Err(Error::BadPtr(format!( + "errcode={} {}:{}:{}:{}", + result_int, + module_path!(), + file!(), + line!(), + column!() + )) + .into()); + } + result + }}; +} + +impl Encoder { + pub fn new(config: &Config, num_threads: u32) -> Result { + let i; + if cfg!(feature = "VP8") { + i = match config.codec { + VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), + VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()), + }; + } else { + i = call_vpx_ptr!(vpx_codec_vp9_cx()); + } + let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0)); + + // https://www.webmproject.org/docs/encoder-parameters/ + // default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63 + // try rc_resize_allowed later + + c.g_w = config.width; + c.g_h = config.height; + c.g_timebase.num = config.timebase[0]; + c.g_timebase.den = config.timebase[1]; + c.rc_target_bitrate = config.bitrate; + c.rc_undershoot_pct = 95; + c.rc_dropframe_thresh = 25; + if config.rc_min_quantizer > 0 { + c.rc_min_quantizer = config.rc_min_quantizer; + } + if config.rc_max_quantizer > 0 { + c.rc_max_quantizer = config.rc_max_quantizer; + } + let mut speed = config.speed; + if speed <= 0 { + speed = 6; + } + + c.g_threads = if num_threads == 0 { + num_cpus::get() as _ + } else { + num_threads + }; + c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; + // https://developers.google.com/media/vp9/bitrate-modes/ + // Constant Bitrate mode (CBR) is recommended for live streaming with VP9. + c.rc_end_usage = vpx_rc_mode::VPX_CBR; + // c.kf_min_dist = 0; + // c.kf_max_dist = 999999; + c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot + + /* + VPX encoder支持two-pass encode,这是为了rate control的。 + 对于两遍编码,就是需要整个编码过程做两次,第一次会得到一些新的控制参数来进行第二遍的编码, + 这样可以在相同的bitrate下得到最好的PSNR + */ + + let mut ctx = Default::default(); + call_vpx!(vpx_codec_enc_init_ver( + &mut ctx, + i, + &c, + 0, + VPX_ENCODER_ABI_VERSION as _ + )); + + if config.codec == VideoCodecId::VP9 { + // set encoder internal speed settings + // in ffmpeg, it is --speed option + /* + set to 0 or a positive value 1-16, the codec will try to adapt its + complexity depending on the time it spends encoding. Increasing this + number will make the speed go up and the quality go down. + Negative values mean strict enforcement of this + while positive values are adaptive + */ + /* https://developers.google.com/media/vp9/live-encoding + Speed 5 to 8 should be used for live / real-time encoding. + Lower numbers (5 or 6) are higher quality but require more CPU power. + Higher numbers (7 or 8) will be lower quality but more manageable for lower latency + use cases and also for lower CPU power devices such as mobile. + */ + call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, speed,)); + // set row level multi-threading + /* + as some people in comments and below have already commented, + more recent versions of libvpx support -row-mt 1 to enable tile row + multi-threading. This can increase the number of tiles by up to 4x in VP9 + (since the max number of tile rows is 4, regardless of video height). + To enable this, use -tile-rows N where N is the number of tile rows in + log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile + rows). The total number of active threads will then be equal to + $tile_rows * $tile_columns + */ + call_vpx!(vpx_codec_control_( + &mut ctx, + VP9E_SET_ROW_MT as _, + 1 as c_int + )); + + call_vpx!(vpx_codec_control_( + &mut ctx, + VP9E_SET_TILE_COLUMNS as _, + 4 as c_int + )); + } + + Ok(Self { + ctx, + width: config.width as _, + height: config.height as _, + }) + } + + pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result { + assert!(2 * data.len() >= 3 * self.width * self.height); + + let mut image = Default::default(); + call_vpx_ptr!(vpx_img_wrap( + &mut image, + vpx_img_fmt::VPX_IMG_FMT_I420, + self.width as _, + self.height as _, + stride_align as _, + data.as_ptr() as _, + )); + + call_vpx!(vpx_codec_encode( + &mut self.ctx, + &image, + pts as _, + 1, // Duration + 0, // Flags + VPX_DL_REALTIME as _, + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the encoder to return any pending packets + pub fn flush(&mut self) -> Result { + call_vpx!(vpx_codec_encode( + &mut self.ctx, + ptr::null(), + -1, // PTS + 1, // Duration + 0, // Flags + VPX_DL_REALTIME as _, + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } +} + +impl Drop for Encoder { + fn drop(&mut self) { + unsafe { + let result = vpx_codec_destroy(&mut self.ctx); + if result != VPX_CODEC_OK { + panic!("failed to destroy vpx codec"); + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct EncodeFrame<'a> { + /// Compressed data. + pub data: &'a [u8], + /// Whether the frame is a keyframe. + pub key: bool, + /// Presentation timestamp (in timebase units). + pub pts: i64, +} + +#[derive(Clone, Copy, Debug)] +pub struct Config { + /// The width (in pixels). + pub width: c_uint, + /// The height (in pixels). + pub height: c_uint, + /// The timebase numerator and denominator (in seconds). + pub timebase: [c_int; 2], + /// The target bitrate (in kilobits per second). + pub bitrate: c_uint, + /// The codec + pub codec: VideoCodecId, + pub rc_min_quantizer: u32, + pub rc_max_quantizer: u32, + pub speed: i32, +} + +pub struct EncodeFrames<'a> { + ctx: &'a mut vpx_codec_ctx_t, + iter: vpx_codec_iter_t, +} + +impl<'a> Iterator for EncodeFrames<'a> { + type Item = EncodeFrame<'a>; + fn next(&mut self) -> Option { + loop { + unsafe { + let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter); + if pkt.is_null() { + return None; + } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT { + let f = &(*pkt).data.frame; + return Some(Self::Item { + data: slice::from_raw_parts(f.buf as _, f.sz as _), + key: (f.flags & VPX_FRAME_IS_KEY) != 0, + pts: f.pts, + }); + } else { + // Ignore the packet. + } + } + } + } +} + +impl Decoder { + /// Create a new decoder + /// + /// # Errors + /// + /// The function may fail if the underlying libvpx does not provide + /// the VP9 decoder. + pub fn new(codec: VideoCodecId, num_threads: u32) -> Result { + // This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can + // cause UB if uninitialized. + let i; + if cfg!(feature = "VP8") { + i = match codec { + VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), + VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), + }; + } else { + i = call_vpx_ptr!(vpx_codec_vp9_dx()); + } + let mut ctx = Default::default(); + let cfg = vpx_codec_dec_cfg_t { + threads: if num_threads == 0 { + num_cpus::get() as _ + } else { + num_threads + }, + w: 0, + h: 0, + }; + /* + unsafe { + println!("{}", vpx_codec_get_caps(i)); + } + */ + call_vpx!(vpx_codec_dec_init_ver( + &mut ctx, + i, + &cfg, + 0, + VPX_DECODER_ABI_VERSION as _, + )); + Ok(Self { ctx }) + } + + pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> { + let mut img = Image::new(); + for frame in self.decode(data)? { + drop(img); + img = frame; + } + for frame in self.flush()? { + drop(img); + img = frame; + } + if img.is_null() { + Ok(Vec::new()) + } else { + let mut out = Default::default(); + img.rgb(1, rgba, &mut out); + Ok(out) + } + } + + /// Feed some compressed data to the encoder + /// + /// The `data` slice is sent to the decoder + /// + /// It matches a call to `vpx_codec_decode`. + pub fn decode(&mut self, data: &[u8]) -> Result { + call_vpx!(vpx_codec_decode( + &mut self.ctx, + data.as_ptr(), + data.len() as _, + ptr::null_mut(), + 0, + )); + + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the decoder to return any pending frame + pub fn flush(&mut self) -> Result { + call_vpx!(vpx_codec_decode( + &mut self.ctx, + ptr::null(), + 0, + ptr::null_mut(), + 0 + )); + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } +} + +impl Drop for Decoder { + fn drop(&mut self) { + unsafe { + let result = vpx_codec_destroy(&mut self.ctx); + if result != VPX_CODEC_OK { + panic!("failed to destroy vpx codec"); + } + } + } +} + +pub struct DecodeFrames<'a> { + ctx: &'a mut vpx_codec_ctx_t, + iter: vpx_codec_iter_t, +} + +impl<'a> Iterator for DecodeFrames<'a> { + type Item = Image; + fn next(&mut self) -> Option { + let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) }; + if img.is_null() { + return None; + } else { + return Some(Image(img)); + } + } +} + +// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c +pub struct Image(*mut vpx_image_t); +impl Image { + #[inline] + pub fn new() -> Self { + Self(std::ptr::null_mut()) + } + + #[inline] + pub fn is_null(&self) -> bool { + self.0.is_null() + } + + #[inline] + pub fn width(&self) -> usize { + self.inner().d_w as _ + } + + #[inline] + pub fn height(&self) -> usize { + self.inner().d_h as _ + } + + #[inline] + pub fn format(&self) -> vpx_img_fmt_t { + // VPX_IMG_FMT_I420 + self.inner().fmt + } + + #[inline] + pub fn inner(&self) -> &vpx_image_t { + unsafe { &*self.0 } + } + + #[inline] + pub fn stride(&self, iplane: usize) -> i32 { + self.inner().stride[iplane] + } + + pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) { + let h = self.height(); + let mut w = self.width(); + let bps = if rgba { 4 } else { 3 }; + w = (w + stride_align - 1) & !(stride_align - 1); + dst.resize(h * w * bps, 0); + let img = self.inner(); + unsafe { + if rgba { + super::I420ToARGB( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } else { + super::I420ToRAW( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } + } + } + + #[inline] + pub fn data(&self) -> (&[u8], &[u8], &[u8]) { + unsafe { + let img = self.inner(); + let h = (img.d_h as usize + 1) & !1; + let n = img.stride[0] as usize * h; + let y = slice::from_raw_parts(img.planes[0], n); + let n = img.stride[1] as usize * (h >> 1); + let u = slice::from_raw_parts(img.planes[1], n); + let v = slice::from_raw_parts(img.planes[2], n); + (y, u, v) + } + } +} + +impl Drop for Image { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { vpx_img_free(self.0) }; + } + } +} + +unsafe impl Send for vpx_codec_ctx_t {} diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs new file mode 100644 index 00000000000..2cf9a4eca1a --- /dev/null +++ b/libs/scrap/src/common/convert.rs @@ -0,0 +1,188 @@ +use super::vpx::*; +use std::os::raw::c_int; + +extern "C" { + // seems libyuv uses reverse byte order compared with our view + + pub fn ARGBRotate( + src_argb: *const u8, + src_stride_argb: c_int, + dst_argb: *mut u8, + dst_stride_argb: c_int, + width: c_int, + height: c_int, + mode: c_int, + ) -> c_int; + + pub fn ARGBMirror( + src_argb: *const u8, + src_stride_argb: c_int, + dst_argb: *mut u8, + dst_stride_argb: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + pub fn ARGBToI420( + src_bgra: *const u8, + src_stride_bgra: c_int, + dst_y: *mut u8, + dst_stride_y: c_int, + dst_u: *mut u8, + dst_stride_u: c_int, + dst_v: *mut u8, + dst_stride_v: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + pub fn NV12ToI420( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + dst_y: *mut u8, + dst_stride_y: c_int, + dst_u: *mut u8, + dst_stride_u: c_int, + dst_v: *mut u8, + dst_stride_v: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + // I420ToRGB24: RGB little endian (bgr in memory) + // I420ToRaw: RGB big endian (rgb in memory) to RGBA. + pub fn I420ToRAW( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_raw: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + pub fn I420ToARGB( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; +} + +// https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c +#[inline] +fn get_vpx_i420_stride( + width: usize, + height: usize, + stride_align: usize, +) -> (usize, usize, usize, usize, usize, usize) { + let mut img = Default::default(); + unsafe { + vpx_img_wrap( + &mut img, + vpx_img_fmt::VPX_IMG_FMT_I420, + width as _, + height as _, + stride_align as _, + 0x1 as _, + ); + } + ( + img.w as _, + img.h as _, + img.stride[0] as _, + img.stride[1] as _, + img.planes[1] as usize - img.planes[0] as usize, + img.planes[2] as usize - img.planes[0] as usize, + ) +} + +pub fn i420_to_rgb(width: usize, height: usize, src: &[u8], dst: &mut Vec) { + let (_, _, src_stride_y, src_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let src_y = src.as_ptr(); + let src_u = src[u..].as_ptr(); + let src_v = src[v..].as_ptr(); + dst.resize(width * height * 3, 0); + unsafe { + super::I420ToRAW( + src_y, + src_stride_y as _, + src_u, + src_stride_uv as _, + src_v, + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 3) as _, + width as _, + height as _, + ); + }; +} + +pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec) { + let (_, h, dst_stride_y, dst_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let bps = 12; + dst.resize(h * dst_stride_y * bps / 8, 0); + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[u..].as_mut_ptr(); + let dst_v = dst[v..].as_mut_ptr(); + unsafe { + ARGBToI420( + src.as_ptr(), + (src.len() / height) as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ); + } +} + +pub unsafe fn nv12_to_i420( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + width: usize, + height: usize, + dst: &mut Vec, +) { + let (w, h, dst_stride_y, dst_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let bps = 12; + dst.resize(h * w * bps / 8, 0); + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[u..].as_mut_ptr(); + let dst_v = dst[v..].as_mut_ptr(); + NV12ToI420( + src_y, + src_stride_y, + src_uv, + src_stride_uv, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ); +} diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs new file mode 100644 index 00000000000..476027a72be --- /dev/null +++ b/libs/scrap/src/common/dxgi.rs @@ -0,0 +1,104 @@ +use crate::dxgi; +use std::io::ErrorKind::{NotFound, TimedOut, WouldBlock}; +use std::{io, ops}; + +pub struct Capturer { + inner: dxgi::Capturer, + width: usize, + height: usize, +} + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + let width = display.width(); + let height = display.height(); + let inner = dxgi::Capturer::new(display.0, yuv)?; + Ok(Capturer { + inner, + width, + height, + }) + } + + pub fn is_gdi(&self) -> bool { + self.inner.is_gdi() + } + + pub fn set_gdi(&mut self) -> bool { + self.inner.set_gdi() + } + + pub fn cancel_gdi(&mut self) { + self.inner.cancel_gdi() + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } + + pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> { + match self.inner.frame(timeout_ms) { + Ok(frame) => Ok(Frame(frame)), + Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()), + Err(error) => Err(error), + } + } +} + +pub struct Frame<'a>(&'a [u8]); + +impl<'a> ops::Deref for Frame<'a> { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.0 + } +} + +pub struct Display(dxgi::Display); + +impl Display { + pub fn primary() -> io::Result { + match dxgi::Displays::new()?.next() { + Some(inner) => Ok(Display(inner)), + None => Err(NotFound.into()), + } + } + + pub fn all() -> io::Result> { + Ok(dxgi::Displays::new()?.map(Display).collect::>()) + } + + pub fn width(&self) -> usize { + self.0.width() as usize + } + + pub fn height(&self) -> usize { + self.0.height() as usize + } + + pub fn name(&self) -> String { + use std::ffi::OsString; + use std::os::windows::prelude::*; + OsString::from_wide(self.0.name()) + .to_string_lossy() + .to_string() + } + + pub fn is_online(&self) -> bool { + self.0.is_online() + } + + pub fn origin(&self) -> (usize, usize) { + let o = self.0.origin(); + (o.0 as usize, o.1 as usize) + } + + // to-do: not found primary display api for dxgi + pub fn is_primary(&self) -> bool { + self.name() == Self::primary().unwrap().name() + } +} diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs new file mode 100644 index 00000000000..2671c0bd057 --- /dev/null +++ b/libs/scrap/src/common/mod.rs @@ -0,0 +1,23 @@ +pub use self::codec::*; + +cfg_if! { + if #[cfg(quartz)] { + mod quartz; + pub use self::quartz::*; + } else if #[cfg(x11)] { + mod x11; + pub use self::x11::*; + } else if #[cfg(dxgi)] { + mod dxgi; + pub use self::dxgi::*; + } else { + //TODO: Fallback implementation. + } +} + +pub mod codec; +mod convert; +pub use self::convert::*; +pub const STRIDE_ALIGN: usize = 16; // commonly used in libvpx vpx_img_alloc caller + +mod vpx; diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs new file mode 100644 index 00000000000..9b1d8907d53 --- /dev/null +++ b/libs/scrap/src/common/quartz.rs @@ -0,0 +1,125 @@ +use crate::quartz; +use std::marker::PhantomData; +use std::sync::{Arc, Mutex, TryLockError}; +use std::{io, mem, ops}; + +pub struct Capturer { + inner: quartz::Capturer, + frame: Arc>>, + use_yuv: bool, + i420: Vec, +} + +impl Capturer { + pub fn new(display: Display, use_yuv: bool) -> io::Result { + let frame = Arc::new(Mutex::new(None)); + + let f = frame.clone(); + let inner = quartz::Capturer::new( + display.0, + display.width(), + display.height(), + if use_yuv { + quartz::PixelFormat::YCbCr420Full + } else { + quartz::PixelFormat::Argb8888 + }, + Default::default(), + move |inner| { + if let Ok(mut f) = f.lock() { + *f = Some(inner); + } + }, + ) + .map_err(|_| io::Error::from(io::ErrorKind::Other))?; + + Ok(Capturer { + inner, + frame, + use_yuv, + i420: Vec::new(), + }) + } + + pub fn width(&self) -> usize { + self.inner.width() + } + + pub fn height(&self) -> usize { + self.inner.height() + } + + pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> { + match self.frame.try_lock() { + Ok(mut handle) => { + let mut frame = None; + mem::swap(&mut frame, &mut handle); + + match frame { + Some(mut frame) => { + if self.use_yuv { + frame.nv12_to_i420(self.width(), self.height(), &mut self.i420); + } + Ok(Frame(frame, PhantomData)) + } + + None => Err(io::ErrorKind::WouldBlock.into()), + } + } + + Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()), + + Err(TryLockError::Poisoned(..)) => Err(io::ErrorKind::Other.into()), + } + } +} + +pub struct Frame<'a>(quartz::Frame, PhantomData<&'a [u8]>); + +impl<'a> ops::Deref for Frame<'a> { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &*self.0 + } +} + +pub struct Display(quartz::Display); + +impl Display { + pub fn primary() -> io::Result { + Ok(Display(quartz::Display::primary())) + } + + pub fn all() -> io::Result> { + Ok(quartz::Display::online() + .map_err(|_| io::Error::from(io::ErrorKind::Other))? + .into_iter() + .map(Display) + .collect()) + } + + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } + + pub fn name(&self) -> String { + self.0.id().to_string() + } + + pub fn is_online(&self) -> bool { + self.0.is_online() + } + + pub fn origin(&self) -> (usize, usize) { + let o = self.0.bounds().origin; + (o.x as usize, o.y as usize) + } + + pub fn is_primary(&self) -> bool { + self.0.is_primary() + } +} diff --git a/libs/scrap/src/common/vpx.rs b/libs/scrap/src/common/vpx.rs new file mode 100644 index 00000000000..eb655314bdf --- /dev/null +++ b/libs/scrap/src/common/vpx.rs @@ -0,0 +1,25 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(improper_ctypes)] +#![allow(dead_code)] + +impl Default for vpx_codec_enc_cfg { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for vpx_codec_ctx { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for vpx_image_t { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs")); diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs new file mode 100644 index 00000000000..99063f1bb2c --- /dev/null +++ b/libs/scrap/src/common/x11.rs @@ -0,0 +1,87 @@ +use crate::x11; +use std::{io, ops}; + +pub struct Capturer(x11::Capturer); + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + x11::Capturer::new(display.0, yuv).map(Capturer) + } + + pub fn width(&self) -> usize { + self.0.display().rect().w as usize + } + + pub fn height(&self) -> usize { + self.0.display().rect().h as usize + } + + pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> { + Ok(Frame(self.0.frame())) + } +} + +pub struct Frame<'a>(&'a [u8]); + +impl<'a> ops::Deref for Frame<'a> { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.0 + } +} + +pub struct Display(x11::Display); + +impl Display { + pub fn primary() -> io::Result { + let server = match x11::Server::default() { + Ok(server) => server, + Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), + }; + + let mut displays = x11::Server::displays(server); + let mut best = displays.next(); + if best.as_ref().map(|x| x.is_default()) == Some(false) { + best = displays.find(|x| x.is_default()).or(best); + } + + match best { + Some(best) => Ok(Display(best)), + None => Err(io::ErrorKind::NotFound.into()), + } + } + + pub fn all() -> io::Result> { + let server = match x11::Server::default() { + Ok(server) => server, + Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), + }; + + Ok(x11::Server::displays(server).map(Display).collect()) + } + + pub fn width(&self) -> usize { + self.0.rect().w as usize + } + + pub fn height(&self) -> usize { + self.0.rect().h as usize + } + + pub fn origin(&self) -> (usize, usize) { + let r = self.0.rect(); + (r.x as _, r.y as _) + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + self.0.is_default() + } + + pub fn name(&self) -> String { + "".to_owned() + } +} diff --git a/libs/scrap/src/dxgi/gdi.rs b/libs/scrap/src/dxgi/gdi.rs new file mode 100644 index 00000000000..015a42ecc1d --- /dev/null +++ b/libs/scrap/src/dxgi/gdi.rs @@ -0,0 +1,212 @@ +use std::mem::size_of; +use winapi::{ + shared::windef::{HBITMAP, HDC}, + um::wingdi::{ + BitBlt, + CreateCompatibleBitmap, + CreateCompatibleDC, + CreateDCW, + DeleteDC, + DeleteObject, + GetDIBits, + SelectObject, + BITMAPINFO, + BITMAPINFOHEADER, + BI_RGB, + DIB_RGB_COLORS, //CAPTUREBLT, + HGDI_ERROR, + RGBQUAD, + SRCCOPY, + }, +}; + +const PIXEL_WIDTH: i32 = 4; + +pub struct CapturerGDI { + screen_dc: HDC, + dc: HDC, + bmp: HBITMAP, + width: i32, + height: i32, +} + +impl CapturerGDI { + pub fn new(name: &[u16], width: i32, height: i32) -> Result> { + /* or Enumerate monitors with EnumDisplayMonitors, + https://stackoverflow.com/questions/34987695/how-can-i-get-an-hmonitor-handle-from-a-display-device-name + #[no_mangle] + pub extern "C" fn callback(m: HMONITOR, dc: HDC, rect: LPRECT, lp: LPARAM) -> BOOL {} + */ + /* + shared::windef::HMONITOR, + winuser::{GetMonitorInfoW, GetSystemMetrics, MONITORINFOEXW}, + let mut mi: MONITORINFOEXW = std::mem::MaybeUninit::uninit().assume_init(); + mi.cbSize = size_of::() as _; + if GetMonitorInfoW(m, &mut mi as *mut MONITORINFOEXW as _) == 0 { + return Err(format!("Failed to get monitor information of: {:?}", m).into()); + } + */ + unsafe { + if name.is_empty() { + return Err("Empty display name".into()); + } + let screen_dc = CreateDCW(&name[0], 0 as _, 0 as _, 0 as _); + if screen_dc.is_null() { + return Err("Failed to create dc from monitor name".into()); + } + + // Create a Windows Bitmap, and copy the bits into it + let dc = CreateCompatibleDC(screen_dc); + if dc.is_null() { + DeleteDC(screen_dc); + return Err("Can't get a Windows display".into()); + } + + let bmp = CreateCompatibleBitmap(screen_dc, width, height); + if bmp.is_null() { + DeleteDC(screen_dc); + DeleteDC(dc); + return Err("Can't create a Windows buffer".into()); + } + + let res = SelectObject(dc, bmp as _); + if res.is_null() || res == HGDI_ERROR { + DeleteDC(screen_dc); + DeleteDC(dc); + DeleteObject(bmp as _); + return Err("Can't select Windows buffer".into()); + } + Ok(Self { + screen_dc, + dc, + bmp, + width, + height, + }) + } + } + + pub fn frame(&self, data: &mut Vec) -> Result<(), Box> { + unsafe { + let res = BitBlt( + self.dc, + 0, + 0, + self.width, + self.height, + self.screen_dc, + 0, + 0, + SRCCOPY, // | CAPTUREBLT, // CAPTUREBLT enable layered window but also make cursor blinking + ); + if res == 0 { + return Err("Failed to copy screen to Windows buffer".into()); + } + + let stride = self.width * PIXEL_WIDTH; + let size: usize = (stride * self.height) as usize; + let mut data1: Vec = Vec::with_capacity(size); + data1.set_len(size); + data.resize(size, 0); + + let mut bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: size_of::() as _, + biWidth: self.width as _, + biHeight: self.height as _, + biPlanes: 1, + biBitCount: (8 * PIXEL_WIDTH) as _, + biCompression: BI_RGB, + biSizeImage: (self.width * self.height * PIXEL_WIDTH) as _, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [RGBQUAD { + rgbBlue: 0, + rgbGreen: 0, + rgbRed: 0, + rgbReserved: 0, + }], + }; + + // copy bits into Vec + let res = GetDIBits( + self.dc, + self.bmp, + 0, + self.height as _, + &mut data[0] as *mut u8 as _, + &mut bmi as _, + DIB_RGB_COLORS, + ); + if res == 0 { + return Err("GetDIBits failed".into()); + } + crate::common::ARGBMirror( + data.as_ptr(), + stride, + data1.as_mut_ptr(), + stride, + self.width, + self.height, + ); + crate::common::ARGBRotate( + data1.as_ptr(), + stride, + data.as_mut_ptr(), + stride, + self.width, + self.height, + 180, + ); + Ok(()) + } + } +} + +impl Drop for CapturerGDI { + fn drop(&mut self) { + unsafe { + DeleteDC(self.screen_dc); + DeleteDC(self.dc); + DeleteObject(self.bmp as _); + } + } +} + +#[cfg(test)] +mod tests { + use super::super::*; + use super::*; + #[test] + fn test() { + match Displays::new().unwrap().next() { + Some(d) => { + let w = d.width(); + let h = d.height(); + let c = CapturerGDI::new(d.name(), w, h).unwrap(); + let mut data = Vec::new(); + c.frame(&mut data).unwrap(); + let mut bitflipped = Vec::with_capacity((w * h * 4) as usize); + for y in 0..h { + for x in 0..w { + let i = (w * 4 * y + 4 * x) as usize; + bitflipped.extend_from_slice(&[data[i + 2], data[i + 1], data[i], 255]); + } + } + repng::encode( + std::fs::File::create("gdi_screen.png").unwrap(), + d.width() as u32, + d.height() as u32, + &bitflipped, + ) + .unwrap(); + } + _ => { + assert!(false); + } + } + } +} diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs new file mode 100644 index 00000000000..08f4a2764ce --- /dev/null +++ b/libs/scrap/src/dxgi/mod.rs @@ -0,0 +1,539 @@ +use std::{io, mem, ptr, slice}; +pub mod gdi; +pub use gdi::CapturerGDI; + +use winapi::{ + shared::dxgi::{ + CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIResource, IDXGISurface, + IID_IDXGIFactory1, IID_IDXGISurface, DXGI_MAP_READ, DXGI_OUTPUT_DESC, + DXGI_RESOURCE_PRIORITY_MAXIMUM, + }, + shared::dxgi1_2::IDXGIOutputDuplication, + // shared::dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE}, + shared::dxgi1_2::{IDXGIOutput1, IID_IDXGIOutput1}, + shared::dxgitype::DXGI_MODE_ROTATION, + shared::minwindef::{TRUE, UINT}, + shared::ntdef::LONG, + shared::windef::HMONITOR, + shared::winerror::{ + DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_INVALID_CALL, DXGI_ERROR_NOT_CURRENTLY_AVAILABLE, + DXGI_ERROR_SESSION_DISCONNECTED, DXGI_ERROR_UNSUPPORTED, DXGI_ERROR_WAIT_TIMEOUT, + E_ACCESSDENIED, E_INVALIDARG, S_OK, + }, + um::d3d11::{ + D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D, IID_ID3D11Texture2D, + D3D11_CPU_ACCESS_READ, D3D11_SDK_VERSION, D3D11_USAGE_STAGING, + }, + um::d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, + um::winnt::HRESULT, +}; + +//TODO: Split up into files. + +pub struct Capturer { + device: *mut ID3D11Device, + display: Display, + context: *mut ID3D11DeviceContext, + duplication: *mut IDXGIOutputDuplication, + fastlane: bool, + surface: *mut IDXGISurface, + data: *const u8, + len: usize, + width: usize, + height: usize, + use_yuv: bool, + yuv: Vec, + gdi_capturer: Option, + gdi_buffer: Vec, +} + +impl Capturer { + pub fn new(display: Display, use_yuv: bool) -> io::Result { + let mut device = ptr::null_mut(); + let mut context = ptr::null_mut(); + let mut duplication = ptr::null_mut(); + let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() }; + let mut gdi_capturer = None; + + let mut res = wrap_hresult(unsafe { + D3D11CreateDevice( + display.adapter as *mut _, + D3D_DRIVER_TYPE_UNKNOWN, + ptr::null_mut(), // No software rasterizer. + 0, // No device flags. + ptr::null_mut(), // Feature levels. + 0, // Feature levels' length. + D3D11_SDK_VERSION, + &mut device, + ptr::null_mut(), + &mut context, + ) + }); + + if res.is_err() { + gdi_capturer = display.create_gdi(); + println!("Fallback to GDI"); + if gdi_capturer.is_some() { + res = Ok(()); + } + } else { + res = wrap_hresult(unsafe { + let hres = (*display.inner).DuplicateOutput(device as *mut _, &mut duplication); + if hres != S_OK { + gdi_capturer = display.create_gdi(); + println!("Fallback to GDI"); + if gdi_capturer.is_some() { + S_OK + } else { + hres + } + } else { + hres + } + // NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk + + // also try high version DXGI for better performance, e.g. + // https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/dxgi-1-2-improvements + // dxgi-1-6 may too high, only support win10 (2018) + // https://docs.microsoft.com/zh-cn/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + // DXGI_FORMAT_420_OPAQUE + // IDXGIOutputDuplication::GetFrameDirtyRects and IDXGIOutputDuplication::GetFrameMoveRects + // can help us update screen incrementally + + /* // not supported on my PC, try in the future + let format : Vec = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE]; + (*display.inner).DuplicateOutput1( + device as *mut _, + 0 as UINT, + 2 as UINT, + format.as_ptr(), + &mut duplication + ) + */ + + // if above not work, I think below should not work either, try later + // https://developer.nvidia.com/capture-sdk deprecated + // examples using directx + nvideo sdk for GPU-accelerated video encoding/decoding + // https://github.com/NVIDIA/video-sdk-samples + }); + } + + if let Err(err) = res { + unsafe { + if !device.is_null() { + (*device).Release(); + } + if !context.is_null() { + (*context).Release(); + } + } + return Err(err); + } + + if !duplication.is_null() { + unsafe { + (*duplication).GetDesc(&mut desc); + } + } + + Ok(Capturer { + device, + context, + duplication, + fastlane: desc.DesktopImageInSystemMemory == TRUE, + surface: ptr::null_mut(), + width: display.width() as usize, + height: display.height() as usize, + display, + data: ptr::null(), + len: 0, + use_yuv, + yuv: Vec::new(), + gdi_capturer, + gdi_buffer: Vec::new(), + }) + } + + pub fn is_gdi(&self) -> bool { + self.gdi_capturer.is_some() + } + + pub fn set_gdi(&mut self) -> bool { + self.gdi_capturer = self.display.create_gdi(); + self.is_gdi() + } + + pub fn cancel_gdi(&mut self) { + self.gdi_buffer = Vec::new(); + self.gdi_capturer.take(); + } + + unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<()> { + let mut frame = ptr::null_mut(); + let mut info = mem::MaybeUninit::uninit().assume_init(); + self.data = ptr::null(); + + wrap_hresult((*self.duplication).AcquireNextFrame(timeout, &mut info, &mut frame))?; + + if *info.LastPresentTime.QuadPart() == 0 { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + + if self.fastlane { + let mut rect = mem::MaybeUninit::uninit().assume_init(); + let res = wrap_hresult((*self.duplication).MapDesktopSurface(&mut rect)); + + (*frame).Release(); + + if let Err(err) = res { + Err(err) + } else { + self.data = rect.pBits; + self.len = self.height * rect.Pitch as usize; + Ok(()) + } + } else { + self.surface = ptr::null_mut(); + self.surface = self.ohgodwhat(frame)?; + + let mut rect = mem::MaybeUninit::uninit().assume_init(); + wrap_hresult((*self.surface).Map(&mut rect, DXGI_MAP_READ))?; + + self.data = rect.pBits; + self.len = self.height * rect.Pitch as usize; + Ok(()) + } + } + + // copy from GPU memory to system memory + unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<*mut IDXGISurface> { + let mut texture: *mut ID3D11Texture2D = ptr::null_mut(); + (*frame).QueryInterface( + &IID_ID3D11Texture2D, + &mut texture as *mut *mut _ as *mut *mut _, + ); + + let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); + (*texture).GetDesc(&mut texture_desc); + + texture_desc.Usage = D3D11_USAGE_STAGING; + texture_desc.BindFlags = 0; + texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + texture_desc.MiscFlags = 0; + + let mut readable = ptr::null_mut(); + let res = wrap_hresult((*self.device).CreateTexture2D( + &mut texture_desc, + ptr::null(), + &mut readable, + )); + + if let Err(err) = res { + (*frame).Release(); + (*texture).Release(); + (*readable).Release(); + Err(err) + } else { + (*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM); + + let mut surface = ptr::null_mut(); + (*readable).QueryInterface( + &IID_IDXGISurface, + &mut surface as *mut *mut _ as *mut *mut _, + ); + + (*self.context).CopyResource(readable as *mut _, texture as *mut _); + + (*frame).Release(); + (*texture).Release(); + (*readable).Release(); + Ok(surface) + } + } + + pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<&'a [u8]> { + unsafe { + // Release last frame. + // No error checking needed because we don't care. + // None of the errors crash anyway. + let result = { + if let Some(gdi_capturer) = &self.gdi_capturer { + match gdi_capturer.frame(&mut self.gdi_buffer) { + Ok(_) => &self.gdi_buffer, + Err(err) => { + return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); + } + } + } else { + if self.fastlane { + (*self.duplication).UnMapDesktopSurface(); + } else { + if !self.surface.is_null() { + (*self.surface).Unmap(); + (*self.surface).Release(); + self.surface = ptr::null_mut(); + } + } + + (*self.duplication).ReleaseFrame(); + self.load_frame(timeout)?; + slice::from_raw_parts(self.data, self.len) + } + }; + Ok({ + if self.use_yuv { + crate::common::bgra_to_i420( + self.width as usize, + self.height as usize, + &result, + &mut self.yuv, + ); + &self.yuv[..] + } else { + result + } + }) + } + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + if !self.surface.is_null() { + (*self.surface).Unmap(); + (*self.surface).Release(); + } + if !self.duplication.is_null() { + (*self.duplication).Release(); + } + if !self.device.is_null() { + (*self.device).Release(); + } + if !self.context.is_null() { + (*self.context).Release(); + } + } + } +} + +pub struct Displays { + factory: *mut IDXGIFactory1, + adapter: *mut IDXGIAdapter1, + /// Index of the CURRENT adapter. + nadapter: UINT, + /// Index of the NEXT display to fetch. + ndisplay: UINT, +} + +impl Displays { + pub fn new() -> io::Result { + let mut factory = ptr::null_mut(); + wrap_hresult(unsafe { CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory) })?; + + let factory = factory as *mut IDXGIFactory1; + let mut adapter = ptr::null_mut(); + unsafe { + // On error, our adapter is null, so it's fine. + (*factory).EnumAdapters1(0, &mut adapter); + }; + + Ok(Displays { + factory, + adapter, + nadapter: 0, + ndisplay: 0, + }) + } + + // No Adapter => Some(None) + // Non-Empty Adapter => Some(Some(OUTPUT)) + // End of Adapter => None + fn read_and_invalidate(&mut self) -> Option> { + // If there is no adapter, there is nothing left for us to do. + + if self.adapter.is_null() { + return Some(None); + } + + // Otherwise, we get the next output of the current adapter. + + let output = unsafe { + let mut output = ptr::null_mut(); + (*self.adapter).EnumOutputs(self.ndisplay, &mut output); + output + }; + + // If the current adapter is done, we free it. + // We return None so the caller gets the next adapter and tries again. + + if output.is_null() { + unsafe { + (*self.adapter).Release(); + self.adapter = ptr::null_mut(); + } + return None; + } + + // Advance to the next display. + + self.ndisplay += 1; + + // We get the display's details. + + let desc = unsafe { + let mut desc = mem::MaybeUninit::uninit().assume_init(); + (*output).GetDesc(&mut desc); + desc + }; + + // We cast it up to the version needed for desktop duplication. + + let mut inner: *mut IDXGIOutput1 = ptr::null_mut(); + unsafe { + (*output).QueryInterface(&IID_IDXGIOutput1, &mut inner as *mut *mut _ as *mut *mut _); + (*output).Release(); + } + + // If it's null, we have an error. + // So we act like the adapter is done. + + if inner.is_null() { + unsafe { + (*self.adapter).Release(); + self.adapter = ptr::null_mut(); + } + return None; + } + + unsafe { + (*self.adapter).AddRef(); + } + + Some(Some(Display { + inner, + adapter: self.adapter, + desc, + })) + } +} + +impl Iterator for Displays { + type Item = Display; + fn next(&mut self) -> Option { + if let Some(res) = self.read_and_invalidate() { + res + } else { + // We need to replace the adapter. + + self.ndisplay = 0; + self.nadapter += 1; + + self.adapter = unsafe { + let mut adapter = ptr::null_mut(); + (*self.factory).EnumAdapters1(self.nadapter, &mut adapter); + adapter + }; + + if let Some(res) = self.read_and_invalidate() { + res + } else { + // All subsequent adapters will also be empty. + None + } + } + } +} + +impl Drop for Displays { + fn drop(&mut self) { + unsafe { + (*self.factory).Release(); + if !self.adapter.is_null() { + (*self.adapter).Release(); + } + } + } +} + +pub struct Display { + inner: *mut IDXGIOutput1, + adapter: *mut IDXGIAdapter1, + desc: DXGI_OUTPUT_DESC, +} + +impl Display { + pub fn width(&self) -> LONG { + self.desc.DesktopCoordinates.right - self.desc.DesktopCoordinates.left + } + + pub fn height(&self) -> LONG { + self.desc.DesktopCoordinates.bottom - self.desc.DesktopCoordinates.top + } + + pub fn attached_to_desktop(&self) -> bool { + self.desc.AttachedToDesktop != 0 + } + + pub fn rotation(&self) -> DXGI_MODE_ROTATION { + self.desc.Rotation + } + + fn create_gdi(&self) -> Option { + if let Ok(res) = CapturerGDI::new(self.name(), self.width(), self.height()) { + Some(res) + } else { + None + } + } + + pub fn hmonitor(&self) -> HMONITOR { + self.desc.Monitor + } + + pub fn name(&self) -> &[u16] { + let s = &self.desc.DeviceName; + let i = s.iter().position(|&x| x == 0).unwrap_or(s.len()); + &s[..i] + } + + pub fn is_online(&self) -> bool { + self.desc.AttachedToDesktop != 0 + } + + pub fn origin(&self) -> (LONG, LONG) { + ( + self.desc.DesktopCoordinates.left, + self.desc.DesktopCoordinates.top, + ) + } +} + +impl Drop for Display { + fn drop(&mut self) { + unsafe { + (*self.inner).Release(); + (*self.adapter).Release(); + } + } +} + +fn wrap_hresult(x: HRESULT) -> io::Result<()> { + use std::io::ErrorKind::*; + Err((match x { + S_OK => return Ok(()), + DXGI_ERROR_ACCESS_LOST => ConnectionReset, + DXGI_ERROR_WAIT_TIMEOUT => TimedOut, + DXGI_ERROR_INVALID_CALL => InvalidData, + E_ACCESSDENIED => PermissionDenied, + DXGI_ERROR_UNSUPPORTED => ConnectionRefused, + DXGI_ERROR_NOT_CURRENTLY_AVAILABLE => Interrupted, + DXGI_ERROR_SESSION_DISCONNECTED => ConnectionAborted, + E_INVALIDARG => InvalidInput, + _ => { + // 0x8000ffff https://www.auslogics.com/en/articles/windows-10-update-error-0x8000ffff-fixed/ + return Err(io::Error::new(Other, format!("Error code: {:#X}", x))); + } + }) + .into()) +} diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs new file mode 100644 index 00000000000..6274a0801c7 --- /dev/null +++ b/libs/scrap/src/lib.rs @@ -0,0 +1,20 @@ +#[cfg(quartz)] +extern crate block; +#[macro_use] +extern crate cfg_if; +pub extern crate libc; +#[cfg(dxgi)] +extern crate winapi; + +pub use common::*; + +#[cfg(quartz)] +pub mod quartz; + +#[cfg(x11)] +pub mod x11; + +#[cfg(dxgi)] +pub mod dxgi; + +mod common; diff --git a/libs/scrap/src/quartz/capturer.rs b/libs/scrap/src/quartz/capturer.rs new file mode 100644 index 00000000000..5be55ea2266 --- /dev/null +++ b/libs/scrap/src/quartz/capturer.rs @@ -0,0 +1,111 @@ +use std::ptr; + +use block::{Block, ConcreteBlock}; +use libc::c_void; +use std::sync::{Arc, Mutex}; + +use super::config::Config; +use super::display::Display; +use super::ffi::*; +use super::frame::Frame; + +pub struct Capturer { + stream: CGDisplayStreamRef, + queue: DispatchQueue, + + width: usize, + height: usize, + format: PixelFormat, + display: Display, + stopped: Arc>, +} + +impl Capturer { + pub fn new( + display: Display, + width: usize, + height: usize, + format: PixelFormat, + config: Config, + handler: F, + ) -> Result { + let stopped = Arc::new(Mutex::new(false)); + let cloned_stopped = stopped.clone(); + let handler: FrameAvailableHandler = ConcreteBlock::new(move |status, _, surface, _| { + use self::CGDisplayStreamFrameStatus::*; + if status == Stopped { + let mut lock = cloned_stopped.lock().unwrap(); + *lock = true; + return; + } + if status == FrameComplete { + handler(unsafe { Frame::new(surface) }); + } + }) + .copy(); + + let queue = unsafe { + dispatch_queue_create( + b"quadrupleslap.scrap\0".as_ptr() as *const i8, + ptr::null_mut(), + ) + }; + + let stream = unsafe { + let config = config.build(); + let stream = CGDisplayStreamCreateWithDispatchQueue( + display.id(), + width, + height, + format, + config, + queue, + &*handler as *const Block<_, _> as *const c_void, + ); + CFRelease(config); + stream + }; + + match unsafe { CGDisplayStreamStart(stream) } { + CGError::Success => Ok(Capturer { + stream, + queue, + width, + height, + format, + display, + stopped, + }), + x => Err(x), + } + } + + pub fn width(&self) -> usize { + self.width + } + pub fn height(&self) -> usize { + self.height + } + pub fn format(&self) -> PixelFormat { + self.format + } + pub fn display(&self) -> Display { + self.display + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + let _ = CGDisplayStreamStop(self.stream); + loop { + if *self.stopped.lock().unwrap() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(30)); + } + CFRelease(self.stream); + dispatch_release(self.queue); + } + } +} diff --git a/libs/scrap/src/quartz/config.rs b/libs/scrap/src/quartz/config.rs new file mode 100644 index 00000000000..11a6d5fc0b8 --- /dev/null +++ b/libs/scrap/src/quartz/config.rs @@ -0,0 +1,75 @@ +use std::ptr; + +use libc::c_void; + +use super::ffi::*; + +//TODO: Color space, YCbCr matrix. +pub struct Config { + /// Whether the cursor is visible. + pub cursor: bool, + /// Whether it should letterbox or stretch. + pub letterbox: bool, + /// Minimum seconds per frame. + pub throttle: f64, + /// How many frames are allocated. + /// 3 is the recommended value. + /// 8 is the maximum value. + pub queue_length: i8, +} + +impl Config { + /// Don't forget to CFRelease this! + pub fn build(self) -> CFDictionaryRef { + unsafe { + let throttle = CFNumberCreate( + ptr::null_mut(), + CFNumberType::Float64, + &self.throttle as *const _ as *const c_void, + ); + let queue_length = CFNumberCreate( + ptr::null_mut(), + CFNumberType::SInt8, + &self.queue_length as *const _ as *const c_void, + ); + + let keys: [CFStringRef; 4] = [ + kCGDisplayStreamShowCursor, + kCGDisplayStreamPreserveAspectRatio, + kCGDisplayStreamMinimumFrameTime, + kCGDisplayStreamQueueDepth, + ]; + let values: [*mut c_void; 4] = [ + cfbool(self.cursor), + cfbool(self.letterbox), + throttle, + queue_length, + ]; + + let res = CFDictionaryCreate( + ptr::null_mut(), + keys.as_ptr(), + values.as_ptr(), + 4, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + + CFRelease(throttle); + CFRelease(queue_length); + + res + } + } +} + +impl Default for Config { + fn default() -> Config { + Config { + cursor: false, + letterbox: true, + throttle: 0.0, + queue_length: 3, + } + } +} diff --git a/libs/scrap/src/quartz/display.rs b/libs/scrap/src/quartz/display.rs new file mode 100644 index 00000000000..ff96b2c1c3e --- /dev/null +++ b/libs/scrap/src/quartz/display.rs @@ -0,0 +1,63 @@ +use std::mem; + +use super::ffi::*; + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[repr(C)] +pub struct Display(u32); + +impl Display { + pub fn primary() -> Display { + Display(unsafe { CGMainDisplayID() }) + } + + pub fn online() -> Result, CGError> { + unsafe { + let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); + let mut len: u32 = 0; + + match CGGetOnlineDisplayList(16, arr.as_mut_ptr(), &mut len) { + CGError::Success => (), + x => return Err(x), + } + + let mut res = Vec::with_capacity(16); + for i in 0..len as usize { + res.push(Display(*arr.get_unchecked(i))); + } + Ok(res) + } + } + + pub fn id(self) -> u32 { + self.0 + } + + pub fn width(self) -> usize { + unsafe { CGDisplayPixelsWide(self.0) } + } + + pub fn height(self) -> usize { + unsafe { CGDisplayPixelsHigh(self.0) } + } + + pub fn is_builtin(self) -> bool { + unsafe { CGDisplayIsBuiltin(self.0) != 0 } + } + + pub fn is_primary(self) -> bool { + unsafe { CGDisplayIsMain(self.0) != 0 } + } + + pub fn is_active(self) -> bool { + unsafe { CGDisplayIsActive(self.0) != 0 } + } + + pub fn is_online(self) -> bool { + unsafe { CGDisplayIsOnline(self.0) != 0 } + } + + pub fn bounds(self) -> CGRect { + unsafe { CGDisplayBounds(self.0) } + } +} diff --git a/libs/scrap/src/quartz/ffi.rs b/libs/scrap/src/quartz/ffi.rs new file mode 100644 index 00000000000..ca39c0a618d --- /dev/null +++ b/libs/scrap/src/quartz/ffi.rs @@ -0,0 +1,240 @@ +#![allow(dead_code)] + +use block::RcBlock; +use libc::c_void; + +pub type CGDisplayStreamRef = *mut c_void; +pub type CFDictionaryRef = *mut c_void; +pub type CFBooleanRef = *mut c_void; +pub type CFNumberRef = *mut c_void; +pub type CFStringRef = *mut c_void; +pub type CGDisplayStreamUpdateRef = *mut c_void; +pub type IOSurfaceRef = *mut c_void; +pub type DispatchQueue = *mut c_void; +pub type DispatchQueueAttr = *mut c_void; +pub type CFAllocatorRef = *mut c_void; + +#[repr(C)] +pub struct CFDictionaryKeyCallBacks { + callbacks: [usize; 5], + version: i32, +} + +#[repr(C)] +pub struct CFDictionaryValueCallBacks { + callbacks: [usize; 4], + version: i32, +} + +macro_rules! pixel_format { + ($a:expr, $b:expr, $c:expr, $d:expr) => { + ($a as i32) << 24 | ($b as i32) << 16 | ($c as i32) << 8 | ($d as i32) + }; +} + +pub const SURFACE_LOCK_READ_ONLY: u32 = 0x0000_0001; +pub const SURFACE_LOCK_AVOID_SYNC: u32 = 0x0000_0002; + +pub fn cfbool(x: bool) -> CFBooleanRef { + unsafe { + if x { + kCFBooleanTrue + } else { + kCFBooleanFalse + } + } +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum CGDisplayStreamFrameStatus { + /// A new frame was generated. + FrameComplete = 0, + /// A new frame was not generated because the display did not change. + FrameIdle = 1, + /// A new frame was not generated because the display has gone blank. + FrameBlank = 2, + /// The display stream was stopped. + Stopped = 3, + #[doc(hidden)] + __Nonexhaustive, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum CFNumberType { + /* Fixed-width types */ + SInt8 = 1, + SInt16 = 2, + SInt32 = 3, + SInt64 = 4, + Float32 = 5, + Float64 = 6, + /* 64-bit IEEE 754 */ + /* Basic C types */ + Char = 7, + Short = 8, + Int = 9, + Long = 10, + LongLong = 11, + Float = 12, + Double = 13, + /* Other */ + CFIndex = 14, + NSInteger = 15, + CGFloat = 16, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[must_use] +pub enum CGError { + Success = 0, + Failure = 1000, + IllegalArgument = 1001, + InvalidConnection = 1002, + InvalidContext = 1003, + CannotComplete = 1004, + NotImplemented = 1006, + RangeCheck = 1007, + TypeCheck = 1008, + InvalidOperation = 1010, + NoneAvailable = 1011, + #[doc(hidden)] + __Nonexhaustive, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum PixelFormat { + /// Packed Little Endian ARGB8888 + Argb8888 = pixel_format!('B', 'G', 'R', 'A'), + /// Packed Little Endian ARGB2101010 + Argb2101010 = pixel_format!('l', '1', '0', 'r'), + /// 2-plane "video" range YCbCr 4:2:0 + YCbCr420Video = pixel_format!('4', '2', '0', 'v'), + /// 2-plane "full" range YCbCr 4:2:0 + YCbCr420Full = pixel_format!('4', '2', '0', 'f'), + #[doc(hidden)] + __Nonexhaustive, +} + +pub type CGDisplayStreamFrameAvailableHandler = *const c_void; + +pub type FrameAvailableHandler = RcBlock< + ( + CGDisplayStreamFrameStatus, // status + u64, // displayTime + IOSurfaceRef, // frameSurface + CGDisplayStreamUpdateRef, // updateRef + ), + (), +>; + +#[cfg(target_pointer_width = "64")] +pub type CGFloat = f64; +#[cfg(not(target_pointer_width = "64"))] +pub type CGFloat = f32; +#[repr(C)] +pub struct CGPoint { + pub x: CGFloat, + pub y: CGFloat, +} +#[repr(C)] +pub struct CGSize { + pub width: CGFloat, + pub height: CGFloat, +} +#[repr(C)] +pub struct CGRect { + pub origin: CGPoint, + pub size: CGSize, +} + +#[link(name = "System", kind = "dylib")] +#[link(name = "CoreGraphics", kind = "framework")] +#[link(name = "CoreFoundation", kind = "framework")] +#[link(name = "IOSurface", kind = "framework")] +extern "C" { + // CoreGraphics + + pub static kCGDisplayStreamShowCursor: CFStringRef; + pub static kCGDisplayStreamPreserveAspectRatio: CFStringRef; + pub static kCGDisplayStreamMinimumFrameTime: CFStringRef; + pub static kCGDisplayStreamQueueDepth: CFStringRef; + + pub fn CGDisplayStreamCreateWithDispatchQueue( + display: u32, + output_width: usize, + output_height: usize, + pixel_format: PixelFormat, + properties: CFDictionaryRef, + queue: DispatchQueue, + handler: CGDisplayStreamFrameAvailableHandler, + ) -> CGDisplayStreamRef; + + pub fn CGDisplayStreamStart(displayStream: CGDisplayStreamRef) -> CGError; + + pub fn CGDisplayStreamStop(displayStream: CGDisplayStreamRef) -> CGError; + + pub fn CGMainDisplayID() -> u32; + pub fn CGDisplayPixelsWide(display: u32) -> usize; + pub fn CGDisplayPixelsHigh(display: u32) -> usize; + + pub fn CGGetOnlineDisplayList( + max_displays: u32, + online_displays: *mut u32, + display_count: *mut u32, + ) -> CGError; + + pub fn CGDisplayIsBuiltin(display: u32) -> i32; + pub fn CGDisplayIsMain(display: u32) -> i32; + pub fn CGDisplayIsActive(display: u32) -> i32; + pub fn CGDisplayIsOnline(display: u32) -> i32; + + pub fn CGDisplayBounds(display: u32) -> CGRect; + + // IOSurface + + pub fn IOSurfaceGetAllocSize(buffer: IOSurfaceRef) -> usize; + pub fn IOSurfaceGetBaseAddress(buffer: IOSurfaceRef) -> *mut c_void; + pub fn IOSurfaceIncrementUseCount(buffer: IOSurfaceRef); + pub fn IOSurfaceDecrementUseCount(buffer: IOSurfaceRef); + pub fn IOSurfaceLock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; + pub fn IOSurfaceUnlock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; + pub fn IOSurfaceGetBaseAddressOfPlane(buffer: IOSurfaceRef, index: usize) -> *mut c_void; + pub fn IOSurfaceGetBytesPerRowOfPlane(buffer: IOSurfaceRef, index: usize) -> usize; + + // Dispatch + + pub fn dispatch_queue_create(label: *const i8, attr: DispatchQueueAttr) -> DispatchQueue; + + pub fn dispatch_release(object: DispatchQueue); + + // Core Foundation + + pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks; + pub static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks; + + // EVEN THE BOOLEANS ARE REFERENCES. + pub static kCFBooleanTrue: CFBooleanRef; + pub static kCFBooleanFalse: CFBooleanRef; + + pub fn CFNumberCreate( + allocator: CFAllocatorRef, + theType: CFNumberType, + valuePtr: *const c_void, + ) -> CFNumberRef; + + pub fn CFDictionaryCreate( + allocator: CFAllocatorRef, + keys: *const *mut c_void, + values: *const *mut c_void, + numValues: i64, + keyCallBacks: *const CFDictionaryKeyCallBacks, + valueCallBacks: *const CFDictionaryValueCallBacks, + ) -> CFDictionaryRef; + + pub fn CFRetain(cf: *const c_void); + pub fn CFRelease(cf: *const c_void); +} diff --git a/libs/scrap/src/quartz/frame.rs b/libs/scrap/src/quartz/frame.rs new file mode 100644 index 00000000000..61dd4b50dd3 --- /dev/null +++ b/libs/scrap/src/quartz/frame.rs @@ -0,0 +1,79 @@ +use std::{ops, ptr, slice}; + +use super::ffi::*; + +pub struct Frame { + surface: IOSurfaceRef, + inner: &'static [u8], + i420: *mut u8, + i420_len: usize, +} + +impl Frame { + pub unsafe fn new(surface: IOSurfaceRef) -> Frame { + CFRetain(surface); + IOSurfaceIncrementUseCount(surface); + + IOSurfaceLock(surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); + + let inner = slice::from_raw_parts( + IOSurfaceGetBaseAddress(surface) as *const u8, + IOSurfaceGetAllocSize(surface), + ); + + Frame { + surface, + inner, + i420: ptr::null_mut(), + i420_len: 0, + } + } + + pub fn nv12_to_i420<'a>(&'a mut self, w: usize, h: usize, i420: &'a mut Vec) { + if self.inner.is_empty() { + return; + } + unsafe { + let plane0 = IOSurfaceGetBaseAddressOfPlane(self.surface, 0); + let stride0 = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0); + let plane1 = IOSurfaceGetBaseAddressOfPlane(self.surface, 1); + let stride1 = IOSurfaceGetBytesPerRowOfPlane(self.surface, 1); + crate::common::nv12_to_i420( + plane0 as _, + stride0 as _, + plane1 as _, + stride1 as _, + w, + h, + i420, + ); + self.i420 = i420.as_mut_ptr() as _; + self.i420_len = i420.len(); + } + } +} + +impl ops::Deref for Frame { + type Target = [u8]; + fn deref<'a>(&'a self) -> &'a [u8] { + if self.i420.is_null() { + self.inner + } else { + unsafe { + let inner = slice::from_raw_parts(self.i420 as *const u8, self.i420_len); + inner + } + } + } +} + +impl Drop for Frame { + fn drop(&mut self) { + unsafe { + IOSurfaceUnlock(self.surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); + + IOSurfaceDecrementUseCount(self.surface); + CFRelease(self.surface); + } + } +} diff --git a/libs/scrap/src/quartz/mod.rs b/libs/scrap/src/quartz/mod.rs new file mode 100644 index 00000000000..94488e0aab4 --- /dev/null +++ b/libs/scrap/src/quartz/mod.rs @@ -0,0 +1,11 @@ +pub use self::capturer::Capturer; +pub use self::config::Config; +pub use self::display::Display; +pub use self::ffi::{CGError, PixelFormat}; +pub use self::frame::Frame; + +mod capturer; +mod config; +mod display; +pub mod ffi; +mod frame; diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs new file mode 100644 index 00000000000..d989660f9df --- /dev/null +++ b/libs/scrap/src/x11/capturer.rs @@ -0,0 +1,123 @@ +use std::{io, ptr, slice}; + +use libc; + +use super::ffi::*; +use super::Display; + +pub struct Capturer { + display: Display, + shmid: i32, + xcbid: u32, + buffer: *const u8, + + size: usize, + use_yuv: bool, + yuv: Vec, +} + +impl Capturer { + pub fn new(display: Display, use_yuv: bool) -> io::Result { + // Calculate dimensions. + + let pixel_width = 4; + let rect = display.rect(); + let size = (rect.w as usize) * (rect.h as usize) * pixel_width; + + // Create a shared memory segment. + + let shmid = unsafe { + libc::shmget( + libc::IPC_PRIVATE, + size, + // Everyone can do anything. + libc::IPC_CREAT | 0o777, + ) + }; + + if shmid == -1 { + return Err(io::Error::last_os_error()); + } + + // Attach the segment to a readable address. + + let buffer = unsafe { libc::shmat(shmid, ptr::null(), libc::SHM_RDONLY) } as *mut u8; + + if buffer as isize == -1 { + return Err(io::Error::last_os_error()); + } + + // Attach the segment to XCB. + + let server = display.server().raw(); + let xcbid = unsafe { xcb_generate_id(server) }; + unsafe { + xcb_shm_attach( + server, + xcbid, + shmid as u32, + 0, // False, i.e. not read-only. + ); + } + + let c = Capturer { + display, + shmid, + xcbid, + buffer, + size, + use_yuv, + yuv: Vec::new(), + }; + Ok(c) + } + + pub fn display(&self) -> &Display { + &self.display + } + + fn get_image(&self) { + let rect = self.display.rect(); + unsafe { + let request = xcb_shm_get_image_unchecked( + self.display.server().raw(), + self.display.root(), + rect.x, + rect.y, + rect.w, + rect.h, + !0, + XCB_IMAGE_FORMAT_Z_PIXMAP, + self.xcbid, + 0, + ); + let response = + xcb_shm_get_image_reply(self.display.server().raw(), request, ptr::null_mut()); + libc::free(response as *mut _); + } + } + + pub fn frame<'b>(&'b mut self) -> &'b [u8] { + self.get_image(); + let result = unsafe { slice::from_raw_parts(self.buffer, self.size) }; + if self.use_yuv { + crate::common::bgra_to_i420(self.display.w(), self.display.h(), &result, &mut self.yuv); + &self.yuv[..] + } else { + result + } + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + // Detach segment from XCB. + xcb_shm_detach(self.display.server().raw(), self.xcbid); + // Detach segment from our space. + libc::shmdt(self.buffer as *mut _); + // Destroy the shared memory segment. + libc::shmctl(self.shmid, libc::IPC_RMID, ptr::null_mut()); + } + } +} diff --git a/libs/scrap/src/x11/display.rs b/libs/scrap/src/x11/display.rs new file mode 100644 index 00000000000..0c5ba503553 --- /dev/null +++ b/libs/scrap/src/x11/display.rs @@ -0,0 +1,55 @@ +use std::rc::Rc; + +use super::ffi::*; +use super::Server; + +#[derive(Debug)] +pub struct Display { + server: Rc, + default: bool, + rect: Rect, + root: xcb_window_t, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct Rect { + pub x: i16, + pub y: i16, + pub w: u16, + pub h: u16, +} + +impl Display { + pub unsafe fn new( + server: Rc, + default: bool, + rect: Rect, + root: xcb_window_t, + ) -> Display { + Display { + server, + default, + rect, + root, + } + } + + pub fn server(&self) -> &Rc { + &self.server + } + pub fn is_default(&self) -> bool { + self.default + } + pub fn rect(&self) -> Rect { + self.rect + } + pub fn w(&self) -> usize { + self.rect.w as _ + } + pub fn h(&self) -> usize { + self.rect.h as _ + } + pub fn root(&self) -> xcb_window_t { + self.root + } +} diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs new file mode 100644 index 00000000000..5df5c46a8f9 --- /dev/null +++ b/libs/scrap/src/x11/ffi.rs @@ -0,0 +1,205 @@ +#![allow(non_camel_case_types)] + +use libc::c_void; + +#[link(name = "xcb")] +#[link(name = "xcb-shm")] +#[link(name = "xcb-randr")] +extern "C" { + pub fn xcb_connect(displayname: *const i8, screenp: *mut i32) -> *mut xcb_connection_t; + + pub fn xcb_disconnect(c: *mut xcb_connection_t); + + pub fn xcb_connection_has_error(c: *mut xcb_connection_t) -> i32; + + pub fn xcb_get_setup(c: *mut xcb_connection_t) -> *const xcb_setup_t; + + pub fn xcb_setup_roots_iterator(r: *const xcb_setup_t) -> xcb_screen_iterator_t; + + pub fn xcb_screen_next(i: *mut xcb_screen_iterator_t); + + pub fn xcb_generate_id(c: *mut xcb_connection_t) -> u32; + + pub fn xcb_shm_attach( + c: *mut xcb_connection_t, + shmseg: xcb_shm_seg_t, + shmid: u32, + read_only: u8, + ) -> xcb_void_cookie_t; + + pub fn xcb_shm_detach(c: *mut xcb_connection_t, shmseg: xcb_shm_seg_t) -> xcb_void_cookie_t; + + pub fn xcb_shm_get_image_unchecked( + c: *mut xcb_connection_t, + drawable: xcb_drawable_t, + x: i16, + y: i16, + width: u16, + height: u16, + plane_mask: u32, + format: u8, + shmseg: xcb_shm_seg_t, + offset: u32, + ) -> xcb_shm_get_image_cookie_t; + + pub fn xcb_shm_get_image_reply( + c: *mut xcb_connection_t, + cookie: xcb_shm_get_image_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_shm_get_image_reply_t; + + pub fn xcb_randr_get_monitors_unchecked( + c: *mut xcb_connection_t, + window: xcb_window_t, + get_active: u8, + ) -> xcb_randr_get_monitors_cookie_t; + + pub fn xcb_randr_get_monitors_reply( + c: *mut xcb_connection_t, + cookie: xcb_randr_get_monitors_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_randr_get_monitors_reply_t; + + pub fn xcb_randr_get_monitors_monitors_iterator( + r: *const xcb_randr_get_monitors_reply_t, + ) -> xcb_randr_monitor_info_iterator_t; + + pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); +} + +pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; + +pub type xcb_atom_t = u32; +pub type xcb_connection_t = c_void; +pub type xcb_window_t = u32; +pub type xcb_keycode_t = u8; +pub type xcb_visualid_t = u32; +pub type xcb_timestamp_t = u32; +pub type xcb_colormap_t = u32; +pub type xcb_shm_seg_t = u32; +pub type xcb_drawable_t = u32; + +#[repr(C)] +pub struct xcb_setup_t { + pub status: u8, + pub pad0: u8, + pub protocol_major_version: u16, + pub protocol_minor_version: u16, + pub length: u16, + pub release_number: u32, + pub resource_id_base: u32, + pub resource_id_mask: u32, + pub motion_buffer_size: u32, + pub vendor_len: u16, + pub maximum_request_length: u16, + pub roots_len: u8, + pub pixmap_formats_len: u8, + pub image_byte_order: u8, + pub bitmap_format_bit_order: u8, + pub bitmap_format_scanline_unit: u8, + pub bitmap_format_scanline_pad: u8, + pub min_keycode: xcb_keycode_t, + pub max_keycode: xcb_keycode_t, + pub pad1: [u8; 4], +} + +#[repr(C)] +pub struct xcb_screen_iterator_t { + pub data: *mut xcb_screen_t, + pub rem: i32, + pub index: i32, +} + +#[repr(C)] +pub struct xcb_screen_t { + pub root: xcb_window_t, + pub default_colormap: xcb_colormap_t, + pub white_pixel: u32, + pub black_pixel: u32, + pub current_input_masks: u32, + pub width_in_pixels: u16, + pub height_in_pixels: u16, + pub width_in_millimeters: u16, + pub height_in_millimeters: u16, + pub min_installed_maps: u16, + pub max_installed_maps: u16, + pub root_visual: xcb_visualid_t, + pub backing_stores: u8, + pub save_unders: u8, + pub root_depth: u8, + pub allowed_depths_len: u8, +} + +#[repr(C)] +pub struct xcb_randr_monitor_info_iterator_t { + pub data: *mut xcb_randr_monitor_info_t, + pub rem: i32, + pub index: i32, +} + +#[repr(C)] +pub struct xcb_randr_monitor_info_t { + pub name: xcb_atom_t, + pub primary: u8, + pub automatic: u8, + pub n_output: u16, + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub width_mm: u32, + pub height_mm: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_randr_get_monitors_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_shm_get_image_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_void_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +pub struct xcb_generic_error_t { + pub response_type: u8, + pub error_code: u8, + pub sequence: u16, + pub resource_id: u32, + pub minor_code: u16, + pub major_code: u8, + pub pad0: u8, + pub pad: [u32; 5], + pub full_sequence: u32, +} + +#[repr(C)] +pub struct xcb_shm_get_image_reply_t { + pub response_type: u8, + pub depth: u8, + pub sequence: u16, + pub length: u32, + pub visual: xcb_visualid_t, + pub size: u32, +} + +#[repr(C)] +pub struct xcb_randr_get_monitors_reply_t { + pub response_type: u8, + pub pad0: u8, + pub sequence: u16, + pub length: u32, + pub timestamp: xcb_timestamp_t, + pub n_monitors: u32, + pub n_outputs: u32, + pub pad1: [u8; 12], +} diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs new file mode 100644 index 00000000000..cb3310be9bb --- /dev/null +++ b/libs/scrap/src/x11/iter.rs @@ -0,0 +1,93 @@ +use std::ptr; +use std::rc::Rc; + +use libc; + +use super::ffi::*; +use super::{Display, Rect, Server}; + +//TODO: Do I have to free the displays? + +pub struct DisplayIter { + outer: xcb_screen_iterator_t, + inner: Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)>, + server: Rc, +} + +impl DisplayIter { + pub unsafe fn new(server: Rc) -> DisplayIter { + let mut outer = xcb_setup_roots_iterator(server.setup()); + let inner = Self::next_screen(&mut outer, &server); + DisplayIter { + outer, + inner, + server, + } + } + + fn next_screen( + outer: &mut xcb_screen_iterator_t, + server: &Server, + ) -> Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)> { + if outer.rem == 0 { + return None; + } + + unsafe { + let root = (*outer.data).root; + + let cookie = xcb_randr_get_monitors_unchecked( + server.raw(), + root, + 1, //TODO: I don't know if this should be true or false. + ); + + let response = xcb_randr_get_monitors_reply(server.raw(), cookie, ptr::null_mut()); + + let inner = xcb_randr_get_monitors_monitors_iterator(response); + + libc::free(response as *mut _); + xcb_screen_next(outer); + + Some((inner, root)) + } + } +} + +impl Iterator for DisplayIter { + type Item = Display; + + fn next(&mut self) -> Option { + loop { + if let Some((ref mut inner, root)) = self.inner { + // If there is something in the current screen, return that. + if inner.rem != 0 { + unsafe { + let data = &*inner.data; + + let display = Display::new( + self.server.clone(), + data.primary != 0, + Rect { + x: data.x, + y: data.y, + w: data.width, + h: data.height, + }, + root, + ); + + xcb_randr_monitor_info_next(inner); + return Some(display); + } + } + } else { + // If there is no current screen, the screen iterator is empty. + return None; + } + + // The current screen was empty, so try the next screen. + self.inner = Self::next_screen(&mut self.outer, &self.server); + } + } +} diff --git a/libs/scrap/src/x11/mod.rs b/libs/scrap/src/x11/mod.rs new file mode 100644 index 00000000000..382d1f6a2f7 --- /dev/null +++ b/libs/scrap/src/x11/mod.rs @@ -0,0 +1,10 @@ +pub use self::capturer::*; +pub use self::display::*; +pub use self::iter::*; +pub use self::server::*; + +mod capturer; +mod display; +mod ffi; +mod iter; +mod server; diff --git a/libs/scrap/src/x11/server.rs b/libs/scrap/src/x11/server.rs new file mode 100644 index 00000000000..df7d2bae8e3 --- /dev/null +++ b/libs/scrap/src/x11/server.rs @@ -0,0 +1,122 @@ +use std::ptr; +use std::rc::Rc; + +use super::ffi::*; +use super::DisplayIter; + +#[derive(Debug)] +pub struct Server { + raw: *mut xcb_connection_t, + screenp: i32, + setup: *const xcb_setup_t, +} + +/* +use std::cell::RefCell; +thread_local! { + static SERVER: RefCell>> = RefCell::new(None); +} +*/ + +impl Server { + pub fn displays(slf: Rc) -> DisplayIter { + unsafe { DisplayIter::new(slf) } + } + + pub fn default() -> Result, Error> { + Ok(Rc::new(Server::connect(ptr::null())?)) + /* + let mut res = Err(Error::from(0)); + SERVER.with(|xdo| { + if let Ok(mut server) = xdo.try_borrow_mut() { + if server.is_some() { + unsafe { + if 0 != xcb_connection_has_error(server.as_ref().unwrap().raw) { + *server = None; + println!("Reset x11 connection"); + } + } + } + if server.is_none() { + println!("New x11 connection"); + match Server::connect(ptr::null()) { + Ok(s) => { + let s = Rc::new(s); + res = Ok(s.clone()); + *server = Some(s); + } + Err(err) => { + res = Err(err); + } + } + } else { + res = Ok(server.as_ref().map(|x| x.clone()).unwrap()); + } + } + }); + res + */ + } + + pub fn connect(addr: *const i8) -> Result { + unsafe { + let mut screenp = 0; + let raw = xcb_connect(addr, &mut screenp); + + let error = xcb_connection_has_error(raw); + if error != 0 { + xcb_disconnect(raw); + Err(Error::from(error)) + } else { + let setup = xcb_get_setup(raw); + Ok(Server { + raw, + screenp, + setup, + }) + } + } + } + + pub fn raw(&self) -> *mut xcb_connection_t { + self.raw + } + pub fn screenp(&self) -> i32 { + self.screenp + } + pub fn setup(&self) -> *const xcb_setup_t { + self.setup + } +} + +impl Drop for Server { + fn drop(&mut self) { + unsafe { + xcb_disconnect(self.raw); + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Error { + Generic, + UnsupportedExtension, + InsufficientMemory, + RequestTooLong, + ParseError, + InvalidScreen, +} + +impl From for Error { + fn from(x: i32) -> Error { + use self::Error::*; + match x { + 2 => UnsupportedExtension, + 3 => InsufficientMemory, + 4 => RequestTooLong, + 5 => ParseError, + 6 => InvalidScreen, + _ => Generic, + } + } +} diff --git a/libs/scrap/vpx_ffi.h b/libs/scrap/vpx_ffi.h new file mode 100644 index 00000000000..3db471e8fc8 --- /dev/null +++ b/libs/scrap/vpx_ffi.h @@ -0,0 +1,9 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/libs/systray-rs/.gitignore b/libs/systray-rs/.gitignore new file mode 100644 index 00000000000..00023b60af1 --- /dev/null +++ b/libs/systray-rs/.gitignore @@ -0,0 +1,5 @@ +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock +/target/ + diff --git a/libs/systray-rs/.travis.yml b/libs/systray-rs/.travis.yml new file mode 100644 index 00000000000..9316ecc6563 --- /dev/null +++ b/libs/systray-rs/.travis.yml @@ -0,0 +1,32 @@ +language: rust + +rust: + - stable + - beta + - nightly + +matrix: + allow_failures: + - rust: nightly + +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - build-essential + - libgtk-3-dev + - libappindicator3-dev + - gcc-5 + +before_install: . ./ci/before_install.sh + +script: + - RUST_BACKTRACE=1 PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH cargo build --verbose + - RUST_BACKTRACE=1 PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH cargo test --verbose + +global_env: + secure: O40C4FadE2C8yApgCbQNYmeWQuytrhu4W3a2HKRvGgB39LP0ysMU2UKXQIyZqlZUS9mP9qi5HYN+GTt83aE3Ac0eAwRqq+9zMjC2qMaiZ1JBSfCJI5wiiIXP0HpbsxXipG2Z21aqupVfu0HjNP4RVkaZ7ONKAeLAieI06+7VHbMPw6mcJd4Drv8VTyKn89VvB4lxKexLcURfagoic3fzeFKaIIVBSqGHiXrURbpD5tffOnzc5YFWxeGKTVFl8WqQVrRk2gnl/39UhSsOHGuSExw5GSxh+OaNHTiAkvOaSQLa05Y5mkNlHAsMyqg1mW3mI2xuzCQaFFT5G5JF7uxvZsa4GfROaEG8r1CZvpWxG2NtpupXvIC25nN+QQeeMZv5PHaxlk9OkG0k+2+z1Tu0Yd05x/o3+52YFo3geVwDmI3zx4Zgg9u9nIwGhdtzqbKV2fQNnKbNWVQH6D5M1DlBMYyY25jpkehcazqUbLsJXJFIoMkXhdkjTIpZg4w+CQ617WCnoDhXh6+Iqkw+iBBJJugaf2D6qBpXNiLZNJwbv2M5fj8uDsDtsUvjg56qBw+g+TeHJDKjzpEId/zFrAe4lmuFjN4/SlDk3n5xjZ5eY4PGRp1K8DGgeBQI5gyvHR3H7lm4GE2NCEvNILYFjpANZsiWwDepb2/rHvYNiLK+jhc= + +env: + - LLVM_VERSION=3.9 CLANG_VERSION=clang_3_9 diff --git a/libs/systray-rs/CHANGELOG.md b/libs/systray-rs/CHANGELOG.md new file mode 100644 index 00000000000..133d4c08a1c --- /dev/null +++ b/libs/systray-rs/CHANGELOG.md @@ -0,0 +1,33 @@ +# 0.4.0 (2020-02-15) + +## Features + +- Brought up to Rust 2018 (thanks to https://github.com/udoprog) + +## Bugfixes + +- Updated libappindicator to compile on modern rust + +# 0.3.0 (2018-04-28) + +## Bugfixes + +- Update gtk so linux version will run again + +# 0.2.0 (2017-05-04) + +## Features + +- Add Linux Support + +# 0.1.1 (2017-02-28) + +## Bugfixes + +- Some cleanup and CI work + +# 0.1.0 (2017-02-22) + +## Features + +- Basic Win32 systray support diff --git a/libs/systray-rs/Cargo.toml b/libs/systray-rs/Cargo.toml new file mode 100644 index 00000000000..b686ef22c13 --- /dev/null +++ b/libs/systray-rs/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "systray" +version = "0.4.1" +authors = ["Kyle Machulis "] +description = "Rust library for making minimal cross-platform systray GUIs" +license = "BSD-3-Clause" +homepage = "http://github.com/qdot/systray-rs" +repository = "https://github.com/qdot/systray-rs.git" +readme = "README.md" +keywords = ["gui"] +edition = "2018" + +[dependencies] +log= "0.4" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi= { version = "0.3", features = ["shellapi", "libloaderapi", "errhandlingapi", "impl-default"] } +libc= "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +gtk= "0.9" +glib= "0.10" +libappindicator= "0.5" + +# [target.'cfg(target_os = "macos")'.dependencies] +# objc="*" +# cocoa="*" +# core-foundation="*" diff --git a/libs/systray-rs/LICENSE.txt b/libs/systray-rs/LICENSE.txt new file mode 100644 index 00000000000..dff6cdd298b --- /dev/null +++ b/libs/systray-rs/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright (c) 2016, Kyle Machulis +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/libs/systray-rs/README.md b/libs/systray-rs/README.md new file mode 100644 index 00000000000..72ef5b645f7 --- /dev/null +++ b/libs/systray-rs/README.md @@ -0,0 +1,58 @@ +# systray-rs + +[![Crates.io](https://img.shields.io/crates/v/systray)](https://crates.io/crates/systray) [![Crates.io](https://img.shields.io/crates/d/systray)](https://crates.io/crates/systray) + +[![Build Status](https://travis-ci.org/qdot/systray-rs.svg?branch=master)](https://travis-ci.org/qdot/systray-rs) [![Build status](https://ci.appveyor.com/api/projects/status/lhqm3lucb5w5559b?svg=true)](https://ci.appveyor.com/project/qdot/systray-rs) + +systray-rs is a Rust library that makes it easy for applications to +have minimal UI in a platform specific way. It wraps the platform +specific calls required to show an icon in the system tray, as well as +add menu entries. + +systray-rs is heavily influenced by +[the systray library for the Go Language](https://github.com/getlantern/systray). + +systray-rs currently supports: + +- Linux GTK +- Win32 + +Cocoa core still needed! + +# License + +systray-rs includes some code +from [winapi-rs, by retep998](https://github.com/retep998/winapi-rs). +This code is covered under the MIT license. This code will be removed +once winapi-rs has a 0.3 crate available. + +systray-rs is BSD licensed. + + Copyright (c) 2016-2020, Nonpolynomial Labs, LLC + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/libs/systray-rs/appveyor.yml b/libs/systray-rs/appveyor.yml new file mode 100644 index 00000000000..39123c80617 --- /dev/null +++ b/libs/systray-rs/appveyor.yml @@ -0,0 +1,22 @@ +version: "0.1.0.{build}" +environment: + matrix: + - TARGET: nightly-x86_64-pc-windows-msvc + - TARGET: nightly-i686-pc-windows-msvc + - TARGET: nightly-x86_64-pc-windows-gnu + - TARGET: nightly-i686-pc-windows-gnu +install: + - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:TARGET}.exe" -FileName "rust-install.exe" + - ps: .\rust-install.exe /VERYSILENT /NORESTART /DIR="C:\rust" | Out-Null + - ps: $env:PATH="$env:PATH;C:\rust\bin" + - rustc -vV + - cargo -vV + - erase rust-install.exe +build_script: + - cargo build + # Skip packaging step while we're running off a local winapi build + #- cargo package +skip_commits: + files: + - README.md + - .travis.yml diff --git a/libs/systray-rs/ci/before_install.sh b/libs/systray-rs/ci/before_install.sh new file mode 100644 index 00000000000..ebe4f4fb941 --- /dev/null +++ b/libs/systray-rs/ci/before_install.sh @@ -0,0 +1,39 @@ +set -e +pushd ~ + +# Workaround for Travis CI macOS bug (https://github.com/travis-ci/travis-ci/issues/6307) +if [ "${TRAVIS_OS_NAME}" == "osx" ]; then + rvm get head || true +fi + +function llvm_version_triple() { + if [ "$1" == "3.8" ]; then + echo "3.8.0" + elif [ "$1" == "3.9" ]; then + echo "3.9.0" + fi +} + +function llvm_download() { + export LLVM_VERSION_TRIPLE=`llvm_version_triple ${LLVM_VERSION}` + export LLVM=clang+llvm-${LLVM_VERSION_TRIPLE}-x86_64-$1 + + wget http://llvm.org/releases/${LLVM_VERSION_TRIPLE}/${LLVM}.tar.xz + mkdir llvm + tar -xf ${LLVM}.tar.xz -C llvm --strip-components=1 + + export LLVM_CONFIG_PATH=`pwd`/llvm/bin/llvm-config + if [ "${TRAVIS_OS_NAME}" == "osx" ]; then + cp llvm/lib/libclang.dylib /usr/local/lib/libclang.dylib + fi +} + + +if [ "${TRAVIS_OS_NAME}" == "linux" ]; then + llvm_download linux-gnu-ubuntu-14.04 +else + llvm_download apple-darwin +fi + +popd +set +e diff --git a/libs/systray-rs/examples/systray-example.rs b/libs/systray-rs/examples/systray-example.rs new file mode 100644 index 00000000000..85c1a6e1baf --- /dev/null +++ b/libs/systray-rs/examples/systray-example.rs @@ -0,0 +1,43 @@ +#![windows_subsystem = "windows"] + +//#[cfg(target_os = "windows")] +fn main() -> Result<(), systray::Error> { + let mut app; + match systray::Application::new() { + Ok(w) => app = w, + Err(_) => panic!("Can't create window!"), + } + // w.set_icon_from_file(&"C:\\Users\\qdot\\code\\git-projects\\systray-rs\\resources\\rust.ico".to_string()); + // w.set_tooltip(&"Whatever".to_string()); + app.set_icon_from_file("/usr/share/gxkb/flags/ua.png")?; + + app.add_menu_item("Print a thing", |_| { + println!("Printing a thing!"); + Ok::<_, systray::Error>(()) + })?; + + app.add_menu_item("Add Menu Item", |window| { + window.add_menu_item("Interior item", |_| { + println!("what"); + Ok::<_, systray::Error>(()) + })?; + window.add_menu_separator()?; + Ok::<_, systray::Error>(()) + })?; + + app.add_menu_separator()?; + + app.add_menu_item("Quit", |window| { + window.quit(); + Ok::<_, systray::Error>(()) + })?; + + println!("Waiting on message!"); + app.wait_for_message()?; + Ok(()) +} + +// #[cfg(not(target_os = "windows"))] +// fn main() { +// panic!("Not implemented on this platform!"); +// } diff --git a/libs/systray-rs/resources/rust.ico b/libs/systray-rs/resources/rust.ico new file mode 100644 index 0000000000000000000000000000000000000000..3466392a140aa74a3c00d758f9593343f866a6bf GIT binary patch literal 64752 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 VB)}OGzK_^Avv+rjeVPptP!_#{2$`qmkl44|)^l zbhZn;c@z+RxcApr#KruS= z=vws~CD%u&<=L(tCvyB+hQWy~c{@ALs-3Is@!n*)#%^b;Zd-2Tm(-cB-QG$G z-5o2^yLjv}-MpRU$=jkM<(5oXuCpilfMVdD3v$zfpFek#%s8%kJmFba+J@VHErOAP z@-FGR(xpd~#jm%^l-1YCY4|?+Fi-S_q2jBK{1S(g)jx!i&dy*2iIgkn5?AJ$>><@gT@Bqi|V(oUO)WG>G;eag0g$w_SklfS{7R% zb6{AvPupCpx02((?`*62{I&Ns;oTY7)-O>g_C9>pDD7!+OU~&>9+7anxAjY2T~@W~ zNs5!Stzp#5aUDX3<1YHP$Be93oV{1!)qRva-;tta7{!@dur*7C* zFg2vnVGrd%Q}CW2d81N9UoFw#Cz6`GLj3w(4=m&OK5* zsC3?M{fG5}3K?hbo%D2nYrZ8>xFbYxv8>sxNi(EZ`8pd}^c)X!m~YAWHcaJCle_=i z+%_E*+xwBv_r!+VQq<>r#J4q zsI_i)B%7}5x6HaXc5mI`qc0*&18fX5MJ$NkX+6%%Qye!gRiRDZ8C<-Ys3_62mY!U~ zoWW$%=ameL@3H63=Ga<&6iVp5v|Yo}^z^xF*V=tF%A}0sgOBdz9ufAf4eIo6s#zCw zM*Pw)>xqjFa(}F(&b&$YDm@pkmXhXkPk2=J?2D5;o*XINV0e7gv_0z^OQXV{H_d#x zUnunZyztvc&jc9VwahzYa-i4jYtC7PG0!j7e5z9uANAxNPbGGW!KNQUI3k@@nA#Hcm4;% z^yv|PrCB>()F`H|bS^&Uxp4bo#hVA}uBO_DWG>pvXzO_x-ts6}{N`1S(j}#tt1sSf zxVQU7;pscF2X92GZTWs&EL*&rGomZCZ&{wt!l1ihTP(8JCWl>~(2hu`FH!FidMR=7 z=<)dAsuc&{HmWr3jlG?gr;)-~$8fYf+*DSQpJx8{6}$L{h-%<;jR}%PhgyopWsW~i zd{`++)SfI6**Rm+T8T|1Th`LEL%SEXY?GE&bL$@Y>z%qUv*+z3nk71SWL)Kah!L-^Ja=es&gqStOp+~&MqXlu zU)tN2e>*v^we{FZmUOzo4Vfc;vU7HL2j43HI!&KzyK3bLdh;G;nnbRt+7D0Vr_`z& zQkO&a?;=E3zV>}lw_%$`@PpWs24%Nv-;4-9RJ?Y$@8K;McBJJp8*KIVeY{doX8$$w z$XfZrcBP>2=V}%Q3FK+4g zy?;}1*BZ(2;Wu^|v)X5MiZ@2=nKf$lILEMS^ghjvf;&%ZKAtg`u=2flGI6(*j;}m5 zzb#pF$C+?riyI+l=IYA0*{(SlaVBE#uy-x``t%doE*kV2h+ z>IOG%nu#^s|N3o4!>d*?d58DipSna=y9idVJF#i=y6b*9(;L<6?q061?wK)dV_;j@ ztsA*2Z`ThOE zJooe!U3SIkJtB?*fita!+eI2mTo!AaS~kv6ckQU4_pS|_^d?--+IM#D+KGn8FYgJ< z+_A7Uy28ui;Ew$PAyyt*6zwdv5Pq(5W|YXr zw)6FypG4fBn=p56!S_cs_0Kn!GVS$loCt|Iw?F<#{f@OE?`>Dj(vy^P*H)QOdpATV zjFVIqul{B7*iZSL4U5HuPCapZrKv_#A59N26bYHr;=jA#d=s~4c1iS1y>~~aMlfdf zWj~u#b8_o^kDwQ>_FGrb#;wY!XfYajXSUvTqY;+1#rt#e>y}7Z5*GU#zfqX-v%ik?J|0hRNFm?!X*;~t4DUrx0%*{T$p`iVO{Q*0-e`~*B=Sc zb`{d6Zt@CWZ`>feuO>w8{-aYO^yXPF^!1ihH;#XmH7<%{RVS~{aeQ%~{yOBrZ?dAhiJZ|m#(v!5(oY(m^0^Q^noVY8as z56MKagAYGsDD)Pe6%`ZunAB-lJ73E`C-l?z#hSab=Om4mkSLTcI19OWgJ+xipCj{R0KmU2^(GKCMRJ}DG$0aT%#JAM%Q`^}bc46$2 ztB0cY6-}6bd+xpNX!Zn+W|Nm@oewTf5~*sEZMh?NxaQi`?52_>4eRCi`Wk=fJ$Z7R zR4yB z*$7e2ea^f`WI`TPEh^&h9zKM7qc$qL(jBP zu46MQ>Q3iAF6}r)x2%oGy`kKfFZ=Rv?UVY7T|0@wrP(HH5(YYIQyzBPtoP*Rtt?nb z+^HQqQmpTq-v6H0x+{6jtPc`y z{nojgg%8%Wc3r8de*d8GaTd4Qa(KF%cAEUPlx>NtC3P5q7ru_&y<=i*+|iTq&n9)( z+J^CieU6s`|=2cVrDq%n*NcBu%4zscGB$0ND|V zrB{E9NIzCLk$sg?l)2B6rT_SN{j)HwV2v}yMmy{+$EU~F`UfYLc^sbk=If(xEM3X5 z2ftl130_t=#>!ryE`R?9ty<&OD=Hyz%kFn7YI~&TtDfFl7W{aT3iiDnC-bFOPcdFr z8&}?uSFKm{IFNQwE;Yc(oZV(LYN>W&!nAi<)H~v%hcB9HcA7Hm?6n71=+BdsYcfA6 z)m&T3G~Gnxj=3PSc~zT7`u=qJ(pFQ4QNgBf+d{?+AGYY*NRjcb@gp1hxb4ZyRc#&}-s7M7$&(s5aP|DKR}X6!>vg*eRDY>J2(4{# zll;7MPgUHK5MKITdat1V%=oXF1zS>ImDopKq>tUa>*|3t)rKYe zTOU=tH@mY--(#;xOo)EDaut0=vzwuO)qB?Zt_R0k?dq9pc_MQ*RxUhm>90TKyi~>b zSp~IUtOoqYm!37HvimmIrpRO~*P6}@9PrO<@7bzwHtX~lo8|IiW$dpLM$EEZ*0^iZ z4Y{I1MoeuGy*cLn2)!l6OAf~!JN9g4dAwxmcJ-KxVGEf$3EQ4k?Gd&P8z$c5Nn!d= z$v^vEhL_P2JmuP2&edikkHqsY4MNwJzGm94aaof1NRhrk>q_nP_BJWmx7u+5v)S7}ZhCd8SG{v{ zZCGttO6O87jj~TRCLb%Vq=WlOo!L|0wOq-QIe+oA!l|+80-dxl-D{RbD?1(Ac8uR= z`9`l+gs${_tG?i>Nvc9+h37r91DgWxe}7=0ICE5KSz7y#$8kPb?#G_qS2p$V0=tyS z?X4-LtIL7$Y^ABub_&(!kF89SxE?rd#oy+PZ>?O9ckW#iMDD|WWZy)qK_Fqru6Xqsq; z(8P#`-2y7Pk40=bFCsOX)jn<9(;*{!epbVQDc!er540{_=W>8X5u4krn6>r5J=2_( z3lcp$1vDg-CxxAfSAX|9GEM5>_O#%EW?40hmA--NUKa)Ayl)sc`^Y4{liOGPKLDse zSHGF6!*1E%F1GrPk5xiIRAUF6t`MM9W_}f}(>^WJCdS480eC%ejepxeUU<2F>#7@< zZSKB()eSu0puJtX_6#uzXyEg$=PAtdxe(+WkgJ5{WBzjd-;MXMkr?v)AK+(12jyNs zKJ3`xLZNdn(z90?^0Q*djA3irvAI>SYO29ZGpAkpnFgG_D=67j2hv^FkhWc}@cy98 zef5{4+o5j{+Mxu%7k+XbryR7a0tcvuhj|}_wj+epH&0WBADu_*Br?AcF~Ce>;)W4+ zozC$JzuC~D<&_l;OIEhwO1RTKt+uP*G|OsTc{&Lx!!$qOKz;=B7LdP%{A<9*f4t~2 zmXq=8R1*T<1J)>VR8*>CWZ!lF83aBJyo4C<=UvOl{Jr;(?qe4C5%4*pAlQQ_2*!G6 zW$4yY7<4>-Vuaip3Ln@o+h)39SvA$fg$QWccTCS!I##0eU0s#Ww+sDG72)ew?sx)V zhgt#vyz$IK5#bb%eAX+|u82{9kQP{m()5oezauUInsgi*&CIZFDralqxprd<1ZHxJ zDN91r+FlaaLl;vUL&&nKZlKzp{u$zvzM2TV^>HxQg6cnRBZe77=d3>(qUL-(@E1Bl zdk&yLT(-OnxZ&Lw{b0M@2gIepMpmgfg>XWPuR-H>zjEDE(=ngvGi0%xo8ksURqc!#n808YNgTGV@^K0Lx>c5@;1h1YVu zpQLdz%?(0^RLnn}8Frb<+0b@eyQxi<;J8io)M_x^T_vHhb5F?fYFSXl>xE&dwzm-( z|D{CF{)T_N=!bmksvEZJ9De7j>$(3yd(Q#?NA#>8!Fa;HATs~U*#VdzqD64oRlnr{ z2kpIuXxUsu#5uSp$m(9JW?YRn(6E=-YY|{W>S?fUMUwy&35&4X4^X!XWYF1f2mT4T z?$WDwJnauVpa6KtJ$J#YZb9W_g?oG7&HT~HVsK*cM5mxwi$z(HTh>kI+&Wuu-R3sU zEOw|NOSf6N<*kijKcIy04p}x}%|FPEBq-7*ChokP=zYKLpDwy=yEA^BYC_;UM911; zj867xOYEWWH^9w10`vFY&v&l6o(CMX_Z`H>F5eICLt zGccy=Mq5HYt;S&f~m~V%u=Jv)aMS7Xx=C9emq~a2I%$pcX3Bo;n1&v zw*s#Mt^nLW4atVv-jsyEX5iAadOC>y=zxO!4tQI-uRGqegE4>aJ$>h@>-GKz?R_oL z8n}j-X55ty-5n$x-$JN62dxn{&J`3sux`aPA~FzqtbVN$|1tgfWt2~X=r?x?fbr>s zd+vt9Dn%X#STPo344BBgdGJ^blz#h_nPHcyjGHz`=AUjPPyc|dy^`t0$IL+Sq2tmw zGK%r%h_U<^665(>b~LR&h`w{x^&A(?=l>-_WnUYYE^;l%_d)&@cmtUK24;6;=I_0) z`yaG-o0wPeRbt-2y%|z`G{{02DGUTmiV$pR6%<}syJ9K{fx(tQXO$@k1dxxYm~X%B z3*$qwZEM=$oPYp-bJ}45hp2L)!u|kR@K{y6wV2@1pcD|z{;7oaw+GZv*GzRp< zE5C;A3=UB5_%EpZSg%6bBi`3_dj|SvlQcuQfAlZ8nW8kZ6CKqkNBm)YXW@DK)ACqKR{ygK(uD=GW70?e!%nBAH7B3 zqZ*NrU#;*684>-cKj#hzNaxo&Lr3_rjLoxcZUFXL6H6C1&$Uz536$u8wLIqEAo82- zJvZ~w4!H#Q#??3Tz`L()qk1IpAf!{fA4|BE>D;iknRDaTf}7?%D>6$XWdI^V$a8|V zv5}^+RZO3hOQ2a6(O)6{o`68pE)bh%ewY{wzd}Io%*=mRr_xDy+(CQa0DOrU(YP$w} z8OA%Nt+2M4bIYvb=B;g}n>m?@w!D-u-`>Z5eI!Sr2&4j`G8ceUyz(;h_R@Dfrr<7SRo|sFT%7ayG`fZG3WT*<`&bLrIE+5o_Jq5^i%|&HzM=vpm5$$ z5RdOG{Ix_avQz8dNh$g}nn3GV;EhDkla3i!qe<%vqEDb;-HIu8SvAe3tu5L;;3CAn z4CfIWv(DAuqnDp}?=qD=(>ya6gAp-enKA}3#&~OFGidm`|Gtdnq$L*s?|bG$QP)O9 zcGY(4Y2Nu0ob#)k^Xi@7=rOYnp=i6p2fbaC5F4?`CWVb^tOlFyCf+?0)jjIx&WvG? z>5SRJ^V^McG&4gpiy=Ip=ZN!B2mrxmgv<``>JMi8wBEk}ctr+3Er56IWd1ufJ#YOn zgqS+EAMiSmrvS}`lofaVuHd|9ws7pRdYWBk8r;5Rt`i;Tlp$^;E(3m>wyuy6)|z%Z zdK^U7X`VGjM2(P($U0*%)<|Kjd`m?B*$4ld%w)Xgzn3REv*b&F5ImCQtF>sqF$6u! z`;Y}62tLG|{@`OuZvyCy^VzAbD)INEW_y+E|8>(jb3wUgQ;RImXxNy&RRMk)+kyO+ zdPr|1<3j{~_$2UZ;P*S3|E@u0WaGK(kGYoU%ef9XM`0BV$_$u)nV{(hYYk1!xMkxk zO)IROX>!|^*n_gn9z|JAETgVg1}cIYNhPe^#bx4MEF!iBmF!e8a(rg9X8^xjt~pv# zC*XUZaeN8W`UrLI?|e}-y$_w1++HbCGe(GWP?QyT2o4bkf_5_T%XHs$(>Ydz-)@*g ztz~MeiAm*{5&t{HiL(YY#`x6?&+MyZac==R59Bo10n!nqnD)+_2omFg7+y$NLy;lJ`MW~ToB_RSa( zR!uef@CCb$388XCs|ZOmR#mQnRPh?EW)oD8wMMcKl;xnM6#zl`>f1iA&bd}y^Lv%p zmP-|*AjT4GhNL1MV>7S@3$YO=Ar|i#!%Svb-ORXg^Bl8CQp#z3BTS{qsrS`M|IH8Gn>8=-*dFXl5BMz;zq8FqLP_ zG;+$&b=SVW;Z6# zFWfOc=zY9os1ImRf(ivtU9(pkF%)8Os(7N)Z{O(V0cYGxF3ZPW)OBwtmpC!vBN?)0Usm#Cxs0O9qBY<|O+e!eVovE;46e7nWm zmiA`v!zV&uHiYQ?*R);_B(HsKjUGt80h8fvj{t`hI-z44)?!cn$Ql54JJ~FAt0{G&0Lg8@CdI zvSunrlGnd#JysVSbv%mX52&seTU-A7tmB5|>`=VqslZ>l@-|L6a932h(b??5A#i~A zog4EukEpv+mgy!9F0sTH4NP0Zx`yStO>@k;z)YT1P+wY2oS-(P@0J=rfqXJJuYUvZ zN`>QPsueU%t|Dx~nm+OxSB79O+ra|9A1R5EBYvr`m{w;&nL4>J1V@u0%^X3+NtZ2}7 zo_5d}|2INH8%?PKrs_8UB_5@P5dL$IU;gCkn+`l=S?_<@Qh{F;`={;ey#7z{?&zRk zQioj^1R-F8i&22u%d&2|fgoIc$5t9fXlAyPyI-r^3tga{<%1cOwn`>>0O>=|7}H48WxCb1-77i4e(=PZT}B}U!r~|JBX7ENBM;igBJQ5yluavq<5hZ%c?>tVFH> zeH3uH8pC-PUbbxay=>Khe{_%C;HDcknW?FDM)hGv4A|Ir5ATuS$bhkc()X^u zo?{Q%#{)k^WE~>Mrq^@+=}|?Ti_X3fV(GdS#CtZk9J@?65R{_r5eZe4*#}K3EEuw4GIJ9g6;OoD-g@+z;H-K9dIW54hYHdvJ2_6eo;)57NCc?U@2Di-> z+%{J*J=Mfy770-@l1h5ZHU79-zYf;txrfz;O@jm zqK_bgo8EZLLHpjiT!P@(gZ4p{Eh78~cmS|Z*Zigl8Z^~Xy6aPYim`?a*gRJ-om*xa zmRT2TVfv02NL@w$2i5lh?(LWSY&jl77cHv*IPQR5anXvpLqyIrusWz#SpFbb6=rh7 zsz%Q5HqB84Wjdu-s!gVTnfz0mts#!S^!KFZ{+JHF-t#O(+mFNa<1+k&v|#35$bk!# zMMDlVL@VGy#I|930oNRJ(7v}HbI`uJl!D-US6#=kF*0xqF-hnoU`?m@#}Gk~-cyY2 zw44ipJj68L&27(aGYu4Id+i)soflNhMnwJ&V}6RY{NFaHNm*6_@a^B+%JB#6h9I|O zBKIo8#UR8EKI^7(Hnt+Y%HTS#!USh_dMB5KkDGOux zdGBF4)%-$fJ~u|^e`|<>;9;c3t4ANS@7!@q=p*?4)s+zVHL+6kWD>mCNI_6G1uS=R z0W^>c%q7R)+Nqqm)OM`W`YGt|G<*fsw-G}79beowK%z^Y2-Hh9PE--#^BS?|Bc=T3 zncSix+_J8l%wf? z)=5F>00m$KZ)4CmYRoZsXhmCIdHT|f*!rk6%Bei!vJ{(O_O0HzmW0p5b}ZF=N&*E=vk5U90~Wsq6&%wPm=+|p($ zk0@60T}h2@1#)$vflUdP_}2Ti*ooJ4F6efcp5?K}5_tCwau zaO{EmpfQQ;I)R5HtQZ0r&{+obEDfxQkuD>`)>gr^6;@=XatK1qjZ@g)!(|43anu33 zbKC*D^WERvysQ(qXt(j^lg{AJ_&&$6RfW)1FjGs=nWbyi8 z7%TSOt?03UO=z7KppAioymnhdV>=#Adkwnf^Tr<>u=KK~sazC`cD zM9=Ikk2n+&5$AoQ@Y$NZBsKGaP8`rm_QY!u`7X5o!#+K=9_^t7YM zl_eG&f!NyoTw<#6QVM~6h=pV)Rbw9_2&zIql%{KAgO3@kA+WjS*lX1ks~Q*!rX^%V#S#Ru3hP1) zf9`M4`+FdW1X0~2A{&fyyAXZ>#V_+{6!XaYKNn+$78r%cF zt1&V2kMitGXTQF4RnDGt&^!*kpxNiZTPDK65pMBcmqXQqf=)0im zh)7Zcak{I*z8*o~#p1-GL@?22X1Q&)0F;?5vSCOE)eMR1RswJVpN6&!_-kU~ z(4wirDiHYwBG=C={A+?hYuT-049(1N+pI%?W+RKQ6p<;d2qvW#t1g!yW;ScPbqVb@+AdJ9c$?v~oKU9g8jDHZxlo@I5iF3StZY~|7ZEL` znU#gW{V%xIwR(l{)0tGt+<@x1`O|pEGWUlu{-8Dfum?WykI85!(=Q#RdAzT}OBAM_ zwMZ-A>`tQ5@x`2uJD3r)!!!Lj zm;yU?`SBBo^#d$M2&8AU8h8@1TCsnQh5)gsg2zORz{CShE8I3)U3 z!-S&8OBPS&#n3hu7x6KEtI9il7`!YQM)L}Qzy9Wp1Rt22(_e(Z>s7g}0@@65?eBt8 z5TgKPvX3Ab>4X`kbIYuYG0>)sjX>sQ{&b_7Cbt9w33--;z!+M;d@%HA{ZZ@u-j{fp z7=ODneT`{#03&Pt(=* z*ZPdm`n@^&1IDxzjK5826l3>{Wtfw3BIY0CoP&hLHnJj4Bv$q<+zL<{C{HH#{Tu!) z>-W*4VO$p`rlA4cHd~NcgV>H=EY!{)zek87LlgvA${Fy=zoUH2d)2pJJXsat*u_Av z`|>ZDo0=|sp1(^~zbMW@2ysBhR?;b`P?h{5c!8yhNjVsU`D?$64^Twr z7w7srt&H;U&*$AA^zep_X+kbuPHo?H&Kg4v)7;(Tk$FEX@H79phYN~j5(+DUCj*PA zw>c7cc-NpA@mvRt-6OaFrYJDZ(J;c+!n4UKQ+cf*PC8Mta!x{8I(vv=MUn42m4BEE zX49?h$>(UIYCu&*w#?p$bFU-luQaXmjS+Hduq9(x<4}x5tiKJdf)JG4bS^o}(_aVt z>@D3L9uZii)~`RXLHAx>dx4C<98@liq=(__^E8an`vTQN#=R7u5t9ug1jD>SVPW)8 z;Lu4AO6Gr=Ww`gC$r^dTy+Xi;7{3Rs5sV0T%sHkqTNN5uRqAmvO-Mtja0o?j1$iBq zU#h?-zB+j-@I(UOO&9+JwT4gq>d6077!@ zwgko%5`zT8cmiRJIo}I70T#3jI0$(3=m#{$vLN}jJd%)sGxE%^xmBP_ijc)Kbz)_7 zQsdJ~TY`U&sJ}q8;C)}bY|?uH7Jvuv#!G(5o#n(v07hUY zv&^}`oDVdtsbZa7$vYwu={GUC_z)e8rgS!jE=><~1cR~jn*2jUdn0o87rM@07L7k9 z!}l`#uzg1}>(XlfZYG~kNQ@E;V+fPFH8f&2fd?|dGT>RK9ap;h9}OHh?h7#NU66!| zEfCY^5sVmoP&T(6Q+d{v2vqS%x8wU_gm;T~?))$RXrkAtP77)bvI&@OOm6{wOCG}g zM8kuOQepiWjirr7V(9P2%`FUqHHr0mBPFpLVzMC0v?-5-QBqwYRakXd0o5#1nPr0y z*YVc*W71MFeQ8Av(`a?eY5p;O2`Vyhy!~RJ{;g&p_nfS54ff625x}kt zX$Lll=Az~Bc%oh#=Vq&6R?jpJCP9NoUs(uoi7O&lYuGqfu&SA1t-<*az5IYmEP@lG z5W-aouS2dw2)HR@Wv z``W`a`=nZUIhmg^u076p$FmT6_NiG0iFOC>InIb{%Y;*jf@9og9<6*$k-XYiv~uK? zYx+3m+!|WWsLiu~zww%zIsYpQH~mXmRBynyeC21n@RY+nhA%04j%wJe^aC_S zm=fi>xq{4eoPXt%C5bo({44Mez}29<`;woOJKgxalMeqSa1n9s^*Z2Q9p<;O%W&tE z0N0`XGq9!Wxs+>;e&!Fr{S_Vwm~F-Ub*8VMZv@jTumPl9-(eh+f7F7fISpU%K5V^hwmr#X7)py0 z0Oy=^2#N>gaDhF9$WfM9_@f##obu%SD<4dmCjfjLcqyZT8M!2W_Agzfy&$l%Bo4^-ytQ?}&z+Fl@5oxKwH zCJ9xo=cHqecM^dyV8`8nLlrq5IF{JGXT+T8h_jEfY;>StkfF!+im|%(*S=$G+jmiv zXRdz$AHL|@qwc*)%pI67gYW`{7XVj(XaEAyvrjv&Ey4XC$k_ycJ@8xwTSl_Pd(>D= zGov-vq6mSeK+C7(xj61=L5@9*4WIqu^nw!_yJ*1z;GC0Uag_$95KgwK$ETGj0GgR$ zOIpv&pma`(J+3xxTELH9CYyeIms_4fx6O9+prH}R*NJn#XcA8R}kxe3*G zs4)*jStG&4`_ahgxe>Os3f4~LG>uTC()$o^=GXuq3?}kKjGG*k~2bjl_!EHji!jK3{y&y`+DH(lMGZ^ zEpGdxNBn->w=Vj++U)2uzTY#kpt>LM7_7bg?Dfa#2QFF|Amnm0{s?W~w|z&Wk6-i+ z0RD9S@xLQt1)Ek>pYKHRSy)AN2foA z?RG73*~y2-03Ut-B|qZqhaUyVH`Un7#1$vh!EXhy*3xpGLX|AD6zv!y55{8DPm6}f zWx+klsJYU+;>$+$v(Gu{u;$ zYK(wn;2o+;Q9G__Erkn&5Xkd^m$**Ri5j1s|J5IJ_Vf(EPl^3SyUosSN8e)!;iJHB zCb>pQi(>wHO#RV}KYVSxWTD0-q)%S-4gPffLp;$@+X6lcvj4FC2gQKKa|FgMy}p7v zqYn~0gDiuZFu08#=!%k_vHk(%%+aR#XibSfA`(;K`*;5Z34M5c66W@~+lHy%U3uXA(xPJe12gqHvGv}#_TX4|_c;3F z|2v*l@X3oV1%dtanoCsV?JCmlF9)Xo({>7JOEO*|sA~xB-YD8nXw>aAANl zU4E5`H4bkCJ};`Dl;Hnd(0iVH;vx1WCk_WgsszB_{qYgJ;;4IIrknRtBX1P=oP_W% zs`^y*e*Y{4V~C}UMTpf*L8VH+%xvs@k{O|4U~Aifi4Vb%@}ASHY+wbk5Bx-w@Y451 z)pp{i3<~jOz>^eq9hafEHRDs;R^KlSILD4opS3$Icxm`Np`|%REa-5MONK{V3jf%5NC}V!+x;FFz0+^{49@v zp(!UI(_VGaHF0~RJO=n6@Oc$Ek32i-6%RdF&OPO@zU!F!kt^=8=1_!>0k2TxNEI{X zG-CbLO?*wMyP^Rx7Gopqu>od`Ayb%hfw)X4A4G=&vcJNClQ7-cryYwLo1uEN_`--Q z5^C_^W7;DUIz0MZP(_lz8~Dy@H|puwF}{ z-$JN8lmWt^FH7)7g`W@27sIRod7MI1`_9{)VTj27z%G-#L#hE)&4GIl+sBxwWD7N9 z)*{LD4@i}13ki+e^n{j0iR*Ljqax2o^^@La4J1EuWq}|Jq!x*jmxIu!U z_n3}A=v^`l5(Wd0jmGR(LFR_XC1f99W{@zi@0Xs{3WaAYtb6AA`;RqOA#dcGWoK!Y z?;o-TA#b2r1I=>O=7cQA5IddD6`qE*U7>VQK~s3$rjA*G7>_IfCkcHx%hSuF07odS z?46hB;80LiRq%>Hf*T`w+kzN^$q1?Zw!8kiGl|v+R)j5u#~7(SqYmTO(17*^-lTBs z^G`ZFCi@Ja#QZCOmmu=QPHAi^2kylN?7qX+koRvQ>ec$A%>Di~e&-y4exWd`$S3su zz9U-%r5cih26_Dk357w-+ASDHVH(UQ42Jm(15I7AtDHSq7p7rkASEs zqys-5eDob?EGy`ZX2#fpD!XpFVbhJP@Ag%1_W#=NJ2KyQk9`-3dGiBEMjovrju;-; zE0>j>tkCL7Y4;vA`kaB^4qyKRa3io)U`^*r^*C1jZve>^UIc0d&U^a$2VI|2@S%%h zDtfJ^U^Vb(z^i~=;)&7~(>o^RcQF{7(F)N*ZF^dsYcI9gacZ*@P`!t7g_KNR^$09` z67)S|ATs}@u(0m1b#uRS`a$OY=-7V$af4(1edmzD`|pVKq|;9zAwfJ*%Ph@#NJjis z-nouArkfo*Tl$1SG~dc!e=YE@0m3IzGE@Im0pwSpKO|)UcIO-P3xagTX^_1e^j)ql$P}Irl2dI>ZKH7=ucVpKH^en23(i# zTVfpnkonkXL{(#bLAvQM3`}3rlTJS|=>hv_Vh5f9Au%0eOLhBw-^wF&z<%}T(Vr3| zb*mtt^3SSz#b`W10oV-kFA67suIRpIzaXet13Ab8f#X1L1#SQWB72~^2Q_}H*ZG%! z*Ru^Jc#K#Kfue9sPc4e$BKul}s- zZmTfhd0)7KjS4}z#;csGUjI4xP=wUtTPlgAsnI8hF?cc6+Cz;n=L5OKTE{B#ufx}5 zF}hH7{}D}yVpIht5fi=h$Rm7M$g<7}x~T=c3-}126Knr@Jp6;5e6@S&JEp(!=|Jzl zbcAN_3%MhP0iSsK2|O_}|7zeb6yB|{-{7^qjQDiZD3AUypF;PsgM2E2BL9c*Q8gxf z@}f(J-dD{p@LAyfAVn{Nac4Y3orG4AJrwSzv6yHdM22VGil8~Yz1BqycM{mH~d#0!zd?k*VyWo zN7?c&F~1s{(6DXS92kjVm5}9FJfbne7Dz1tsR8nqZc7mld@8_mXGN~|MmTQ(^TU8z zfcJepmKVP4r2E`qlvjt$Zm~vQ0Icxd;EfPO5|T-5?;S+2HhM6(_-GIo64RXOfSiN8 zz182w)J&peJ7|c^k!7(7h|&Q8>6~De*sA&8feV0N17T9lKibhZj+1Y+xj&An-?vZq zTs1+OPd@zwJ81qGaP0WPW3XkRogb%&k(gZr9Go0|4+QQDG6uM&PWX!qy^}%9fbO~P zdrF1Ey9K!b;9oBsuLkX3F8U@Q5m0u~g zx-)3jpi9k&iEc!SD!)ZcL@{0)%M|dTu~o?v+)FL|=e+h=DvwvBp%BDJ%Tu=8ZJ0(8w$~Q98|OcVole;&By0 zZC+dJr|rg~c_&-4$J>895q9ZQz*m5qVKRr`vtr2R*}!`>3B%FMzdVf308QVc?HurZ z;3gvG>c~eq9^;Q>`ud*&tRez}>`fE|4HiJdU$*&2JfCPA9#i4p1)c}o`l$=YMF8v2 z)7C$@{F)Ox3_lKdEO2koe6Y)3GGJ&CzjUS9CuqY zLpuaEwNmM@7#ad5l@bGJtQ>w1=BXrfRYVhwc4m18EY#Bd!wLL#OI_uP+Qf=7c= zgg|Cer=Zvm5Yd8+DmP$@g?AOgn*`bL-mi=YPhjWU;XA*+ndA1~%}EG9M7UMZV}VtM zXwFC)Y@SY}(1O=sYwHDTUd@_W6(Z>t~H95Q!tD_io_^G9EH7m4%zQ}0_TAI3=IF@h5x(g@n3$`wOoGHwR*%s z`)wdvWd8$lInn36Coo-)6m*R*HTYDC9e|~RDwhtly|M8~ReVYelHhS6(9A5&EH>E1 zWMJYFAUXav1$d*tn*}z%cLF?sv7gEFPd*e?tO$<<-a(3ymhSqMGdY{vo*Or}Sl!HG ze{&*CuoGyjnP1xpgsc%KBjTrTRWUzMkxv1iCvYt?`TBksJ!Aa?AT0riUCil`%Uxrg zPwV$;{N8;=Yy9zy-1(V)JSJYOe_%XX%wlBJKJzYuz6boJ!1omR_a$KdLG-ls$D{0t zaKj4Fn8X+K6vqWb)njR5FK^U z-5?fZoT*^OHTg$teQM_ZaOUsV_=D!9^ZRgCnqSM0YblHqrkjiwe(w48{?K!LLy&V6 z`#sQQV}6Jh!4+5ij3W-ZTS3e@`JzO0pqnJ(Q|T{RLyND{a|G$ zEMxJ>?4NEnnQOJG?G{D6`yR;4F=p%g7Jv^pAts;z7|@mF-?1S`^!m4hV$!l8q_~8S z_g>v<`RVrHrY#}dAm05y&GLU!k?*$eeZMV@UtGaQK6~i|V;rA)h0th|zdUBpT7URD zf3>d@O^v_m6M>`x<=S@#bV{NygYRd`&-ioE?9F2BL51T!mjiSXSn*)4yEwt@3N} z^z{$=G4NA`k4nU+d=kj9DzY-45%31qanKOTe6x9*>m=@;X7UwRtu1ROQyaZ-Y53G#8^ z?||Qf^8a1<)$MSH@Xt{ggg;;ZplgZXw4X-9L&f`N0P8U(E1lh4iZDbcxJdy+o9@@1#5-Tzyn$67_5e04WV~YufR~(dIA+rv0e_$u^IR`-C|InILck;^mk-)m z%GB1t<)(U?V)XzkXGdru{6_QKzyAvhLpYt3My<2^P5$mCzhgb(U0dLpn$Jsawd~Q22f1N%S1%W>Eq4&F0t^IFeAHrh@ zVQ;lIzVjNY+8QZ}nBi(OjCbkY()|hqA4BiP2qgrDGZwtxN8xV;-uB#+4*QXUp1)L} zpf5V}XmEjGWRmgwz1n1BIA`#uZcYy#QjHmhenxc- z!ApiS89pU6c$0z6!Db!Juf)O#S8NsMHb(d1oD)U+v1IDuz812A8-+(VQ+-LM)|Y^eWI$)lvrd!szM^J=|vN zH*ej_e_gbrGkzL+tmNA&-p|$j7y{7>2qEHJBnMy=k}*0Kg`)+V5ln_rt+ciw3QnK; z6&MkM&2j2?x9VSvm{Zo>@RRcz-u>q}Cmy=Vuq*HQYIiM!0eJcP`|&qt-cO8hIPi92 z+pdR-YG$0PVv-TMC`Za{CSoE`DF}?z5(^|It>-{Ppkbkb$M&&F(g4=BL*8tKysHEA z@;;aG7@3RUUGq-D{Jm!SLOsP2;tTLabP`I!^70Nt6aW!tL+}#w z1j@*pgxIJ@td$%+BGd35kPnNs5AZXy_MB6X>>8W-t22+lX0p;N&jCIya5lo4IPwS% zN@~MnQN)xWADx#A6`X?8?sDHi#O3lttU#*)D@rZ_BcL|n3}#uU^T~FiN!I&)IPtZ8 z2Ql!nF@D{&ukAAt!_ULGx`@xb*O>2_)CEF3r%`wiF%|q73R920lL&!0eAyI1N@68o z$a6v-GhbCfokN{NT&(#=Llg!YqT?@k1jS1YV_QR2NEAd+?+{;Lyu*5jQJn#v0DM}U z`x`-bdqGVIm^_n=SS|H7Vn?0YlM!kVe%O@c<01A6(9|!$KnSrAsI&w~4=vU!M>D59 z-j)dsSr+pELcsWfi1lBvt!WhV4_fcn_Z_v?*DlP_*w=Q-~LLZL?( z+)d;A`f6b9W6wB+$DO&u3j!rBVWI*t1~pk_3E(4-l%aA%RJ20pnTGU!gj#m3Xowhq zDfQl>5rRLVc9(fK62g9>`fA{nf;L}x>Jb2#pd2Xh43L@nP{sKU{1#9cOWS04jS%5M zOxdk8B?8r6T@%;)Uu-gm%$3})hCTEBN% z>c97oqlZQ?{!r?50q2U)gFN<0NEZ|y$?qdMusytnlx2UoQ?3hBJw`W0p zd%97OPPMF-{Wl;!=%f^5)x1wAzn$~>Qrk;J2e!#ypbBcT7=h~)9PI)eQeHU&HfRTE zq?-7MKNeZp5Fx(QCx>8i@Rcv6D0sAMxd31#me@Igpo;gN78>JX&;)EC^p>2_8)E z_rc6RYP~;bzhV0f^X!ju>~$SKpQfMhd}Gcb%oE5f*rA z`u=iaX5bD-NmWWWoOpcK1ck)B5nD4ugH|;(<$Ne4hhKHR7$;DT4#QF*hmHyslO?>t zYFnW-cyNR2kAd%09!TdZ#;?c-nLw_gT9d@JMFa#fBoil%Qqoh&lnM_p0CQNpiAbk{ zb_;jaH1;pDHgk2e82`GZMIo{+kS0)tIe!a6_zN03cw(~=X zPQmnBMpD9E**@}9plXS1oac5`UP}mH7w5h4fqLkVa>DBaNdN}HWRaCML4(zvMngc2 z!KdvW!Ko0Em(r$>aYglZ3tFvjw&jx<#{8Y{oxATf--ns}^<#&b`~Ab|tkYo(#%DYx z>Swk=%)TwDU*Jq45C$$2;yeEg;Licsk%T~Ea8YEMHHt* zGzzKRTm3oKu{AsXwrD{FjX(ghKuy0Q!DJD+yF#MU+d<9;9&eG0cVRQ{_`>A?WN*IU z3b^13UUuSru9Xm87xMgKwf0;>I1L|W%F@M979Z3cgypN~5DE+)p-=&h;Ru^xzPzNa zC$;rpmuaH`T$mA&6>Li?MdJYGpLD%H+QZL)bCf@KYkY?6GxYd@*X+LT{QC}>m?fpU zUtpZ8)#pt|?%U<55V;5N3gGE1utaw0NdQ)WPRH?%??vkOyRH^O^Z0#kkSvS`~cA-X|N&~fkI<$0IC@A_|dOj1J!Mp21qyK*Z*D& z^Uvo9?EKXI+fa?q{93U0dh_o$|Ncu)!>%>D-oJOtn8M&x;E}*59&^US`0oq8wB&d5 z#M4hGN3bXG5MV`AecevJ#Gop$R*=1_5sHAMVtJ$Bx?6xpJOHV^U7a(QDZ}OBpff9R zZ>s?dzj5gKLFJ#{b>!P`e8G2~%R9gN)4u!YO20cVnFTLC@sJHNJNqGBvEu6%mJ_?GI!`$t`to)b|kyTeN`I8!x=4bx-g~5_( z{fQWV5~iP=;TP+Xng5FYF0RqBhkju&;2P7wGl7dW!No5<89*$AbBV=CU9Pr{h1+HI zW|ExURX3n==bnkyD&8-t4X9wm;4A+?6a%TYFm4e6V>e6C&lnAF&(X_4J#RUYcaFG= zK6?9Cf5tz4`!~!XzbL$aS@8a84!#_WZJYXZS0=O)lB>gLz^cc1hf&|P>WrC6RA?um zo(CP7e-*&lC*5~>o>?E2>rEN3eHoa4fOnq(`wTobCSA5PeL&KOu8(%@Q{S(D-vP(C zNxk#)(d7PsZePXd{k#1IJ@f$JVE});Ouhg`bC5%UCXj3F4Oq6eOMl00>AO%C|9GG3 z=Tjem5Nbjr`OhdreS`nJk7e)8~@c>XAgK}<%KF6$E_5E?;o#U#(a0d<&AHp_`&^PpwZ z-O_#rMQ>E(!vTH2Z*RTE+6EuH=T;l(E&a?^AOZ`)wAOM!m_xw|Ih z5(P4pDd}Lf+N4+Oi3^rmN~98T0lJ^$K1=YG>+-d(0!_3EkMt_Ton$i)k0wqS)OO#opo^ zHaYo7-Xpb!whC>HzW-7RmVL{$=YnD^J!^n{NPX6}4Gl`|joasE{$a;5zsWCyeIvcr zNbmNg(yo)x=Pvj{%#(s|0%s9B2T7P>meJ_3Pf-jUea3#n~LB2mIop&)-%cR0!0zt4F(TBmK&v^I}&PthnEUU}w z)yD=U@Tad7p()y(#-It@BWC^ymcixg(}P24W8f-Ul3s z2v0xph>7lEUIFmplMW-a=kP%fR`1{IRiCI}>V-0;nbJs*;N^*XPhJ-S+rpW#R!N=guxhsz$hH8F)X)al&-r1 zVu4S&ts?JZyg%Vxi1)F+MwK!pzVcOM8Q3zoBlRQ#l<4@g*5X3v3YyA<)mXt~Cm`m0 zF!x`v;adKD0!v^dr@iQ;!w?Zxsvg;Z^KA&HCD5Phmc>Uue+Yr5F<42RfJ6p9z!IgT ztOr1A0zh>Q!QTV#dGX9JetUX3W-Y;-U29nV+;U$GEtVYje zme4`H_noy${UN2A=h6}g{oXMFIeeNKWShSHGc0+}d21hh)JkJnQXG`KRK;4t9)2AlzyafHA?0r0|8kKp_J z?McY8lQHr!wdVfi+?7*kft;B0FMtaHkwC)`!+UAp9HohpiNwCz@nIc(Eju@|9o112c`i04~xjPU#u_~R}c`H z^k9HTb&{PHOzFA@BJ%Lw?d2EoDPAuzS&raiK7f?gfvu;(SNB=nQ$qPsUyzyT@%Jfp zxZ^`8|L*wL;z#yVlfBb7rqA-tsqC!N?mzTix&**WPCEhv2IX-G9{}!6>2;LaI8!XL ztHp;#AuyH2WT3M6r`%69ke6XYV^avDoUgb>O3uIi6b@A2S@TG*r68Sm`|wa-`J4uB zpXYqTwv8IojzXXB_yVo~ez+9p9A6j=KX#sT9|p{@NP5iarw{^0m8U8k)Elu*YIr94kWCCZQprvcBZl5%>4Ku9rvHMWA44_O|8Im_^%j~Rg`6cHjXIE*ji z$F(7K2o9<=vkVvdt;Z-g5)7Caf@y%wkt{kDt?I4~{2SXrURpSvI{RTq_1%*Jc*U7V zl+${e2ydh-CqIQPjG#s;m1mP+2&Gd`!FeUOF>SG3NvkqPx&~$_fVHuWk!?g%s7i4J z6?ve~8#d(2oqpaiYjntOV|)AOW&Yl6-P^~|U^|p>h*m%VY_M2B*>c2KMcnH549{1{?Wkk}q6s8%-{&yl>ES$gPX-8uEA@mF2hT%UiZKBZd^Y`yRfKCBE0z4eZA9=>9^9q7LKI38h@fi<8 zVU4Oj6Zj}1T`}rmmO#w^FQ_oTLO?m_sP329i=ITpi$=J#^8KXpOprcu8?57 zMnV({0h@Ah(}-3APd}2)YkTi6tBreFV1LyTZ=_o8$7$C188Nx|5GC4FZrMENYG;XL z03(LBfTmE0&;xiV0wLVfc>iEUzWIz(jMso&mvqP(y5H8J`w9`g=aax^flmWJc+?rE-b!+5 z)!q&Pnu^{-VpZW2fD;s^A`c3^3Q9>4f@*o&b6=_UUsYq{5ACe`1yGwsg__p&UM!gv zj7W3ow|sZc{0jqGM6 zn*OC5LRE1=naM31W@8GYshJW1Nftv{^PEB<)>c_k5o%0VYpg`u5Mv?%q$_23T<>qs z_dZi!i5LWCUXOoukE5^E`Fly3U*b4rk4KzwnjPo<`wt)bLjZ0ETA#n*qKVF$s0I84 zIC&ul_m6M9I~yzrWZ*fpZ$EY?A_zYoxEi=T{r@J7rPsSk%&)%>$h{T4o776@tMk>E zJ=Mi^2>}&Luvten`P8_RWbGP*@xJEF%TNhP*($+%n$4_=u#t|7WdH_@puqq>HQ328 zsqdyCqWcS)mh0TEz5PE(H`Qq5+;PX|ty9yD&mf$Nu(CdfqA9MKWl`Y<#RQKHu|~OE z%LwtRQ&~bUNl;;>G>NfF#RP{J8_k!LFwqTi48(f$&KhapJ6sn>gC6}Tb91tFfBBGm zw=abGCpY*EVrDeA(A{CkeO-O;T`Ex`Z&^!J=_$i4_%smd1MUMQ=zYYvjB zErJg*^*0DHbf=vO%S4!IWZW?qg-vEmnw8F!meW~gOPLT*)s9JD`+r$QMNck5#b{F7 zcci&5LxsaQrvG+i{_>x7z@8dGD989n?K5)UJ%Bll$T{sRue^$J>B=jw;^2c1g2+gZ zCZh0+Me_@V$^8W~;;(Ujkuko>IOh_&I~hS#a+->NjEO11B4A?=z|v13>7DkeGtiou zCN1Z2N!eDN0ux$y*^Nhn2O~60Y{!>`m>Q8Atl>QZ@A8NdRVp;zgLwEk)4SVod5%J$}y!*cAFu?Q})0 z*7`P!rIbSpgZkJ0<2~kd{@RG@`*$(nh;ZNVIRAig&a=;82iHOqgT`YV?fxv*Wq&mj zw&WT=vN68%u>X7$IS2lajn%jatOp2NE->A&7>SMyY%lVzRQ>=>*-K@!k;)_46AkzB_ui|{d!FSJ0z-}+;SUg3Bu#Bl z69GaaH#vAh?u9EDCzPi1|hk^FNASYLII^;y-xtx5t|A zc|k$nb@Aop{a%00`lCMsd|u!{E1YaZ9w~4fBD+>M4zzt>rDQa$p%oNk>t?!kF|?G0 z5h+V57DRL)lFO3F^4z1=>w~p__e(H|>A$#j|KPtzZy(QBFlhhrudyr?h>pOa?%GLd zDFi|PITj}bnD9KQ-ly}1sm7^NKrCuwa-CE!c08{SiZw9-X!BgV<{L|@PQwPoT!xqn zz<)tiewhEkuAw672`khE%$SS~9hEeXmp%4+gQYX9Np{`~m=X}>{;L<@BK z4Wi{{{sl1pmBh-viJXA2I7i?lG`UM}*j_TxS%y0Shd2d$-W@?>YH#YKrCD_T#kG4} zLdRvVR`~$Vd8Qf}Q;i0*ZP)uee=G1TgTCee{_G=P-sfjuyx~({{FeXGH8*j^Pk%Y_ zye$dXvJ?`W@+8`SzDGHq=?~+s`zQDKFP{0Al<}96t_E%dc3F;-kG2TLvj!G#3G|;w z2dI^Ul3p0G$2sqf;AVtHcZ{ zf(XuJv{R^XRWpl)3`umxG`6w`B47wuiSn_m2rwabM2TUJ5Q`_g8~TWrO#2T!bX?Pa zA=g+M=HEf6#S)bU&qt#L-qH(!(Z>w9UWx7RU9{{aDmaYo3Tt->j>HcxD9t?Py%etV z&Z}60$y|UCYM^|Jr9}yVY1g6wdt3CcKz1!B#1ioU=8C|osXUtesvRU?Ix8rnfm|H6 zp5Io*LVT*CDiG(85Evf!Uu4*CkoKR*^dJ29@a>CZ{vAZ?_tF)_h`=_X?I{FC{jDZp z{b!B%UWq|%gJ7~63UD_%7R20tFP52^l@w}gka;lyNHjDt_9a2gA!;m7I`yH8p2GzT zfES%~7(i3t#kqzPb5h!GirpKcA0SWOOy^^0qW}t|gBqNw40AkLEgg7}`CfO<4T}5E zM?;qcvanF$!kvDLXZ{_DF6g8W|B~nj*k&|b5H3y#%zw=Aza##CGiv)3-isx=E*8&- zppwyrek~TS(L!!!mX_~{Bc}d#Ua2DM>K@vR2*762-*bg$Wi!JVp$MIv zzX}?>Mj5fgaIA4o0>YS-G!bj7rD#=XDce%}FU0i!17m)ODX8DtUKigMLSW=lp!E2s zW}Y$g&y-0!)h->oC4qlp2qB^Z&rCIGJ5LCG#c}oMLlZdHIC#sq%t3) zcP6g)_i@91{SSOHum8fe|DBWhYkq+5!=l5}i=s(|z(V{1{rlHE^BQ99KawohHbNlS z*e5WgUV)vQ6U8~liiRP$B6a|h4nj~l#mRV7xf;JY=n3~h zLcryXlO^~U0eS3dlDYt;?}H|POX1107~3p-Y#;84=mfnbzM_FF6cIJ0dODr6#*E29 zk%0Nkw=o~=JlcPGP5(P5^Y_rTz!$goAeK@HjAaER;M}q*FO=W4gYXlxTC>`IakKXh zOJF9q%(W9@kd02l&hk2z5*w79h^Ccnh-ZL4-mNg4Jp%rF7>#cMWEmK+=C`0D9BR^K5yfWYHjuzlAPCIs@IT}jdW#^!BVuh_vS=F>;Mkst~^h=i?pfe~XlNj^Y+H>eyrhA`!0pqaA(yaX#rv1mE zy9V?3ECW8Xy~hJf=Mxy)7a%46MOt1OQ#HNmKY^rnU$A0&in+oOG@AM`S+0)xA0fbU zlo)caUsdk|`br_}GU=VoD*#@4#*y**qK_7lKSxAL=8Nm~=#-NXP!Sw4)?cz_Ku}fM z?E->l%~YNsj{3E8{6vRIM(i#$GU3I!*m8+(?cbdvF#ZD_yw7rL|GNhB_fP?Rkcbhm z-Dwgbu<+2}i1(sgvTCP8umlIe;hbZ-nPY86t4R2OAt1ip$ssVYi;!^zwrFFEqSJP~ zLyrWWk%Ro@1XjSj0zizAYB~f&A$Z5uwr93c0MGK#bhGP4?@)QZVKX@|rlwJWTQG&H{ zKjs2Y@xpGCsHk}bfQg8~O(SwZY~mwT29u_)J{24)(VVZx1ImcL0CUb`L|8YK({d3? zOuO=$>s4BDQP9BCWB`NL-1qgc;DVih10U@8+J7lb|2sSLmsAr19|kVi-s350Dc+6w zo>9FJDCPxdwLI>eA!WsSM>DtNnWfe4my@aXzbX_mbv_U%@c^q$!mhyDNp5R?A3)^q zmWcdXO3!6x(L9em&hos%{+3kaQxFrvizERX0=Y(%(=Bb!>X{r7!If2l(sRM2@3ZRD zzZB?beyKGmSzzc-CXw=!CGjjYU;QpZw*Y@fgbm+8Y4WAO(0xk?I}M17ExSwR*VMDG zT;B%`_@JzsnWF7HMF>N#9m$F!#>9}jv4kF{VjAHs0=F-z1t5SAI*agcB5aHbTPXX2 z8|RWgpyLD#sc}INA(TlzsYH6V@Q6rE4|YCw1u9Dg(JsyFi9Us@??8RhyCM%7CtSmT z(T_)?vi{~f*HUQiyNdQNsb&TI5cq50_uG3+ur$jt9d$%a(@V+-6d&QhVntuX^K;HK zm1i_<#$4M+!62sZI!e_184g28V7W1j*jZpG2yNh#4dK>F=3`z&pi6&oJ%4oXeGuU) zRc;0L1Ey7J5Q0SWyOyM5VhmI<752HQ^$83_sk3q_XG1II{##RG21>PD=xF@-TL;fP zIthc#YmtNQ=A|7P;9whq_A_8(&%+*E3qI_)(fjUjrvHKwfkvLRgBElpa2v5J&!Ri@ zE(OVmzXty@{}Ckss8rbh8E7gEPRdCV9zfw7>sHMWRB07uKY;GHEhfyxLOFwCHG2Oe zj97)GwOQa=5qY=3`5s#Dz36)joQDzg@<$$(qJO%tD);fK`%(CN;Qc`g2NE&pR|O-h zR3s-TULrn#1SJ!22tgq;!n##c+&b&IV@rz_4O3MTT2g+2#=9$fq|Iv9of;_?F*a?B zi4%3~!1DISG5_b!`Jbc)U_L>y{9!!k001}@Nkl2l?1@P zD%=e?0OTOxpdjptvL~?8C1%SMrl&HRdB(4ApT*jYW)|BfN4KWwcvDR*lblmKz%;KV zOBh4!CM?~8qU&xKCjgel^S{Gse^_P#0F@9hz=^k~KK?@*!@Z#?*@f<9YUJy%#1tbTWBCJ+;sw%H-i!_z!5X_9Qawca(%X9OVf|a?! zn8Dq96k#hlBLP6g5b|b?q5GM(sv#It08F0qKc4{DA+}z&uufcM$|M?JFXLycwedu1%{bn3-y_cEt=Ew`|4x+G01pivNV_ z`G~pMTGLuNGu57M-Jw4?>`(dT+g`j4?th8t1t%X4uq!J6pM#Tw#0)_NOrEiFD(BkU zx1u3-74AwPNE8re$?4Vpq4r&EhGsb$lTiK_VKM=57t#KA5Ig055xSgr z%SJV8;2uOv;9kJqz|^+B`FR9D8DIWmiCv`o_URJ++_{z z!ZF)wQ+d2p5#c8Sj{&aCGRvnfzI55|e?h3glTNXU+$!kDtnv_}G)Qavb`YEz_FCEC zx;tj^L9n(zFi`@AmX5>$O<3w4)Eu}5I2YJbJ|i7Ex}{9%9X$^_C{=x3wgvOot`mS? zopHwcvU0Fj!tV%_0WSkqlyR!zr0G8)n4A(XSjx&k4HV9^c4hQ|&b5mO6_(`@?oJ)5 zeB3#%G&y{F8!&%)gUd>;(LmY-n&+$3c^g7V*-HWAm0~uq`%KQ3X2$Jp&x|o$*y;vq zeFGy_0BD3)qB{oLA$c-znZSF1@E;d`Wt&XqPMVx*LZ{f?V=3v(GfpeNJ{ovFowAlh z9krpF;#97PPCgU6UWO{5vvRkgJD2a9Du2)ONyk1#nwCmO$>hqCv~W z45{+>Qu}u%J(RW3iQ@WS1f*yu1mOZg*bDf3g=1i+5ZFnRklX`!BXCH4YoA)K74=kU z>8WnN@*`<%mb+rQNuF6|TkTQm{a}#ChAo>2Gi$cVoi3{Y@L5jCveiDzo+8#vWfIXt zrNl@}GvQR()bgyEZbVQ7=R1HPJyfq8s7N*91Zpj3Fn+%u@Mc8r0r1!}PT3}tx|8ml zbmkfB0oe`sdt$hdIu3c0`a09J;5klU061(no z_mDTY$vrQd)8)CR9Yw4QpwC$rhx>gf>HM?hu|2ykCL;9`k)& zC@q@+c;0D8K}-<(2+C7puD+sKMlc3-E^2vADH8(L7}l+rVnfSw>&7k2G%{?3d>|D3 z|Lwhbyd_6<=lMM+BJX`$RjEq5*d!>i8-WC}1va+8fLy|Ox-o4VV{CA@$MksVv1#`} z&w%ae@q(A`j~fGK7d8ePn?)cYHH$<8Vv!KE1Qtk0Ahe+ksZ?*dH!~v6{1K6P^S<}0 zR3)mUN9moW!k24?+s|FoY(Q=miZ_IcVaL)%m zy7r>}%&zA^1~$!u96>HkJ<;It?xdp8DN{i!$2MnJjwDb{}_;C$mO3YwAx;vzm35^0KWs= ze%_`NIB3=z{HFnb2|R;+BM8U99^gi@Blv3h`*m{Dl086d9YPPL{2Ra@3T&C)UGI*! z&uT~Vsi)rWBSGpWAR-*SeH-JtW;CuR@3#r-o&y*VnU|QgHAPl1ULHl?TUq377aTgJ z^+Qqy)EJPac1is&m~f0qrZAd1Qc9#4i%fDZmmr3->;UQz@r`iImLWIXwM-o&Gu}}l zM5Y^$tmnU44t0n4YZEdeL-H~}5&0Z>R{x>$_aWpPKiu1X*SGF-6&XrPc38HffG3lmf@N~^+N;UV;9qv_I_qoY|BJv2f&a{!!9OvHO=)GHd1Qxm$n2m;0Dl17 zedglW`vYboA#d5x2iqXWd9xfx4U;TuuMtaCqnSv-rLLP9s+#-9UC{T zZx01_9prsFaw7N9z_TcWe_iXIO*i#o=J(4){g=G%r(WTR0UR}}gwYBEmkvU z4MPhItzq68<}4x@D_XH4gGr*>*ao~9tU8pqpo8uJ)Ctu$u6-nxe-V`jr+HoDT*=gH zH~YI*T%^5#B8Y*hW7(9&I&sv(kQuM6G_iCQvT13go=t>OCo)Ri8~coP4q70ja}`ci;MdN2+9CP92h{=m#}}NMcdGK^qMVE?O+CHq4(8QR z6`U&KE7W^La~3p52G#+6Mopv%iQ~4;Amlb-r$KQ|Pkt_mid6+gef&Yi_xuwm$_}47k*A!j>Uwusn`M^Rnn< zihy%Sk;khPIaw~D?sE7;+lgvOC_ab1Ck;d6RvkzU4apq8B3$UY~?t5VMrYk%_?!!)ikQNX5s|8^Acm_vS)l%}NRC$e#65=k+K`=WUX+yAO1>LxHdJHudR zhTY2znxI+D1&Kg%ugYOQEoMWY@wW0sm38U_}bwWeF%N@2ZVVz&XG04@f(uBfg&9C#XV7Rh#Iw>!5F2KiDo^0zcyUYV4uLupknL7 z9CwdK3_`E;loB{suqat5*H%3|-XawNX<3C#R;(g&hY06JNQWvBc+maIt6q3I+m71G z-M8)DmCXK2iuTx;Oj9zW4#AS;qKK*`=Nb$nMgdtwWs#H&WDb!Nld2QTWte~42WUnlmQN?(<7W0JM7so$@wpd%9Dtub>p38J zgOf@2+lagrmDzjI{`)|oB-iKNPaONh&B_wsUy%V;)xCWDd%(NOb`1p*T)uY<@N6=? z_cYe1vScc6*M-M#Qv7kSl- z&cF=@AGNx^65+|YqmTi17iP->LW~X0ea}=wZY!pZIoD`v%wT&o{!(LlUKy?glu>QW zO5n&XbKJgriE$km4yq~rfVF1bD3#bu97IY;HEhGZ4+6g}A~)iN51l`0Zu{X2J_Zn; z|Gb~P3geXscL1-X0}ZWb@<{T7cjF{t2j8L4KQEyUx0a>=-@= zypBw{?a58W%;qa5sYrCERAzg?1hEO@&n(dtq~DIM3&fZhjcW|0##337i+RH3V)lQG z(d$9fNM2yjP8up4DcDR&d<_wfDxv=fk^iM4*OM&1;UkA;@WcI?TsZ)H$+Mn{Ax~E9 zoQm`$0Y0@%)SSd0x-o z^;|I9is&1G7sFl^r0UWrM*}nqbM;@?yvpv9X1Lpu~Kk)u10q-WukYxf- zY#{cuNL=pbtD#;+?y*j+wrO$;@1wSDL4d`Tao5OG1?Fwc(Cm7FUq$$Y26)S-_Ft;| z{$T*Vcc~a?pZs~HA6R11$E32S)gW4 z0$UizEjE6KlCIir%lmh^MDHOh4c*$U8)GRKO>7JBga`UM{(#%D-23|f1LP$_+D``H zx5!bzo_y~@51dx4t?#eXWG4BFrbyc#wRIjZur$g|JRON4f!Q?9xhMFabH+v3#vS~y zK^?$xtZ=L>x>@3h%356jGh=a-g%yt8I**i0Z0b$-U9!a}7;VQuVOA4P47CWcxk*ww ze`{RkZ$5hOP|J@@982gHoxr~P2iya^hm7;TS3Cdw3re`3 zE+U4{Nf=LR_^!$mM2qc@Yt_k2lu*}LGv>&x3v6=X!zVKqN8`-6+PVNhT`0!cCOBh>0UvAT;|k7FrzjvP zU8kYBRl)%`uw4PJxR(XgW7!9}WYg}|p1{7Z-_2y|?|zG!N;5*jc(;W5GD;7MEb75D z&VP*=A>>ZJJGRU-bg;6#jEfK;8UP-48#D)Wi!w0CFuQRIVOTQ7 zy`{iOz;O@swfsS|9suac?3?y`D_I0&zwNwFe7YUzx6sN*q*Mq!Mb%eZWL|4V6Cx=l zc5Ge1JI8o=i6%w@5K^K+i0X19Tj$WKf~qY z$YdtT57|T504=b7Ky98?dVGh>gte63(s|d&dVVP-c5KUoe{p$)tx}{G%k};=7<6@i zL@Gp16A@nef}ePxujgKE!#RL|f98|8YvCxgsZWy_RboQSs7TC|CW)=J%Yu{=i_1&Q z6L{RV0b4bp^_baIGmK*-r6NEoSp;7ljXDB6GMR0U`w#OiVh(YL$`hteWL2+ra+&bS z5A^l?0kz|~_XObaz>x>C)3cxTV>u$s48y6WG1+>Ze1WD!lRLI;!8^zD$_N+h?io}v zT-T}MQq21PF?OgzgQq031*z);ILrIjHk<z!jLkv_W(df zUJmSDsZkOqZ5uLhu-r6gsu^0%9D!k**)7n|l$C&fB8l?rKk?)PImMGPPVYDZM&S#T zce6tgGS6H#6M+Y8Cjk#z4bfd?51}MlckSo(HD6(>J5Zeni*&5NjJu`n-rF=F!japy z;8a;!88alXMFOSwH3Gx}>6H+&Za|Dk%*<8H9wl)6M$Q6kNCyBsTwte=mjf-G*wj%; zqEf*7DK0j^3Xa>xFf!w~BW9VY9Lse?QeZ%2AcnJvxA!m<)t8C$Cp3e5S7CjAruYtl zmt*#bZt(B@4)5duXQ@pC;7O!3oFZhcwRtiYV_|=Gxa+5$M=s;K=bE@vSHItNmbOG_ z8Y+cjwk;r{ERGwzBnFaDGC(2Y1p!Ni&f7zVPI68U#q8w!vCqc=+lF%hl27V!0y}6a zloSse8pMp5)G!ERiquy}pfL5+;@CY3)J@{}ZF6jy9k5boJ^_~7l#79wAfg1(AFYw! z8;fpv$@9+G>v_HKr_N*{E;+E50x!!aD^qxb&L$7-Bgr$>rtOE7nLI~M(lW_<*5WQE z4&kTCBB8pkDB$d8KNAUKBsGtf*!-@fFu(dURSFE^n5tgk=qmE4t&X|43@i6AFEz6g z(U>(XO*JngMRIA;63O{IS(U9MoQHy)dH_t)``sSU_RQ5n)TRsJ1t#$-u+xn5HffjcrUp+z1%)^k&^i9*8i=_gG_45S$Sw+aO=q@@4VyW zlXu2zuKDHzeTg@u^~5=!0=xo|V|%Qa)dRo1z7_B^EbW%@^4Ct@`Ka*dlOE1B-@3m0 z*xAp1CYT8sR{WQk{aF9F0?ZJYjbjK6N6t8oUKp?v5=-NN33!*g_@o#MtV9XU=f!AQ zI*8Mf4A!n7BpD8Tj|lI7%Fc(TFMo66A;UM+^5nOk|1^LtjOq`X*|RJ`5+OO4?|tf# zqC^NqSIv`H!BeBm5i+gVdr-%m8b=J2<#FJq#hQdM>m6P*sjuyfY3huWzUr}m6!=Un za-(D_;jtoeN~41ptt9uO2^l7>y7Hp+*N{C4^Zwe>L~bQ_-~A`xGi370ebTQ#oNU*# z&xGY=*t&HK@HA9kgxOh`J)(;M+9)8#=`Xu7ccp}cdJmS~2fP!}E3KMYba8Ac#){lO z;03_HC1W!tyTBM&gQW;Daon6|+f2os&l2O!eXb|nS~U6%b$M89e3zabWvNby8675EXf#J8g8t0^!a zM}$y=Dkp3ivfz}vR~iDMy9%#GIJA`iR-#d_ z)XdIQJY?GpcPx(BGYV8D43tdrP}jD65;Hjh;fR*^4WJfLe9?AOR?OvgWToHXG@iWo zqxuno**qdoCpVt_Epjw)0q{R}?K$wm@mV$!g8I7KX$l1V;BXS}l zljpE6wcWp_MN2Q~W zzAWrPlO_#B0CNtsEy3m4L9m!&x`P-@Nyizmf+wKNr-nT%4KoAJF$*)?;~VZ+stF>@ z`3bvU5WM7N0hW8d#8UQ|(a69uDr}5RElEvEUU+YyqIM0NXD=n!qJN&ONq-&idEjE; zi@SE6bshWB(PkrCFDd7Nhcg(QN=nZvYw$-QGOhDBRgUX%@y#Y89gqQD1;dXf%nvd! ztB>E`Thh%8*Njk$SQxWquHu;40izJPYaD5c{-3nvKv4|2Ly*Qqj&dqhG6)G2rP7i` zmmH}~4gL}-?9a$9h zWU>IqGk{ZpM<6ojuE}~C{{GD0Px`f3fPPZ2PyF&#-d&ic_2zvPnl1@nL_8~n?F$2D zD#zX9z)BNoTGE*M-1H;gv9AEHHTBmS>2ap=REzNxjGZR(P>=y+DJzl5IDgrdt|_zgi7+Q3nhQjNs?@6TPK3{HR4BlPb4#!K`DY>`GbyEi zol<&XjOoaf(syID%VOdyDRHftebMrsA38=Mar>SzO-#&qhj&8Nb~I8)ZUzy%-tQ|?QJX8h2we_EyP2t-r4rhVC#}v! zJ6W>H9gyh|ci*-`CbHa2mcIHrS^w@9a+GiiSFMp;TK;I#>@0%f^&J?hg%%z&c|E6^4kqLdbO~^`^hTJN~ zY^t%Q#zfe@FkpV@88^mw9I)a{5;?reT9h8=9fG|YzRN@*DgfARkP&!Yb9 zv(G@_mkr+REe+;qCfR;qU|APnj3DaRGW0kn+_l_r_exDA!n}7x=kj+_27$D+&m}5W zN&sa=ZvZW-u`M3J5*nJ)3aaqVTlC`Uu(tj8fmw!N%p580Aa~9=8r7o_Ii5WDw~=S{9rUBeHhkZkBU`<`?+b9QjtE5Z8o#XRyET5< zkA%9*`Ap8dIEmP!bz4G1jZY2pL(h)+0T?WfLy?v*d8DE_X=Fzr&SBo8A^e%C{@ytk zUb5de;uSA=I)IAEya*vhR^IsG&)%1NdcWC*S_1U#b?>_bGI6i@K6bMM@U57$0#TKi zN`E#>2ug^F-77V7mFJlG0Sg1q9ZO^Gt^)&aRBrNM#}@r7y)fP^YsRHkOwus%lO~cD zb9&Jdgz;o40FwdHpAiqt92stWZ;)dHmM{z5ai>jyEja_$zOs*`W~Z3bJO93_Zn-W~ zCB42Qa6Js#>hJUhxU|ue8Cb$xZX}b1JIa9XI|Q?Qe?!B9iyXCehS|z7sw3k%WAwF@ zW^*U13By#$&RkMLQaWIi>>JO$1hOAFqzyd}rfpaU@cml3u^vGhlm?|D;bQE{HGTeG zGFYkubuwlLjuW=ca?iNodrLKShz!fJKtyPY_~Y7x(y;=Ch*?30#S)Qs4{9bfna??~ zb+guMQE&TxeLpS0>vtGlDHNT;s$XlHY=sUmu?`eBG=AGo; z5i!u~^gN;6_5@&3GMW%@B#xbTY@eGYhQM9R4Iw5xOwIg#bg8wk1JVl1tOg$)hPL6j z00+ym#=4o9k;tN=t*nTUlY2pwRD>R_T#OJRcP}^W9!KV8XL;C;1-8r$87COmkr)%v zG752x^(n!Yx|ttj?Wa@BS?hu?U$dWnd#bMfz3TM?wW+%0UWhiUkG1CgR0gAUvvsGQ zyLul}1A3ifuRf19!RO0Tsc1QL*xLMVb_B82=btp^XquYsL*?Y{LynlAVX-#uT5f3K zB*brzDHFWseM?w1uwxGe}WoJJ1K0kv$2zJ;;0A;w(ClFV%!3l7ZlXTIPSONGJ z2kAobRuQHb)G~@tFEz}0L06}?fYx<*4m+J9J!=RJTPrwrOT|{N>}dkuTWN^Knh+nz0I}6h zsav9SaPtLoROFn=VQYu21DLmfU`J$VPrjphE!mdw%E(Nydq~>H&8cPw5^BZ)nmRB$ zGvN3w1D0nT-(8OEZURmO@BP|{xqavgQjk_sx@sTd{+hKnVAjmY)(d@mr7xH&iA@Pw z?u|j$ey)GAtG}~e>CGsOR?E!X%f9E&=wz_3-p^FhPRfk#^Yw@^@1`{3Sd%0yzgc1^ z4#DSoe^Zkz5kkPHzzzv)+v=IA98E~vv0PI}BbDYJZ8^8^hx21F9hFH8z1NKXBIIzk z!`1;fpMAch)N|_4&cM0%U`a3)%>ikF!P0F%E z)oMUbgc>WJ#!9Ya`ZC984 zUN5N38RpE^>v-I)wIO1|#Ce36s85i50#1i_c3fpqKE{D7O4 z@)pMGHL;|KIVsD6*gIm+S+t5Z{cQT`SrevTZ@oC&=Ogt$zgq0;MMF!q;+(N~1Ria78_f`Vq7)V%#l8cbc z5LKKAh9j0_Uw4FZom)qct2ow;b_Gz`o~mjnW(V3F_6N!iO9$X%#98zW)pje)j?`s) zCZ99P0~vCgC+n4_S_Ps+jsT>zE^kB_M;Nb+8T-g^IK%cMwz8#ZSX>#gGHz%>#JLPD z)-w2XRi0tW`uO%~CJ~+VSpg$;1Gw#2t7W~sI?2}m+YirI56U&A$Mr*Tq#S?M>@P9H z3NpZ{g#0CH$PHQC1Pa}T|ITcxLY9aT2j4WfD#du3iJf4s!n72Ay@Lo+W&}xTGVsYH z9x9KhnvxFVI$5^E(*d}NDoZABD;DDcH*&A4uIeY!*hT4vpx5NjV{LsD2X zNF^oA6$PVI@C5~Alyep>RJIp#uV&S)UJi6SUxhDd2XtHNn?l@W1e9RCjC}9?6zF54 ziY8Wvpfu+uc!;*55HkQbB_lL}7#h?PgQ{ZN%#h)*!doKNBVvf%{YvUli-a=!D_taT zr@=Xv(mR2x4W5MP&tdigjm=Iq6Z0i1v52PBqwxYxp>kQ;L2Xu?`VC6rJ7{)TIsl!S znK*7Mz>XA22GMy8kHnP2dsPrM;2O-{kIDN4t`w|!#|4-0E4!X?oxmTcvSjN1ETx*nPip4~#mvGK8tY#T(nIl$o=axjg__^B22@M+vP)B87YG?59oHuke5Gsz20{MMf_6A`4Fe~ z3sX4?=}~mM<{SCfWu|vbd3`S6D`9?qmf6Z@*|r!+D@)WVL5g{7dPg)!CM5HdvaK0a z3V2KZ7~oZ6R{!lMFXiW-`=ghpYVajDT6(jj^sfc}4aO5C#Y$sngPP($1uKK!rHG2m zTCVRt(2w&WwZqc^R2AkJyCm03N;%taG3x}Xl2kD7z^KKzf@J@o@WCW@onYy07fkvD zzVrOg^2=vE9gJ&$WmBDl>^P*iJ~Zfh$>+2}B!nf#;|BbYK~=G1+kli3qo!fGWUW)v zprTo6#(PA4o;5qM(Fw}V#8XM6N;xkjDN>_^j+R_XXXJbZI#>fFlGIG)0i-P_rpaLL zh)ruS_r(FllFU?BCS?H&tCloO_NNXDO;%qjoA^ZK9P=|3!$C!*ils7xFh`A9= z5JAn*BLC!^!^&)QzN!DNBiUQ`E!lKEup6_91L)0s21GMr!J7M$g&wBBaszvsyae#xF{~

@vKqB-)95r;8TITi*L3o`>|#0X7f+^i5opeaiP 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 JPDHLkV1n%>d=mfw literal 0 HcmV?d00001 diff --git a/libs/systray-rs/src/api/cocoa/mod.rs b/libs/systray-rs/src/api/cocoa/mod.rs new file mode 100644 index 00000000000..bdfcd0d5188 --- /dev/null +++ b/libs/systray-rs/src/api/cocoa/mod.rs @@ -0,0 +1,28 @@ +use crate::Error; +use std; + +pub struct Window {} + +impl Window { + pub fn new() -> Result { + Err(Error::NotImplementedError) + } + pub fn quit(&self) { + unimplemented!() + } + pub fn set_tooltip(&self, _: &str) -> Result<(), Error> { + unimplemented!() + } + pub fn add_menu_item(&self, _: &str, _: F) -> Result + where + F: std::ops::Fn(&Window) -> () + 'static, + { + unimplemented!() + } + pub fn wait_for_message(&mut self) { + unimplemented!() + } + pub fn set_icon_from_buffer(&self, _: &[u8], _: u32, _: u32) -> Result<(), Error> { + unimplemented!() + } +} diff --git a/libs/systray-rs/src/api/linux/mod.rs b/libs/systray-rs/src/api/linux/mod.rs new file mode 100644 index 00000000000..176d77e34e1 --- /dev/null +++ b/libs/systray-rs/src/api/linux/mod.rs @@ -0,0 +1,182 @@ +use crate::{Error, SystrayEvent}; +use glib; +use gtk::{ + self, MenuShellExt, GtkMenuItemExt, WidgetExt +}; +use libappindicator::{AppIndicator, AppIndicatorStatus}; +use std::{ + self, + cell::RefCell, + collections::HashMap, + sync::mpsc::{channel, Sender}, + thread, +}; + +// Gtk specific struct that will live only in the Gtk thread, since a lot of the +// base types involved don't implement Send (for good reason). +pub struct GtkSystrayApp { + menu: gtk::Menu, + ai: RefCell, + menu_items: RefCell>, + event_tx: Sender, +} + +thread_local!(static GTK_STASH: RefCell> = RefCell::new(None)); + +pub struct MenuItemInfo { + mid: u32, + title: String, + tooltip: String, + disabled: bool, + checked: bool, +} + +type Callback = Box<(Fn(&GtkSystrayApp) -> () + 'static)>; + +// Convenience function to clean up thread local unwrapping +fn run_on_gtk_thread(f: F) +where + F: std::ops::Fn(&GtkSystrayApp) -> () + Send + 'static, +{ + // Note this is glib, not gtk. Calling gtk::idle_add will panic us due to + // being on different threads. glib::idle_add can run across threads. + glib::idle_add(move || { + GTK_STASH.with(|stash| { + let stash = stash.borrow(); + let stash = stash.as_ref(); + if let Some(stash) = stash { + f(stash); + } + }); + gtk::prelude::Continue(false) + }); +} + +impl GtkSystrayApp { + pub fn new(event_tx: Sender) -> Result { + if let Err(e) = gtk::init() { + return Err(Error::OsError(format!("{}", "Gtk init error!"))); + } + let mut m = gtk::Menu::new(); + let mut ai = AppIndicator::new("", ""); + ai.set_status(AppIndicatorStatus::Active); + ai.set_menu(&mut m); + Ok(GtkSystrayApp { + menu: m, + ai: RefCell::new(ai), + menu_items: RefCell::new(HashMap::new()), + event_tx: event_tx, + }) + } + + pub fn systray_menu_selected(&self, menu_id: u32) { + self.event_tx + .send(SystrayEvent { + menu_index: menu_id as u32, + }) + .ok(); + } + + pub fn add_menu_separator(&self, item_idx: u32) { + //let mut menu_items = self.menu_items.borrow_mut(); + let m = gtk::SeparatorMenuItem::new(); + self.menu.append(&m); + //menu_items.insert(item_idx, m); + self.menu.show_all(); + } + + pub fn add_menu_entry(&self, item_idx: u32, item_name: &str) { + let mut menu_items = self.menu_items.borrow_mut(); + if menu_items.contains_key(&item_idx) { + let m: >k::MenuItem = menu_items.get(&item_idx).unwrap(); + m.set_label(item_name); + self.menu.show_all(); + return; + } + let m = gtk::MenuItem::new_with_label(item_name); + self.menu.append(&m); + m.connect_activate(move |_| { + run_on_gtk_thread(move |stash: &GtkSystrayApp| { + stash.systray_menu_selected(item_idx); + }); + }); + menu_items.insert(item_idx, m); + self.menu.show_all(); + } + + pub fn set_icon_from_file(&self, file: &str) { + let mut ai = self.ai.borrow_mut(); + ai.set_icon_full(file, "icon"); + } +} + +pub struct Window { + gtk_loop: Option>, +} + +impl Window { + pub fn new(event_tx: Sender) -> Result { + let (tx, rx) = channel(); + let gtk_loop = thread::spawn(move || { + GTK_STASH.with(|stash| match GtkSystrayApp::new(event_tx) { + Ok(data) => { + (*stash.borrow_mut()) = Some(data); + tx.send(Ok(())); + } + Err(e) => { + tx.send(Err(e)); + return; + } + }); + gtk::main(); + }); + match rx.recv().unwrap() { + Ok(()) => Ok(Window { + gtk_loop: Some(gtk_loop), + }), + Err(e) => Err(e), + } + } + + pub fn add_menu_entry(&self, item_idx: u32, item_name: &str) -> Result<(), Error> { + let n = item_name.to_owned().clone(); + run_on_gtk_thread(move |stash: &GtkSystrayApp| { + stash.add_menu_entry(item_idx, &n); + }); + Ok(()) + } + + pub fn add_menu_separator(&self, item_idx: u32) -> Result<(), Error> { + run_on_gtk_thread(move |stash: &GtkSystrayApp| { + stash.add_menu_separator(item_idx); + }); + Ok(()) + } + + pub fn set_icon_from_file(&self, file: &str) -> Result<(), Error> { + let n = file.to_owned().clone(); + run_on_gtk_thread(move |stash: &GtkSystrayApp| { + stash.set_icon_from_file(&n); + }); + Ok(()) + } + + pub fn set_icon_from_resource(&self, resource: &str) -> Result<(), Error> { + panic!("Not implemented on this platform!"); + } + + pub fn shutdown(&self) -> Result<(), Error> { + Ok(()) + } + + pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { + panic!("Not implemented on this platform!"); + } + + pub fn quit(&self) { + glib::idle_add(|| { + gtk::main_quit(); + glib::Continue(false) + }); + } +} diff --git a/libs/systray-rs/src/api/mod.rs b/libs/systray-rs/src/api/mod.rs new file mode 100644 index 00000000000..d9dd0ff9736 --- /dev/null +++ b/libs/systray-rs/src/api/mod.rs @@ -0,0 +1,11 @@ +#[cfg(target_os = "windows")] +#[path = "win32/mod.rs"] +pub mod api; + +#[cfg(target_os = "linux")] +#[path = "linux/mod.rs"] +pub mod api; + +#[cfg(target_os = "macos")] +#[path = "cocoa/mod.rs"] +pub mod api; diff --git a/libs/systray-rs/src/api/win32/mod.rs b/libs/systray-rs/src/api/win32/mod.rs new file mode 100644 index 00000000000..8247e71ffa8 --- /dev/null +++ b/libs/systray-rs/src/api/win32/mod.rs @@ -0,0 +1,460 @@ +use crate::{Error, SystrayEvent}; +use std; +use std::cell::RefCell; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::sync::mpsc::{channel, Sender}; +use std::thread; +use winapi::{ + ctypes::{c_ulong, c_ushort}, + shared::{ + basetsd::ULONG_PTR, + guiddef::GUID, + minwindef::{DWORD, HINSTANCE, LPARAM, LRESULT, PBYTE, TRUE, UINT, WPARAM}, + ntdef::LPCWSTR, + windef::{HBITMAP, HBRUSH, HICON, HMENU, HWND, POINT}, + }, + um::{ + errhandlingapi, libloaderapi, + shellapi::{ + self, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, + }, + winuser::{ + self, CW_USEDEFAULT, IMAGE_ICON, LR_DEFAULTCOLOR, LR_LOADFROMFILE, MENUINFO, + MENUITEMINFOW, MFT_SEPARATOR, MFT_STRING, MIIM_FTYPE, MIIM_ID, MIIM_STATE, MIIM_STRING, + MIM_APPLYTOSUBMENUS, MIM_STYLE, MNS_NOTIFYBYPOS, WM_DESTROY, WM_USER, WNDCLASSW, + WS_OVERLAPPEDWINDOW, + }, + }, +}; + +// Got this idea from glutin. Yay open source! Boo stupid winproc! Even more boo +// doing SetLongPtr tho. +thread_local!(static WININFO_STASH: RefCell> = RefCell::new(None)); + +fn to_wstring(str: &str) -> Vec { + OsStr::new(str) + .encode_wide() + .chain(Some(0).into_iter()) + .collect::>() +} + +#[derive(Clone)] +struct WindowInfo { + pub hwnd: HWND, + pub hinstance: HINSTANCE, + pub hmenu: HMENU, +} + +unsafe impl Send for WindowInfo {} +unsafe impl Sync for WindowInfo {} + +#[derive(Clone)] +struct WindowsLoopData { + pub info: WindowInfo, + pub tx: Sender, +} + +unsafe fn get_win_os_error(msg: &str) -> Error { + Error::OsError(format!("{}: {}", &msg, errhandlingapi::GetLastError())) +} + +unsafe extern "system" fn window_proc( + h_wnd: HWND, + msg: UINT, + w_param: WPARAM, + l_param: LPARAM, +) -> LRESULT { + if msg == winuser::WM_MENUCOMMAND { + WININFO_STASH.with(|stash| { + let stash = stash.borrow(); + let stash = stash.as_ref(); + if let Some(stash) = stash { + let menu_id = winuser::GetMenuItemID(stash.info.hmenu, w_param as i32) as i32; + if menu_id != -1 { + stash + .tx + .send(SystrayEvent { + menu_index: menu_id as u32, + }) + .ok(); + } + } + }); + } + + if msg == WM_USER + 1 { + if l_param as UINT == winuser::WM_LBUTTONUP || l_param as UINT == winuser::WM_RBUTTONUP { + let mut p = POINT { x: 0, y: 0 }; + if winuser::GetCursorPos(&mut p as *mut POINT) == 0 { + return 1; + } + winuser::SetForegroundWindow(h_wnd); + WININFO_STASH.with(|stash| { + let stash = stash.borrow(); + let stash = stash.as_ref(); + if let Some(stash) = stash { + winuser::TrackPopupMenu( + stash.info.hmenu, + 0, + p.x, + p.y, + (winuser::TPM_BOTTOMALIGN | winuser::TPM_LEFTALIGN) as i32, + h_wnd, + std::ptr::null_mut(), + ); + } + }); + } + } + if msg == winuser::WM_DESTROY { + winuser::PostQuitMessage(0); + } + return winuser::DefWindowProcW(h_wnd, msg, w_param, l_param); +} + +fn get_nid_struct(hwnd: &HWND) -> NOTIFYICONDATAW { + NOTIFYICONDATAW { + cbSize: std::mem::size_of::() as DWORD, + hWnd: *hwnd, + uID: 0x1 as UINT, + uFlags: 0 as UINT, + uCallbackMessage: 0 as UINT, + hIcon: 0 as HICON, + szTip: [0 as u16; 128], + dwState: 0 as DWORD, + dwStateMask: 0 as DWORD, + szInfo: [0 as u16; 256], + u: Default::default(), + szInfoTitle: [0 as u16; 64], + dwInfoFlags: 0 as UINT, + guidItem: GUID { + Data1: 0 as c_ulong, + Data2: 0 as c_ushort, + Data3: 0 as c_ushort, + Data4: [0; 8], + }, + hBalloonIcon: 0 as HICON, + } +} + +fn get_menu_item_struct() -> MENUITEMINFOW { + MENUITEMINFOW { + cbSize: std::mem::size_of::() as UINT, + fMask: 0 as UINT, + fType: 0 as UINT, + fState: 0 as UINT, + wID: 0 as UINT, + hSubMenu: 0 as HMENU, + hbmpChecked: 0 as HBITMAP, + hbmpUnchecked: 0 as HBITMAP, + dwItemData: 0 as ULONG_PTR, + dwTypeData: std::ptr::null_mut(), + cch: 0 as u32, + hbmpItem: 0 as HBITMAP, + } +} + +unsafe fn init_window() -> Result { + let class_name = to_wstring("my_window"); + let hinstance: HINSTANCE = libloaderapi::GetModuleHandleA(std::ptr::null_mut()); + let wnd = WNDCLASSW { + style: 0, + lpfnWndProc: Some(window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: 0 as HINSTANCE, + hIcon: winuser::LoadIconW(0 as HINSTANCE, winuser::IDI_APPLICATION), + hCursor: winuser::LoadCursorW(0 as HINSTANCE, winuser::IDI_APPLICATION), + hbrBackground: 16 as HBRUSH, + lpszMenuName: 0 as LPCWSTR, + lpszClassName: class_name.as_ptr(), + }; + if winuser::RegisterClassW(&wnd) == 0 { + return Err(get_win_os_error("Error creating window class")); + } + let hwnd = winuser::CreateWindowExW( + 0, + class_name.as_ptr(), + to_wstring("rust_systray_window").as_ptr(), + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + 0, + CW_USEDEFAULT, + 0, + 0 as HWND, + 0 as HMENU, + 0 as HINSTANCE, + std::ptr::null_mut(), + ); + if hwnd == std::ptr::null_mut() { + return Err(get_win_os_error("Error creating window")); + } + let mut nid = get_nid_struct(&hwnd); + nid.uID = 0x1; + nid.uFlags = NIF_MESSAGE; + nid.uCallbackMessage = WM_USER + 1; + if shellapi::Shell_NotifyIconW(NIM_ADD, &mut nid as *mut NOTIFYICONDATAW) == 0 { + return Err(get_win_os_error("Error adding menu icon")); + } + // Setup menu + let hmenu = winuser::CreatePopupMenu(); + let m = MENUINFO { + cbSize: std::mem::size_of::() as DWORD, + fMask: MIM_APPLYTOSUBMENUS | MIM_STYLE, + dwStyle: MNS_NOTIFYBYPOS, + cyMax: 0 as UINT, + hbrBack: 0 as HBRUSH, + dwContextHelpID: 0 as DWORD, + dwMenuData: 0 as ULONG_PTR, + }; + if winuser::SetMenuInfo(hmenu, &m as *const MENUINFO) == 0 { + return Err(get_win_os_error("Error setting up menu")); + } + + Ok(WindowInfo { + hwnd: hwnd, + hmenu: hmenu, + hinstance: hinstance, + }) +} + +unsafe fn run_loop() { + log::debug!("Running windows loop"); + // Run message loop + let mut msg = winuser::MSG { + hwnd: 0 as HWND, + message: 0 as UINT, + wParam: 0 as WPARAM, + lParam: 0 as LPARAM, + time: 0 as DWORD, + pt: POINT { x: 0, y: 0 }, + }; + loop { + winuser::GetMessageW(&mut msg, 0 as HWND, 0, 0); + if msg.message == winuser::WM_QUIT { + break; + } + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); + } + log::debug!("Leaving windows run loop"); +} + +pub struct Window { + info: WindowInfo, + windows_loop: Option>, +} + +impl Window { + pub fn new(event_tx: Sender) -> Result { + let (tx, rx) = channel(); + let windows_loop = thread::spawn(move || { + unsafe { + let i = init_window(); + let k; + match i { + Ok(j) => { + tx.send(Ok(j.clone())).ok(); + k = j; + } + Err(e) => { + // If creation didn't work, return out of the thread. + tx.send(Err(e)).ok(); + return; + } + }; + WININFO_STASH.with(|stash| { + let data = WindowsLoopData { + info: k, + tx: event_tx, + }; + (*stash.borrow_mut()) = Some(data); + }); + run_loop(); + } + }); + let info = match rx.recv().unwrap() { + Ok(i) => i, + Err(e) => { + return Err(e); + } + }; + let w = Window { + info: info, + windows_loop: Some(windows_loop), + }; + Ok(w) + } + + pub fn quit(&mut self) { + unsafe { + winuser::PostMessageW(self.info.hwnd, WM_DESTROY, 0 as WPARAM, 0 as LPARAM); + } + if let Some(t) = self.windows_loop.take() { + t.join().ok(); + } + } + + pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { + // Add Tooltip + log::debug!("Setting tooltip to {}", tooltip); + // Gross way to convert String to [i8; 128] + // TODO: Clean up conversion, test for length so we don't panic at runtime + let tt = tooltip.as_bytes().clone(); + let mut nid = get_nid_struct(&self.info.hwnd); + for i in 0..tt.len() { + nid.szTip[i] = tt[i] as u16; + } + nid.uFlags = NIF_TIP; + unsafe { + if shellapi::Shell_NotifyIconW(NIM_MODIFY, &mut nid as *mut NOTIFYICONDATAW) == 0 { + return Err(get_win_os_error("Error setting tooltip")); + } + } + Ok(()) + } + + pub fn add_menu_entry(&self, item_idx: u32, item_name: &str) -> Result<(), Error> { + let mut st = to_wstring(item_name); + let mut item = get_menu_item_struct(); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE; + item.fType = MFT_STRING; + item.wID = item_idx; + item.dwTypeData = st.as_mut_ptr(); + item.cch = (item_name.len() * 2) as u32; + unsafe { + if winuser::InsertMenuItemW(self.info.hmenu, item_idx, 1, &item as *const MENUITEMINFOW) + == 0 + { + return Err(get_win_os_error("Error inserting menu item")); + } + } + Ok(()) + } + + pub fn remove_menu_entry(&self, item_idx: u32) { + unsafe { winuser::RemoveMenu(self.info.hmenu, item_idx, 1 as _); } + } + + pub fn add_menu_separator(&self, item_idx: u32) -> Result<(), Error> { + let mut item = get_menu_item_struct(); + item.fMask = MIIM_FTYPE; + item.fType = MFT_SEPARATOR; + item.wID = item_idx; + unsafe { + if winuser::InsertMenuItemW(self.info.hmenu, item_idx, 1, &item as *const MENUITEMINFOW) + == 0 + { + return Err(get_win_os_error("Error inserting separator")); + } + } + Ok(()) + } + + fn set_icon(&self, icon: HICON) -> Result<(), Error> { + unsafe { + let mut nid = get_nid_struct(&self.info.hwnd); + nid.uFlags = NIF_ICON; + nid.hIcon = icon; + if shellapi::Shell_NotifyIconW(NIM_MODIFY, &mut nid as *mut NOTIFYICONDATAW) == 0 { + return Err(get_win_os_error("Error setting icon")); + } + } + Ok(()) + } + + pub fn set_icon_from_resource(&self, resource_name: &str) -> Result<(), Error> { + let icon; + unsafe { + icon = winuser::LoadImageW( + self.info.hinstance, + to_wstring(&resource_name).as_ptr(), + IMAGE_ICON, + 64, + 64, + 0, + ) as HICON; + if icon == std::ptr::null_mut() as HICON { + return Err(get_win_os_error("Error setting icon from resource")); + } + } + self.set_icon(icon) + } + + pub fn set_icon_from_file(&self, icon_file: &str) -> Result<(), Error> { + let wstr_icon_file = to_wstring(&icon_file); + let hicon; + unsafe { + hicon = winuser::LoadImageW( + std::ptr::null_mut() as HINSTANCE, + wstr_icon_file.as_ptr(), + IMAGE_ICON, + 64, + 64, + LR_LOADFROMFILE, + ) as HICON; + if hicon == std::ptr::null_mut() as HICON { + return Err(get_win_os_error("Error setting icon from file")); + } + } + self.set_icon(hicon) + } + + pub fn set_icon_from_buffer( + &self, + buffer: &[u8], + width: u32, + height: u32, + ) -> Result<(), Error> { + let offset = unsafe { + winuser::LookupIconIdFromDirectoryEx( + buffer.as_ptr() as PBYTE, + TRUE, + width as i32, + height as i32, + LR_DEFAULTCOLOR, + ) + }; + + if offset != 0 { + let icon_data = &buffer[offset as usize..]; + let hicon = unsafe { + winuser::CreateIconFromResourceEx( + icon_data.as_ptr() as PBYTE, + icon_data.len() as u32, + TRUE, + 0x30000, + width as i32, + height as i32, + LR_DEFAULTCOLOR, + ) + }; + + if hicon == std::ptr::null_mut() as HICON { + return Err(unsafe { get_win_os_error("Cannot load icon from the buffer") }); + } + + self.set_icon(hicon) + } else { + Err(unsafe { get_win_os_error("Error setting icon from buffer") }) + } + } + + pub fn shutdown(&self) -> Result<(), Error> { + unsafe { + let mut nid = get_nid_struct(&self.info.hwnd); + nid.uFlags = NIF_ICON; + if shellapi::Shell_NotifyIconW(NIM_DELETE, &mut nid as *mut NOTIFYICONDATAW) == 0 { + return Err(get_win_os_error("Error deleting icon from menu")); + } + } + Ok(()) + } +} + +impl Drop for Window { + fn drop(&mut self) { + self.shutdown().ok(); + } +} diff --git a/libs/systray-rs/src/lib.rs b/libs/systray-rs/src/lib.rs new file mode 100644 index 00000000000..dfc2dcbaa03 --- /dev/null +++ b/libs/systray-rs/src/lib.rs @@ -0,0 +1,192 @@ +// Systray Lib +pub mod api; + +use std::{ + collections::HashMap, + error, fmt, + sync::mpsc::{channel, Receiver}, +}; + +type BoxedError = Box; + +#[derive(Debug)] +pub enum Error { + OsError(String), + NotImplementedError, + UnknownError, + Error(BoxedError), +} + +impl From for Error { + fn from(value: BoxedError) -> Self { + Error::Error(value) + } +} + +pub struct SystrayEvent { + menu_index: u32, +} + +impl error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use self::Error::*; + + match *self { + OsError(ref err_str) => write!(f, "OsError: {}", err_str), + NotImplementedError => write!(f, "Functionality is not implemented yet"), + UnknownError => write!(f, "Unknown error occurrred"), + Error(ref e) => write!(f, "Error: {}", e), + } + } +} + +pub struct Application { + window: api::api::Window, + menu_idx: u32, + callback: HashMap, + // Each platform-specific window module will set up its own thread for + // dealing with the OS main loop. Use this channel for receiving events from + // that thread. + rx: Receiver, + timer: Option<(std::time::Duration, Callback)>, +} + +type Callback = + Box<(dyn FnMut(&mut Application) -> Result<(), BoxedError> + Send + Sync + 'static)>; + +fn make_callback(mut f: F) -> Callback +where + F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, + E: error::Error + Send + Sync + 'static, +{ + Box::new(move |a: &mut Application| match f(a) { + Ok(()) => Ok(()), + Err(e) => Err(Box::new(e) as BoxedError), + }) as Callback +} + +impl Application { + pub fn new() -> Result { + let (event_tx, event_rx) = channel(); + match api::api::Window::new(event_tx) { + Ok(w) => Ok(Application { + window: w, + menu_idx: 0, + callback: HashMap::new(), + rx: event_rx, + timer: None, + }), + Err(e) => Err(e), + } + } + + pub fn set_timer( + &mut self, + interval: std::time::Duration, + callback: F, + ) -> Result<(), Error> + where + F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, + E: error::Error + Send + Sync + 'static, + { + self.timer = Some((interval, make_callback(callback))); + Ok(()) + } + + pub fn add_menu_item(&mut self, item_name: &str, f: F) -> Result + where + F: FnMut(&mut Application) -> Result<(), E> + Send + Sync + 'static, + E: error::Error + Send + Sync + 'static, + { + let idx = self.menu_idx; + if let Err(e) = self.window.add_menu_entry(idx, item_name) { + return Err(e); + } + self.callback.insert(idx, make_callback(f)); + self.menu_idx += 1; + Ok(idx) + } + + pub fn remove_menu_item(&mut self, pos: u32) { + self.window.remove_menu_entry(pos); + self.callback.remove(&pos); + } + + pub fn add_menu_separator(&mut self) -> Result { + let idx = self.menu_idx; + if let Err(e) = self.window.add_menu_separator(idx) { + return Err(e); + } + self.menu_idx += 1; + Ok(idx) + } + + pub fn set_icon_from_file(&self, file: &str) -> Result<(), Error> { + self.window.set_icon_from_file(file) + } + + pub fn set_icon_from_resource(&self, resource: &str) -> Result<(), Error> { + self.window.set_icon_from_resource(resource) + } + + #[cfg(target_os = "windows")] + pub fn set_icon_from_buffer( + &self, + buffer: &[u8], + width: u32, + height: u32, + ) -> Result<(), Error> { + self.window.set_icon_from_buffer(buffer, width, height) + } + + pub fn shutdown(&self) -> Result<(), Error> { + self.window.shutdown() + } + + pub fn set_tooltip(&self, tooltip: &str) -> Result<(), Error> { + self.window.set_tooltip(tooltip) + } + + pub fn quit(&mut self) { + self.window.quit() + } + + pub fn wait_for_message(&mut self) -> Result<(), Error> { + loop { + let mut msg = None; + if let Some((interval, _)) = self.timer.as_ref() { + match self.rx.recv_timeout(interval.clone()) { + Ok(m) => msg = Some(m), + Err(_) => {} + } + } else { + match self.rx.recv() { + Ok(m) => msg = Some(m), + Err(_) => { + self.quit(); + break; + } + } + } + if let Some(msg) = msg { + if let Some(mut f) = self.callback.remove(&msg.menu_index) { + f(self)?; + self.callback.insert(msg.menu_index, f); + } + } else if let Some((interval, mut callback)) = self.timer.take() { + callback(self)?; + self.timer = Some((interval, callback)); + } + } + + Ok(()) + } +} + +impl Drop for Application { + fn drop(&mut self) { + self.shutdown().ok(); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000000..d7070c6d6ed --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,94 @@ +use crate::client::*; +use hbb_common::{ + config::PeerConfig, + log, + message_proto::*, + protobuf::Message as _, + tokio::{self, sync::mpsc}, + Stream, +}; +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] +pub struct Session { + id: String, + lc: Arc>, + sender: mpsc::UnboundedSender, + password: String, +} + +impl Session { + pub fn new(id: &str, sender: mpsc::UnboundedSender) -> Self { + let mut password = "".to_owned(); + if PeerConfig::load(id).password.is_empty() { + password = rpassword::read_password_from_tty(Some("Enter password: ")).unwrap(); + } + let session = Self { + id: id.to_owned(), + sender, + password, + lc: Default::default(), + }; + session + .lc + .write() + .unwrap() + .initialize(id.to_owned(), false, true); + session + } +} + +#[async_trait] +impl Interface for Session { + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + if msgtype == "input-password" { + self.sender + .send(Data::Login((self.password.clone(), true))) + .ok(); + } else if msgtype == "re-input-password" { + log::error!("{}: {}", title, text); + let pass = rpassword::read_password_from_tty(Some("Enter password: ")).unwrap(); + self.sender.send(Data::Login((pass, true))).ok(); + } else if msgtype.contains("error") { + log::error!("{}: {}: {}", msgtype, title, text); + } else { + log::info!("{}: {}: {}", msgtype, title, text); + } + } + + fn handle_login_error(&mut self, err: &str) -> bool { + self.lc.write().unwrap().handle_login_error(err, self) + } + + fn handle_peer_info(&mut self, pi: PeerInfo) { + let username = self.lc.read().unwrap().get_username(&pi); + self.lc.write().unwrap().handle_peer_info(username, pi); + } + + async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + handle_test_delay(t, peer).await; + } +} + +#[tokio::main(basic_scheduler)] +pub async fn start_one_port_forward(id: String, port: i32, remote_host: String, remote_port: i32) { + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + handler.lc.write().unwrap().port_forward = (remote_host, remote_port); + if let Err(err) = + crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await + { + log::error!("Failed to listen on {}: {}", port, err); + } + log::info!("port forward (:{}) exit", port); +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 00000000000..d2fea14c356 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,1113 @@ +pub use async_trait::async_trait; +#[cfg(not(any(target_os = "android")))] +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, StreamConfig, +}; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + bail, + config::{Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT}, + log, + message_proto::*, + protobuf::Message as _, + rendezvous_proto::*, + sodiumoxide::crypto::{box_, secretbox, sign}, + tcp::FramedStream, + timeout, + tokio::time::Duration, + AddrMangle, ResultType, Stream, +}; +use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +use scrap::{Decoder, Image, VideoCodecId}; +use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + net::SocketAddr, + ops::Deref, + sync::{Arc, Mutex, RwLock}, +}; +use uuid::Uuid; + +pub const SEC30: Duration = Duration::from_secs(30); + +pub struct Client; + +#[cfg(not(any(target_os = "android")))] +lazy_static::lazy_static! { +static ref AUDIO_HOST: Host = cpal::default_host(); +} + +cfg_if::cfg_if! { + if #[cfg(target_os = "android")] { + +use libc::{c_float, c_int, c_void}; +use std::cell::RefCell; +type Oboe = *mut c_void; +extern "C" { + fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; + fn push_oboe_data(oboe: Oboe, d: *const c_float, n: c_int); + fn destroy_oboe_player(oboe: Oboe); +} + +struct OboePlayer { + raw: Oboe, +} + +impl Default for OboePlayer { + fn default() -> Self { + Self { + raw: std::ptr::null_mut(), + } + } +} + +impl OboePlayer { + fn new(channels: i32, sample_rate: i32) -> Self { + unsafe { + Self { + raw: create_oboe_player(channels, sample_rate), + } + } + } + + fn is_null(&self) -> bool { + self.raw.is_null() + } + + fn push(&mut self, d: &[f32]) { + if self.raw.is_null() { + return; + } + unsafe { + push_oboe_data(self.raw, d.as_ptr(), d.len() as _); + } + } +} + +impl Drop for OboePlayer { + fn drop(&mut self) { + unsafe { + if !self.raw.is_null() { + destroy_oboe_player(self.raw); + } + } + } +} + +} +} + +impl Client { + pub async fn start(peer: &str) -> ResultType<(Stream, bool)> { + // to-do: remember the port for each peer, so that we can retry easier + let any_addr = Config::get_any_listen_addr(); + let rendezvous_server = crate::get_rendezvous_server(1_000).await; + log::info!("rendezvous server: {}", rendezvous_server); + let mut socket = FramedStream::new(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + let my_addr = socket.get_ref().local_addr()?; + let mut pk = Vec::new(); + let mut relay_server = "".to_owned(); + + let start = std::time::Instant::now(); + let mut peer_addr = any_addr; + let mut peer_nat_type = NatType::UNKNOWN_NAT; + let my_nat_type = crate::get_nat_type(100).await; + let mut is_local = false; + for i in 1..=3 { + log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); + let mut msg_out = RendezvousMessage::new(); + use hbb_common::protobuf::ProtobufEnum; + let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT); + msg_out.set_punch_hole_request(PunchHoleRequest { + id: peer.to_owned(), + nat_type: nat_type.into(), + ..Default::default() + }); + socket.send(&msg_out).await?; + if let Some(Ok(bytes)) = socket.next_timeout(i * 3000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::punch_hole_response(ph)) => { + if ph.socket_addr.is_empty() { + match ph.failure.enum_value_or_default() { + punch_hole_response::Failure::ID_NOT_EXIST => { + bail!("ID not exist"); + } + punch_hole_response::Failure::OFFLINE => { + bail!("Remote desktop is offline"); + } + _ => {} + } + } else { + peer_nat_type = ph.get_nat_type(); + is_local = ph.get_is_local(); + pk = ph.pk; + relay_server = ph.relay_server; + peer_addr = AddrMangle::decode(&ph.socket_addr); + log::info!("Hole Punched {} = {}", peer, peer_addr); + break; + } + } + Some(rendezvous_message::Union::relay_response(rr)) => { + log::info!( + "relay requested from peer, time used: {:?}, relay_server: {}", + start.elapsed(), + rr.relay_server + ); + pk = rr.get_pk().into(); + let mut conn = + Self::create_relay(peer, rr.uuid, rr.relay_server).await?; + Self::secure_connection(peer, pk, &mut conn).await?; + return Ok((conn, false)); + } + _ => { + log::error!("Unexpected protobuf msg received: {:?}", msg_in); + } + } + } else { + log::error!("Non-protobuf message bytes received: {:?}", bytes); + } + } + } + drop(socket); + if peer_addr.port() == 0 { + bail!("Failed to connect via rendezvous server"); + } + let time_used = start.elapsed().as_millis() as u64; + log::info!( + "{} ms used to punch hole, relay_server: {}, {}", + time_used, + relay_server, + if is_local { + "is_local: true".to_owned() + } else { + format!("nat_type: {:?}", peer_nat_type) + } + ); + Self::connect( + my_addr, + peer_addr, + peer, + pk, + &relay_server, + rendezvous_server, + time_used, + peer_nat_type, + my_nat_type, + is_local, + ) + .await + } + + async fn connect( + local_addr: SocketAddr, + peer: SocketAddr, + peer_id: &str, + pk: Vec, + relay_server: &str, + rendezvous_server: SocketAddr, + punch_time_used: u64, + peer_nat_type: NatType, + my_nat_type: i32, + is_local: bool, + ) -> ResultType<(Stream, bool)> { + let mut direct_failures = 0; + let mut connect_timeout = 0; + const MIN: u64 = 1000; + if is_local || peer_nat_type == NatType::SYMMETRIC { + connect_timeout = MIN; + } else { + if relay_server.is_empty() { + connect_timeout = CONNECT_TIMEOUT; + } else { + if peer_nat_type == NatType::ASYMMETRIC { + let mut my_nat_type = my_nat_type; + if my_nat_type == NatType::UNKNOWN_NAT as i32 { + my_nat_type = crate::get_nat_type(100).await; + } + if my_nat_type == NatType::ASYMMETRIC as i32 { + connect_timeout = CONNECT_TIMEOUT; + } else if my_nat_type == NatType::SYMMETRIC as i32 { + connect_timeout = MIN; + } + } + if connect_timeout == 0 { + let config = PeerConfig::load(peer_id); + direct_failures = config.direct_failures; + let n = if direct_failures > 0 { 3 } else { 6 }; + connect_timeout = punch_time_used * (n as u64); + } + } + if connect_timeout < MIN { + connect_timeout = MIN; + } + } + log::info!("peer address: {}, timeout: {}", peer, connect_timeout); + let start = std::time::Instant::now(); + let mut conn = FramedStream::new(peer, local_addr, connect_timeout).await; + let direct = !conn.is_err(); + if conn.is_err() { + if !relay_server.is_empty() { + conn = Self::request_relay( + peer_id, + relay_server.to_owned(), + rendezvous_server, + pk.len() == sign::PUBLICKEYBYTES, + ) + .await; + if conn.is_err() { + bail!("Failed to connect via relay server"); + } + } else { + bail!("Failed to make direct connection to remote desktop"); + } + } + if !relay_server.is_empty() && (direct_failures == 0) != direct { + let mut config = PeerConfig::load(peer_id); + config.direct_failures = if direct { 0 } else { 1 }; + log::info!("direct_failures updated to {}", config.direct_failures); + config.store(peer_id); + } + let mut conn = conn?; + log::info!("{:?} used to establish connection", start.elapsed()); + Self::secure_connection(peer_id, pk, &mut conn).await?; + Ok((conn, direct)) + } + + async fn secure_connection(peer_id: &str, pk: Vec, conn: &mut Stream) -> ResultType<()> { + if pk.len() != sign::PUBLICKEYBYTES { + // send an empty message out in case server is setting up secure and waiting for first message + conn.send(&Message::new()).await?; + return Ok(()); + } + let mut pk_ = [0u8; sign::PUBLICKEYBYTES]; + pk_[..].copy_from_slice(&pk); + let pk = sign::PublicKey(pk_); + match timeout(CONNECT_TIMEOUT, conn.next()).await? { + Some(res) => { + let bytes = res?; + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if let Some(message::Union::signed_id(si)) = msg_in.union { + let their_pk_b = if si.pk.len() == box_::PUBLICKEYBYTES { + let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; + pk_[..].copy_from_slice(&si.pk); + box_::PublicKey(pk_) + } else { + bail!("Handshake failed: invalid public box key length from peer"); + }; + if let Ok(id) = sign::verify(&si.id, &pk) { + if id == peer_id.as_bytes() { + let (our_pk_b, out_sk_b) = box_::gen_keypair(); + let key = secretbox::gen_key(); + let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); + let sealed_key = box_::seal(&key.0, &nonce, &their_pk_b, &out_sk_b); + let mut msg_out = Message::new(); + msg_out.set_public_key(PublicKey { + asymmetric_value: our_pk_b.0.into(), + symmetric_value: sealed_key, + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; + conn.set_key(key); + } else { + bail!("Handshake failed: sign failure"); + } + } else { + // fall back to non-secure connection in case pk mismatch + let mut msg_out = Message::new(); + msg_out.set_public_key(PublicKey::new()); + timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; + } + } else { + bail!("Handshake failed: invalid message type"); + } + } else { + bail!("Handshake failed: invalid message format"); + } + } + None => { + bail!("Reset by the peer"); + } + } + Ok(()) + } + + async fn request_relay( + peer: &str, + relay_server: String, + rendezvous_server: SocketAddr, + secure: bool, + ) -> ResultType { + let any_addr = Config::get_any_listen_addr(); + let mut succeed = false; + let mut uuid = "".to_owned(); + for i in 1..=3 { + // use different socket due to current hbbs implement requiring different nat address for each attempt + let mut socket = FramedStream::new(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + let mut msg_out = RendezvousMessage::new(); + uuid = Uuid::new_v4().to_string(); + log::info!( + "#{} request relay attempt, id: {}, uuid: {}, relay_server: {}, secure: {}", + i, + peer, + uuid, + relay_server, + secure, + ); + msg_out.set_request_relay(RequestRelay { + id: peer.to_owned(), + uuid: uuid.clone(), + relay_server: relay_server.clone(), + secure, + ..Default::default() + }); + socket.send(&msg_out).await?; + if let Some(Ok(bytes)) = socket.next_timeout(i * 3000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + if let Some(rendezvous_message::Union::relay_response(_)) = msg_in.union { + succeed = true; + break; + } + } + } + } + if !succeed { + bail!(""); + } + Self::create_relay(peer, uuid, relay_server).await + } + + async fn create_relay(peer: &str, uuid: String, relay_server: String) -> ResultType { + let mut conn = FramedStream::new( + crate::check_port(relay_server, RELAY_PORT), + Config::get_any_listen_addr(), + CONNECT_TIMEOUT, + ) + .await + .with_context(|| "Failed to connect to relay server")?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_request_relay(RequestRelay { + id: peer.to_owned(), + uuid, + ..Default::default() + }); + conn.send(&msg_out).await?; + Ok(conn) + } +} + +#[derive(Default)] +pub struct AudioHandler { + audio_decoder: Option<(AudioDecoder, Vec)>, + #[cfg(any(target_os = "android"))] + oboe: RefCell, + #[cfg(not(any(target_os = "android")))] + audio_buffer: Arc>>, + sample_rate: (u32, u32), + #[cfg(not(any(target_os = "android")))] + audio_stream: Option>, + channels: u16, +} + +impl AudioHandler { + #[cfg(any(target_os = "android"))] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + self.sample_rate = (format0.sample_rate, format0.sample_rate); + Ok(()) + } + + #[cfg(not(any(target_os = "android")))] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + let device = AUDIO_HOST + .default_output_device() + .with_context(|| "Failed to get default output device")?; + log::info!( + "Using default output device: \"{}\"", + device.name().unwrap_or("".to_owned()) + ); + let config = device.default_output_config().map_err(|e| anyhow!(e))?; + let sample_format = config.sample_format(); + log::info!("Default output format: {:?}", config); + log::info!("Remote input format: {:?}", format0); + let mut config: StreamConfig = config.into(); + config.channels = format0.channels as _; + match sample_format { + cpal::SampleFormat::F32 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::I16 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::U16 => self.build_output_stream::(&config, &device)?, + } + self.sample_rate = (format0.sample_rate, config.sample_rate.0); + Ok(()) + } + + pub fn handle_format(&mut self, f: AudioFormat) { + match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { + Ok(d) => { + let buffer = vec![0.; f.sample_rate as usize * f.channels as usize]; + self.audio_decoder = Some((d, buffer)); + self.channels = f.channels as _; + allow_err!(self.start_audio(f)); + } + Err(err) => { + log::error!("Failed to create audio decoder: {}", err); + } + } + } + + pub fn handle_frame(&mut self, frame: AudioFrame, play: bool) { + if !play { + return; + } + #[cfg(not(any(target_os = "android")))] + if self.audio_stream.is_none() { + return; + } + let sample_rate0 = self.sample_rate.0; + let sample_rate = self.sample_rate.1; + let channels = self.channels; + cfg_if::cfg_if! { + if #[cfg(not(target_os = "android"))] { + let audio_buffer = self.audio_buffer.clone(); + // avoiding memory overflow if audio_buffer consumer side has problem + if audio_buffer.lock().unwrap().len() as u32 > sample_rate * 120 { + *audio_buffer.lock().unwrap() = Default::default(); + } + } else { + if self.oboe.borrow().is_null() { + self.oboe = RefCell::new(OboePlayer::new( + channels as _, + sample_rate0 as _, + )); + } + let mut oboe = self.oboe.borrow_mut(); + } + } + self.audio_decoder.as_mut().map(|(d, buffer)| { + if let Ok(n) = d.decode_float(&frame.data, buffer, false) { + let n = n * (channels as usize); + #[cfg(not(any(target_os = "android")))] + { + if sample_rate != sample_rate0 { + let buffer = crate::resample_channels( + &buffer[0..n], + sample_rate0, + sample_rate, + channels, + ); + audio_buffer.lock().unwrap().extend(buffer); + } else { + audio_buffer.lock().unwrap().extend(buffer.iter().cloned()); + } + } + #[cfg(any(target_os = "android"))] + { + oboe.push(&buffer[0..n]); + } + } + }); + } + + #[cfg(not(any(target_os = "android")))] + fn build_output_stream( + &mut self, + config: &StreamConfig, + device: &Device, + ) -> ResultType<()> { + let err_fn = move |err| { + log::error!("an error occurred on stream: {}", err); + }; + let audio_buffer = self.audio_buffer.clone(); + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &_| { + let mut lock = audio_buffer.lock().unwrap(); + let mut n = data.len(); + if lock.len() < n { + n = lock.len(); + } + let mut input = lock.drain(0..n); + for sample in data.iter_mut() { + *sample = match input.next() { + Some(x) => T::from(&x), + _ => T::from(&0.), + }; + } + }, + err_fn, + )?; + stream.play()?; + self.audio_stream = Some(Box::new(stream)); + Ok(()) + } +} + +pub struct VideoHandler { + decoder: Decoder, + pub rgb: Vec, +} + +impl VideoHandler { + pub fn new() -> Self { + VideoHandler { + decoder: Decoder::new(VideoCodecId::VP9, 1).unwrap(), + rgb: Default::default(), + } + } + + pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { + let mut last_frame = Image::new(); + for vp9 in vp9s.frames.iter() { + for frame in self.decoder.decode(&vp9.data)? { + drop(last_frame); + last_frame = frame; + } + } + for frame in self.decoder.flush()? { + drop(last_frame); + last_frame = frame; + } + if last_frame.is_null() { + Ok(false) + } else { + last_frame.rgb(1, true, &mut self.rgb); + Ok(true) + } + } + + pub fn reset(&mut self) { + self.decoder = Decoder::new(VideoCodecId::VP9, 1).unwrap(); + } +} + +#[derive(Default)] +pub struct LoginConfigHandler { + id: String, + is_file_transfer: bool, + is_port_forward: bool, + hash: Hash, + password: Vec, // remember password for reconnect + pub remember: bool, + config: PeerConfig, + pub port_forward: (String, i32), + pub support_press: bool, + pub support_refresh: bool, +} + +impl Deref for LoginConfigHandler { + type Target = PeerConfig; + + fn deref(&self) -> &Self::Target { + &self.config + } +} + +#[inline] +pub fn load_config(id: &str) -> PeerConfig { + PeerConfig::load(id) +} + +impl LoginConfigHandler { + pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { + self.id = id; + self.is_file_transfer = is_file_transfer; + self.is_port_forward = is_port_forward; + let config = self.load_config(); + self.remember = !config.password.is_empty(); + self.config = config; + } + + fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + pub fn save_config(&mut self, config: PeerConfig) { + config.store(&self.id); + self.config = config; + } + + pub fn save_view_style(&mut self, value: String) { + let mut config = self.load_config(); + config.view_style = value; + self.save_config(config); + } + + pub fn toggle_option(&mut self, name: String) -> Option { + let mut option = OptionMessage::default(); + let mut config = self.load_config(); + if name == "show-remote-cursor" { + config.show_remote_cursor = !config.show_remote_cursor; + option.show_remote_cursor = (if config.show_remote_cursor { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "disable-audio" { + config.disable_audio = !config.disable_audio; + option.disable_audio = (if config.disable_audio { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "disable-clipboard" { + config.disable_clipboard = !config.disable_clipboard; + option.disable_clipboard = (if config.disable_clipboard { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "lock-after-session-end" { + config.lock_after_session_end = !config.lock_after_session_end; + option.lock_after_session_end = (if config.lock_after_session_end { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "privacy-mode" { + config.privacy_mode = !config.privacy_mode; + option.privacy_mode = (if config.privacy_mode { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "block-input" { + option.block_input = BoolOption::Yes.into(); + } else if name == "unblock-input" { + option.block_input = BoolOption::No.into(); + } else { + let v = self.options.get(&name).is_some(); + if v { + self.config.options.remove(&name); + } else { + self.config.options.insert(name, "Y".to_owned()); + } + self.config.store(&self.id); + return None; + } + self.save_config(config); + let mut misc = Misc::new(); + misc.set_option(option); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + Some(msg_out) + } + + fn get_option_message(&self, ignore_default: bool) -> Option { + if self.is_port_forward || self.is_file_transfer { + return None; + } + let mut n = 0; + let mut msg = OptionMessage::new(); + let q = self.image_quality.clone(); + if let Some(q) = self.get_image_quality_enum(&q, ignore_default) { + msg.image_quality = q.into(); + n += 1; + } else if q == "custom" { + let config = PeerConfig::load(&self.id); + let mut it = config.custom_image_quality.iter(); + let bitrate = it.next(); + let quantizer = it.next(); + if let Some(bitrate) = bitrate { + if let Some(quantizer) = quantizer { + msg.custom_image_quality = bitrate << 8 | quantizer; + n += 1; + } + } + } + if self.get_toggle_option("show-remote-cursor") { + msg.show_remote_cursor = BoolOption::Yes.into(); + n += 1; + } + if self.get_toggle_option("lock-after-session-end") { + msg.lock_after_session_end = BoolOption::Yes.into(); + n += 1; + } + if self.get_toggle_option("privacy_mode") { + msg.privacy_mode = BoolOption::Yes.into(); + n += 1; + } + if n > 0 { + Some(msg) + } else { + None + } + } + + fn get_image_quality_enum(&self, q: &str, ignore_default: bool) -> Option { + if q == "low" { + Some(ImageQuality::Low) + } else if q == "best" { + Some(ImageQuality::Best) + } else if q == "balanced" { + if ignore_default { + None + } else { + Some(ImageQuality::Balanced) + } + } else { + None + } + } + + pub fn get_toggle_option(&self, name: &str) -> bool { + if name == "show-remote-cursor" { + self.config.show_remote_cursor + } else if name == "lock-after-session-end" { + self.config.lock_after_session_end + } else if name == "privacy-mode" { + self.config.privacy_mode + } else if name == "disable-audio" { + self.config.disable_audio + } else if name == "disable-clipboard" { + self.config.disable_clipboard + } else { + !self.get_option(name).is_empty() + } + } + + pub fn refresh() -> Message { + let mut misc = Misc::new(); + misc.set_refresh_video(true); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + + pub fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) -> Message { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_image_quality: bitrate << 8 | quantizer, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + let mut config = self.load_config(); + config.image_quality = "custom".to_owned(); + config.custom_image_quality = vec![bitrate, quantizer]; + self.save_config(config); + msg_out + } + + pub fn save_image_quality(&mut self, value: String) -> Option { + let mut res = None; + if let Some(q) = self.get_image_quality_enum(&value, false) { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + image_quality: q.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + res = Some(msg_out); + } + let mut config = self.load_config(); + config.image_quality = value; + self.save_config(config); + res + } + + pub fn get_option(&self, k: &str) -> String { + if let Some(v) = self.config.options.get(k) { + v.clone() + } else { + "".to_owned() + } + } + + pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { + if err == "Wrong Password" { + self.password = Default::default(); + interface.msgbox("re-input-password", err, "Do you want to enter again?"); + true + } else { + interface.msgbox("error", "Login Error", err); + false + } + } + + pub fn get_username(&self, pi: &PeerInfo) -> String { + return if pi.username.is_empty() { + self.info.username.clone() + } else { + pi.username.clone() + }; + } + + pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { + if !pi.version.is_empty() { + self.support_press = true; + self.support_refresh = true; + } + let serde = PeerInfoSerde { + username, + hostname: pi.hostname.clone(), + platform: pi.platform.clone(), + }; + let mut config = self.load_config(); + config.info = serde; + let password = self.password.clone(); + let password0 = config.password.clone(); + let remember = self.remember; + if remember { + if !password.is_empty() && password != password0 { + config.password = password; + log::debug!("remember password of {}", self.id); + } + } else { + if !password0.is_empty() { + config.password = Default::default(); + log::debug!("remove password of {}", self.id); + } + } + // no matter if change, for update file time + self.save_config(config); + } + + fn create_login_msg(&self, password: Vec) -> Message { + #[cfg(any(target_os = "android", target_os = "ios"))] + let my_id = crate::common::MOBILE_INFO1.lock().unwrap().clone(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let my_id = Config::get_id(); + let mut lr = LoginRequest { + username: self.id.clone(), + password, + my_id, + my_name: crate::username(), + option: self.get_option_message(true).into(), + ..Default::default() + }; + if self.is_file_transfer { + lr.set_file_transfer(FileTransfer { + dir: self.get_option("remote_dir"), + show_hidden: !self.get_option("remote_show_hidden").is_empty(), + ..Default::default() + }); + } else if self.is_port_forward { + lr.set_port_forward(PortForward { + host: self.port_forward.0.clone(), + port: self.port_forward.1, + ..Default::default() + }); + } + let mut msg_out = Message::new(); + msg_out.set_login_request(lr); + msg_out + } +} + +pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { + if !t.from_client { + let mut msg_out = Message::new(); + msg_out.set_test_delay(t); + allow_err!(peer.send(&msg_out).await); + } +} + +pub async fn handle_hash( + lc: Arc>, + hash: Hash, + interface: &impl Interface, + peer: &mut Stream, +) { + let mut password = lc.read().unwrap().password.clone(); + if password.is_empty() { + password = lc.read().unwrap().config.password.clone(); + } + if password.is_empty() { + // login without password, the remote side can click accept + send_login(lc.clone(), Vec::new(), peer).await; + interface.msgbox("input-password", "Password Required", ""); + } else { + let mut hasher = Sha256::new(); + hasher.update(&password); + hasher.update(&hash.challenge); + send_login(lc.clone(), hasher.finalize()[..].into(), peer).await; + } + lc.write().unwrap().hash = hash; +} + +async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { + let msg_out = lc.read().unwrap().create_login_msg(password); + allow_err!(peer.send(&msg_out).await); +} + +pub async fn handle_login_from_ui( + lc: Arc>, + password: String, + remember: bool, + peer: &mut Stream, +) { + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&lc.read().unwrap().hash.salt); + let res = hasher.finalize(); + lc.write().unwrap().remember = remember; + lc.write().unwrap().password = res[..].into(); + let mut hasher2 = Sha256::new(); + hasher2.update(&res[..]); + hasher2.update(&lc.read().unwrap().hash.challenge); + send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; +} + +#[async_trait] +pub trait Interface: Send + Clone + 'static + Sized { + fn msgbox(&self, msgtype: &str, title: &str, text: &str); + fn handle_login_error(&mut self, err: &str) -> bool; + fn handle_peer_info(&mut self, pi: PeerInfo); + async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream); + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); +} + +#[derive(Clone)] +pub enum Data { + Close, + Login((String, bool)), + Message(Message), + SendFiles((i32, String, String, bool, bool)), + RemoveDirAll((i32, String, bool)), + ConfirmDeleteFiles((i32, i32)), + SetNoConfirm(i32), + RemoveDir((i32, String)), + RemoveFile((i32, String, i32, bool)), + CreateDir((i32, String, bool)), + CancelJob(i32), + RemovePortForward(i32), + AddPortForward((i32, String, i32)), + NewRDP, +} + +#[derive(Clone)] +pub enum Key { + ControlKey(ControlKey), + Chr(u32), + _Raw(u32), +} + +lazy_static::lazy_static! { + pub static ref KEY_MAP: HashMap<&'static str, Key> = + [ + ("VK_A", Key::Chr('a' as _)), + ("VK_B", Key::Chr('b' as _)), + ("VK_C", Key::Chr('c' as _)), + ("VK_D", Key::Chr('d' as _)), + ("VK_E", Key::Chr('e' as _)), + ("VK_F", Key::Chr('f' as _)), + ("VK_G", Key::Chr('g' as _)), + ("VK_H", Key::Chr('h' as _)), + ("VK_I", Key::Chr('i' as _)), + ("VK_J", Key::Chr('j' as _)), + ("VK_K", Key::Chr('k' as _)), + ("VK_L", Key::Chr('l' as _)), + ("VK_M", Key::Chr('m' as _)), + ("VK_N", Key::Chr('n' as _)), + ("VK_O", Key::Chr('o' as _)), + ("VK_P", Key::Chr('p' as _)), + ("VK_Q", Key::Chr('q' as _)), + ("VK_R", Key::Chr('r' as _)), + ("VK_S", Key::Chr('s' as _)), + ("VK_T", Key::Chr('t' as _)), + ("VK_U", Key::Chr('u' as _)), + ("VK_V", Key::Chr('v' as _)), + ("VK_W", Key::Chr('w' as _)), + ("VK_X", Key::Chr('x' as _)), + ("VK_Y", Key::Chr('y' as _)), + ("VK_Z", Key::Chr('z' as _)), + ("VK_0", Key::Chr('0' as _)), + ("VK_1", Key::Chr('1' as _)), + ("VK_2", Key::Chr('2' as _)), + ("VK_3", Key::Chr('3' as _)), + ("VK_4", Key::Chr('4' as _)), + ("VK_5", Key::Chr('5' as _)), + ("VK_6", Key::Chr('6' as _)), + ("VK_7", Key::Chr('7' as _)), + ("VK_8", Key::Chr('8' as _)), + ("VK_9", Key::Chr('9' as _)), + ("VK_COMMA", Key::Chr(',' as _)), + ("VK_SLASH", Key::Chr('/' as _)), + ("VK_SEMICOLON", Key::Chr(';' as _)), + ("VK_QUOTE", Key::Chr('\'' as _)), + ("VK_LBRACKET", Key::Chr('[' as _)), + ("VK_RBRACKET", Key::Chr(']' as _)), + ("VK_BACKSLASH", Key::Chr('\\' as _)), + ("VK_MINUS", Key::Chr('-' as _)), + ("VK_PLUS", Key::Chr('=' as _)), // it is =, but sciter return VK_PLUS + ("VK_DIVIDE", Key::ControlKey(ControlKey::Divide)), // numpad + ("VK_MULTIPLY", Key::ControlKey(ControlKey::Multiply)), // numpad + ("VK_SUBTRACT", Key::ControlKey(ControlKey::Subtract)), // numpad + ("VK_ADD", Key::ControlKey(ControlKey::Add)), // numpad + ("VK_DECIMAL", Key::ControlKey(ControlKey::Decimal)), // numpad + ("VK_F1", Key::ControlKey(ControlKey::F1)), + ("VK_F2", Key::ControlKey(ControlKey::F2)), + ("VK_F3", Key::ControlKey(ControlKey::F3)), + ("VK_F4", Key::ControlKey(ControlKey::F4)), + ("VK_F5", Key::ControlKey(ControlKey::F5)), + ("VK_F6", Key::ControlKey(ControlKey::F6)), + ("VK_F7", Key::ControlKey(ControlKey::F7)), + ("VK_F8", Key::ControlKey(ControlKey::F8)), + ("VK_F9", Key::ControlKey(ControlKey::F9)), + ("VK_F10", Key::ControlKey(ControlKey::F10)), + ("VK_F11", Key::ControlKey(ControlKey::F11)), + ("VK_F12", Key::ControlKey(ControlKey::F12)), + ("VK_F12", Key::ControlKey(ControlKey::F12)), + ("VK_ENTER", Key::ControlKey(ControlKey::Return)), + ("VK_CANCEL", Key::ControlKey(ControlKey::Cancel)), + ("VK_BACK", Key::ControlKey(ControlKey::Backspace)), + ("VK_TAB", Key::ControlKey(ControlKey::Tab)), + ("VK_CLEAR", Key::ControlKey(ControlKey::Clear)), + ("VK_RETURN", Key::ControlKey(ControlKey::Return)), + ("VK_SHIFT", Key::ControlKey(ControlKey::Shift)), + ("VK_CONTROL", Key::ControlKey(ControlKey::Control)), + ("VK_MENU", Key::ControlKey(ControlKey::Menu)), + ("VK_PAUSE", Key::ControlKey(ControlKey::Pause)), + ("VK_CAPITAL", Key::ControlKey(ControlKey::CapsLock)), + ("VK_KANA", Key::ControlKey(ControlKey::Kana)), + ("VK_HANGUL", Key::ControlKey(ControlKey::Hangul)), + ("VK_JUNJA", Key::ControlKey(ControlKey::Junja)), + ("VK_FINAL", Key::ControlKey(ControlKey::Final)), + ("VK_HANJA", Key::ControlKey(ControlKey::Hanja)), + ("VK_KANJI", Key::ControlKey(ControlKey::Kanji)), + ("VK_ESCAPE", Key::ControlKey(ControlKey::Escape)), + ("VK_CONVERT", Key::ControlKey(ControlKey::Convert)), + ("VK_SPACE", Key::ControlKey(ControlKey::Space)), + ("VK_PRIOR", Key::ControlKey(ControlKey::PageUp)), + ("VK_NEXT", Key::ControlKey(ControlKey::PageDown)), + ("VK_END", Key::ControlKey(ControlKey::End)), + ("VK_HOME", Key::ControlKey(ControlKey::Home)), + ("VK_LEFT", Key::ControlKey(ControlKey::LeftArrow)), + ("VK_UP", Key::ControlKey(ControlKey::UpArrow)), + ("VK_RIGHT", Key::ControlKey(ControlKey::RightArrow)), + ("VK_DOWN", Key::ControlKey(ControlKey::DownArrow)), + ("VK_SELECT", Key::ControlKey(ControlKey::Select)), + ("VK_PRINT", Key::ControlKey(ControlKey::Print)), + ("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)), + ("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)), + ("VK_INSERT", Key::ControlKey(ControlKey::Insert)), + ("VK_DELETE", Key::ControlKey(ControlKey::Delete)), + ("VK_HELP", Key::ControlKey(ControlKey::Help)), + ("VK_SLEEP", Key::ControlKey(ControlKey::Sleep)), + ("VK_SEPARATOR", Key::ControlKey(ControlKey::Separator)), + ("VK_NUMPAD0", Key::ControlKey(ControlKey::Numpad0)), + ("VK_NUMPAD1", Key::ControlKey(ControlKey::Numpad1)), + ("VK_NUMPAD2", Key::ControlKey(ControlKey::Numpad2)), + ("VK_NUMPAD3", Key::ControlKey(ControlKey::Numpad3)), + ("VK_NUMPAD4", Key::ControlKey(ControlKey::Numpad4)), + ("VK_NUMPAD5", Key::ControlKey(ControlKey::Numpad5)), + ("VK_NUMPAD6", Key::ControlKey(ControlKey::Numpad6)), + ("VK_NUMPAD7", Key::ControlKey(ControlKey::Numpad7)), + ("VK_NUMPAD8", Key::ControlKey(ControlKey::Numpad8)), + ("VK_NUMPAD9", Key::ControlKey(ControlKey::Numpad9)), + ("CTRL_ALT_DEL", Key::ControlKey(ControlKey::CtrlAltDel)), + ("LOCK_SCREEN", Key::ControlKey(ControlKey::LockScreen)), + ].iter().cloned().collect(); +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 00000000000..454056335f5 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,365 @@ +pub use copypasta::{ClipboardContext, ClipboardProvider}; +use hbb_common::{ + allow_err, + compress::{compress as compress_func, decompress}, + config::{Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, + log, + message_proto::*, + protobuf::Message as _, + protobuf::ProtobufEnum, + rendezvous_proto::*, + sleep, + tcp::FramedStream, + tokio, ResultType, +}; +#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use std::sync::{Arc, Mutex}; + +pub const CLIPBOARD_NAME: &'static str = "clipboard"; +pub const CLIPBOARD_INTERVAL: u64 = 333; + +lazy_static::lazy_static! { + pub static ref CONTENT: Arc> = Default::default(); + pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref MOBILE_INFO1: Arc> = Default::default(); + pub static ref MOBILE_INFO2: Arc> = Default::default(); +} + +#[inline] +pub fn valid_for_numlock(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + let v = ck.value(); + (v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value()) + || v == ControlKey::Decimal.value() + } else { + false + } +} + +#[inline] +pub fn valid_for_capslock(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::chr(ch)) = evt.union { + ch >= 'a' as u32 && ch <= 'z' as u32 + } else { + false + } +} + +pub fn check_clipboard( + ctx: &mut ClipboardContext, + old: Option<&Arc>>, +) -> Option { + let side = if old.is_none() { "host" } else { "client" }; + let old = if let Some(old) = old { old } else { &CONTENT }; + if let Ok(content) = ctx.get_contents() { + if content.len() < 2_000_000 && !content.is_empty() { + let changed = content != *old.lock().unwrap(); + if changed { + log::info!("{} update found on {}", CLIPBOARD_NAME, side); + let bytes = content.clone().into_bytes(); + *old.lock().unwrap() = content; + let compressed = compress_func(&bytes, COMPRESS_LEVEL); + let compress = compressed.len() < bytes.len(); + let content = if compress { compressed } else { bytes }; + let mut msg = Message::new(); + msg.set_clipboard(Clipboard { + compress, + content, + ..Default::default() + }); + return Some(msg); + } + } + } + None +} + +pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { + let content = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content + }; + if let Ok(content) = String::from_utf8(content) { + match ClipboardContext::new() { + Ok(mut ctx) => { + let side = if old.is_none() { "host" } else { "client" }; + let old = if let Some(old) = old { old } else { &CONTENT }; + *old.lock().unwrap() = content.clone(); + allow_err!(ctx.set_contents(content)); + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } + Err(err) => { + log::error!("Failed to create clipboard context: {}", err); + } + } + } +} + +pub fn resample_channels( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use dasp::{interpolate::linear::Linear, signal, Signal}; + let n = data.len() / (channels as usize); + let n = n * sample_rate as usize / sample_rate0 as usize; + if channels == 2 { + let mut source = signal::from_interleaved_samples_iter::<_, [_; 2]>(data.iter().cloned()); + let a = source.next(); + let b = source.next(); + let interp = Linear::new(a, b); + let mut data = Vec::with_capacity(n << 1); + for x in source + .from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _) + .take(n) + { + data.push(x[0]); + data.push(x[1]); + } + data + } else { + let mut source = signal::from_iter(data.iter().cloned()); + let a = source.next(); + let b = source.next(); + let interp = Linear::new(a, b); + source + .from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _) + .take(n) + .collect() + } +} + +pub fn test_nat_type() { + std::thread::spawn(move || loop { + match test_nat_type_() { + Ok(true) => break, + Err(err) => { + log::error!("test nat: {}", err); + } + _ => {} + } + if Config::get_nat_type() != 0 { + break; + } + std::thread::sleep(std::time::Duration::from_secs(12)); + }); +} + +#[tokio::main(basic_scheduler)] +async fn test_nat_type_() -> ResultType { + let start = std::time::Instant::now(); + let rendezvous_server = get_rendezvous_server(100).await; + let server1 = rendezvous_server; + let mut server2 = server1; + server2.set_port(server1.port() - 1); + let mut msg_out = RendezvousMessage::new(); + let serial = Config::get_serial(); + msg_out.set_test_nat_request(TestNatRequest { + serial, + ..Default::default() + }); + let mut port1 = 0; + let mut port2 = 0; + let mut addr = Config::get_any_listen_addr(); + for i in 0..2 { + let mut socket = FramedStream::new( + if i == 0 { &server1 } else { &server2 }, + addr, + RENDEZVOUS_TIMEOUT, + ) + .await?; + addr = socket.get_ref().local_addr()?; + socket.send(&msg_out).await?; + if let Some(Ok(bytes)) = socket.next_timeout(3000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + if let Some(rendezvous_message::Union::test_nat_response(tnr)) = msg_in.union { + if i == 0 { + port1 = tnr.port; + } else { + port2 = tnr.port; + } + if let Some(cu) = tnr.cu.as_ref() { + Config::set_option( + "rendezvous-servers".to_owned(), + cu.rendezvous_servers.join(","), + ); + Config::set_serial(cu.serial); + } + } + } + } else { + break; + } + } + let ok = port1 > 0 && port2 > 0; + if ok { + let t = if port1 == port2 { + NatType::ASYMMETRIC + } else { + NatType::SYMMETRIC + }; + Config::set_nat_type(t as _); + log::info!("tested nat type: {:?} in {:?}", t, start.elapsed()); + } + Ok(ok) +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub async fn get_rendezvous_server(_ms_timeout: u64) -> std::net::SocketAddr { + Config::get_rendezvous_server() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub async fn get_rendezvous_server(ms_timeout: u64) -> std::net::SocketAddr { + crate::ipc::get_rendezvous_server(ms_timeout).await +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub async fn get_nat_type(_ms_timeout: u64) -> i32 { + Config::get_nat_type() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub async fn get_nat_type(ms_timeout: u64) -> i32 { + crate::ipc::get_nat_type(ms_timeout).await +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +#[tokio::main(basic_scheduler)] +async fn test_rendezvous_server_() { + let servers = Config::get_rendezvous_servers(); + let mut futs = Vec::new(); + for host in servers { + futs.push(tokio::spawn(async move { + let tm = std::time::Instant::now(); + if FramedStream::new( + &crate::check_port(&host, RENDEZVOUS_PORT), + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await + .is_ok() + { + let elapsed = tm.elapsed().as_micros(); + Config::update_latency(&host, elapsed as _); + } else { + Config::update_latency(&host, -1); + } + })); + } + join_all(futs).await; +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +pub fn test_rendezvous_server() { + std::thread::spawn(test_rendezvous_server_); +} + +#[inline] +pub fn get_time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) as _ +} + +pub fn run_me>(args: Vec) -> std::io::Result { + let cmd = std::env::current_exe()?; + return std::process::Command::new(cmd).args(&args).spawn(); +} + +pub fn username() -> String { + // fix bug of whoami + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return whoami::username().trim_end_matches('\0').to_owned(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return MOBILE_INFO2.lock().unwrap().clone(); +} + +#[inline] +pub fn check_port(host: T, port: i32) -> String { + let host = host.to_string(); + if !host.contains(":") { + return format!("{}:{}", host, port); + } + return host; +} + +pub const POSTFIX_SERVICE: &'static str = "_service"; + +#[inline] +pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + ck.value() == key.value() + } else { + false + } +} + +#[inline] +pub fn is_modifier(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + let v = ck.value(); + v == ControlKey::Alt.value() + || v == ControlKey::Shift.value() + || v == ControlKey::Control.value() + || v == ControlKey::Meta.value() + } else { + false + } +} + +pub fn test_if_valid_server(host: String) -> String { + let mut host = host; + if !host.contains(":") { + host = format!("{}:{}", host, 0); + } + match hbb_common::to_socket_addr(&host) { + Err(err) => err.to_string(), + Ok(_) => "".to_owned(), + } +} + +pub fn get_version_number(v: &str) -> i64 { + let mut n = 0; + for x in v.split(".") { + n = n * 1000 + x.parse::().unwrap_or(0); + } + n +} + +pub fn check_software_update() { + std::thread::spawn(move || allow_err!(_check_software_update())); +} + +#[tokio::main(basic_scheduler)] +async fn _check_software_update() -> hbb_common::ResultType<()> { + sleep(3.).await; + let rendezvous_server = get_rendezvous_server(1_000).await; + let mut socket = hbb_common::udp::FramedSocket::new(Config::get_any_listen_addr()).await?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_software_update(SoftwareUpdate { + url: crate::VERSION.to_owned(), + ..Default::default() + }); + socket.send(&msg_out, rendezvous_server).await?; + use hbb_common::protobuf::Message; + if let Some(Ok((bytes, _))) = socket.next_timeout(30_000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + if let Some(rendezvous_message::Union::software_update(su)) = msg_in.union { + let version = hbb_common::get_version_from_url(&su.url); + if get_version_number(&version) > get_version_number(crate::VERSION) { + *SOFTWARE_UPDATE_URL.lock().unwrap() = su.url; + } + } + } + } + Ok(()) +} diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 00000000000..4dfee9d7c6e --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,464 @@ +use hbb_common::{ + allow_err, bail, bytes, + bytes_codec::BytesCodec, + config::{self, Config}, + futures::StreamExt as _, + futures_util::sink::SinkExt, + log, timeout, tokio, + tokio_util::codec::Framed, + ResultType, +}; +use parity_tokio_ipc::{Connection as Conn, Endpoint, Incoming, SecurityAttributes}; +use serde_derive::{Deserialize, Serialize}; +use std::{collections::HashMap, net::SocketAddr}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum FS { + ReadDir { + dir: String, + include_hidden: bool, + }, + RemoveDir { + path: String, + id: i32, + recursive: bool, + }, + RemoveFile { + path: String, + id: i32, + file_num: i32, + }, + CreateDir { + path: String, + id: i32, + }, + NewWrite { + path: String, + id: i32, + files: Vec<(String, u64)>, + }, + CancelWrite { + id: i32, + }, + WriteBlock { + id: i32, + file_num: i32, + data: Vec, + compressed: bool, + }, + WriteDone { + id: i32, + file_num: i32, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum Data { + Login { + id: i32, + is_file_transfer: bool, + peer_id: String, + name: String, + authorized: bool, + port_forward: String, + keyboard: bool, + clipboard: bool, + audio: bool, + }, + ChatMessage { + text: String, + }, + SwitchPermission { + name: String, + enabled: bool, + }, + SystemInfo(Option), + ClickTime(i64), + Authorize, + Close, + SAS, + OnlineStatus(Option<(i64, bool)>), + Config((String, Option)), + Options(Option>), + NatType(Option), + ConfirmedKey(Option<(Vec, Vec)>), + RawMessage(Vec), + FS(FS), + Test, +} + +#[tokio::main(basic_scheduler)] +pub async fn start(postfix: &str) -> ResultType<()> { + if postfix.is_empty() { + crate::common::test_nat_type(); + } + let mut incoming = new_listener(postfix).await?; + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let postfix = postfix.to_owned(); + tokio::spawn(async move { + loop { + match stream.next().await { + Err(err) => { + log::trace!("ipc{} connection closed: {}", postfix, err); + break; + } + Ok(Some(data)) => { + handle(data, &mut stream).await; + } + _ => {} + } + } + }); + } + Err(err) => { + log::error!("Couldn't get client: {:?}", err); + } + } + } + } +} + +pub async fn new_listener(postfix: &str) -> ResultType { + let path = Config::ipc_path(postfix); + #[cfg(not(windows))] + check_pid(postfix).await; + let mut endpoint = Endpoint::new(path.clone()); + match SecurityAttributes::allow_everyone_create() { + Ok(attr) => endpoint.set_security_attributes(attr), + Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err), + }; + match endpoint.incoming() { + Ok(incoming) => { + log::info!("Started ipc{} server at path: {}", postfix, &path); + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + write_pid(postfix); + } + Ok(incoming) + } + Err(err) => { + log::error!( + "Faild to start ipc{} server at path {}: {}", + postfix, + path, + err + ); + Err(err.into()) + } + } +} + +async fn handle(data: Data, stream: &mut Connection) { + match data { + Data::SystemInfo(_) => { + let info = format!( + "log_path: {}, config: {}, username: {}", + Config::log_path().to_str().unwrap_or(""), + Config::file().to_str().unwrap_or(""), + crate::username(), + ); + allow_err!(stream.send(&Data::SystemInfo(Some(info))).await); + } + Data::Close => { + log::info!("Receive close message"); + std::process::exit(0); + } + Data::OnlineStatus(_) => { + let x = config::ONLINE + .lock() + .unwrap() + .values() + .max() + .unwrap_or(&0) + .clone(); + let confirmed = Config::get_key_confirmed(); + allow_err!(stream.send(&Data::OnlineStatus(Some((x, confirmed)))).await); + } + Data::ConfirmedKey(None) => { + let out = if Config::get_key_confirmed() { + Some(Config::get_key_pair()) + } else { + None + }; + allow_err!(stream.send(&Data::ConfirmedKey(out)).await); + } + Data::Config((name, value)) => match value { + None => { + let value; + if name == "id" { + value = Some(Config::get_id()); + } else if name == "password" { + value = Some(Config::get_password()); + } else if name == "salt" { + value = Some(Config::get_salt()); + } else if name == "rendezvous_server" { + value = Some(Config::get_rendezvous_server().to_string()); + } else { + value = None; + } + allow_err!(stream.send(&Data::Config((name, value))).await); + } + Some(value) => { + if name == "id" { + Config::set_id(&value); + } else if name == "password" { + Config::set_password(&value); + } else if name == "salt" { + Config::set_salt(&value); + } else { + return; + } + log::info!("{} updated", name); + } + }, + Data::Options(value) => match value { + None => { + let v = Config::get_options(); + allow_err!(stream.send(&Data::Options(Some(v))).await); + } + Some(value) => { + Config::set_options(value); + } + }, + Data::NatType(_) => { + let t = Config::get_nat_type(); + allow_err!(stream.send(&Data::NatType(Some(t))).await); + } + _ => {} + } +} + +pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType { + let path = Config::ipc_path(postfix); + let client = timeout(ms_timeout, Endpoint::connect(&path)).await??; + Ok(Connection::new(client)) +} + +#[inline] +#[cfg(not(windows))] +fn get_pid_file(postfix: &str) -> String { + let path = Config::ipc_path(postfix); + format!("{}.pid", path) +} + +#[cfg(not(windows))] +async fn check_pid(postfix: &str) { + let pid_file = get_pid_file(postfix); + if let Ok(mut file) = File::open(&pid_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let pid = content.parse::().unwrap_or(0); + if pid > 0 { + if let Ok(p) = psutil::process::Process::new(pid as _) { + if let Ok(current) = psutil::process::Process::current() { + if current.name().unwrap_or("".to_owned()) == p.name().unwrap_or("".to_owned()) + { + // double check with connect + if connect(1000, postfix).await.is_ok() { + return; + } + } + } + } + } + } + hbb_common::allow_err!(std::fs::remove_file(&Config::ipc_path(postfix))); +} + +#[inline] +#[cfg(not(windows))] +fn write_pid(postfix: &str) { + let path = get_pid_file(postfix); + if let Ok(mut file) = File::create(&path) { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + file.write_all(&std::process::id().to_string().into_bytes()) + .ok(); + } +} + +pub struct Connection { + inner: Framed, +} + +impl Connection { + pub fn new(conn: Conn) -> Self { + Self { + inner: Framed::new(conn, BytesCodec::new()), + } + } + + pub async fn send(&mut self, data: &Data) -> ResultType<()> { + let v = serde_json::to_vec(data)?; + self.inner.send(bytes::Bytes::from(v)).await?; + Ok(()) + } + + async fn send_config(&mut self, name: &str, value: String) -> ResultType<()> { + self.send(&Data::Config((name.to_owned(), Some(value)))) + .await + } + + pub async fn next_timeout(&mut self, ms_timeout: u64) -> ResultType> { + Ok(timeout(ms_timeout, self.next()).await??) + } + + pub async fn next_timeout2(&mut self, ms_timeout: u64) -> Option>> { + if let Ok(x) = timeout(ms_timeout, self.next()).await { + Some(x) + } else { + None + } + } + + pub async fn next(&mut self) -> ResultType> { + match self.inner.next().await { + Some(res) => { + let bytes = res?; + if let Ok(s) = std::str::from_utf8(&bytes) { + if let Ok(data) = serde_json::from_str::(s) { + return Ok(Some(data)); + } + } + return Ok(None); + } + _ => { + bail!("reset by the peer"); + } + } + } +} + +#[tokio::main(basic_scheduler)] +async fn get_config(name: &str) -> ResultType> { + get_config_async(name, 1_000).await +} + +async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Config((name.to_owned(), None))).await?; + if let Some(Data::Config((name2, value))) = c.next_timeout(ms_timeout).await? { + if name == name2 { + return Ok(value); + } + } + return Ok(None); +} + +#[tokio::main(basic_scheduler)] +async fn set_config(name: &str, value: String) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send_config(name, value).await?; + Ok(()) +} + +pub fn set_password(v: String) -> ResultType<()> { + Config::set_password(&v); + set_config("password", v) +} + +pub fn get_id() -> String { + if let Ok(Some(v)) = get_config("id") { + // update salt also, so that nexttime reinstallation not causing first-time auto-login failure + if let Ok(Some(v2)) = get_config("salt") { + Config::set_salt(&v2); + } + if v != Config::get_id() { + Config::set_key_confirmed(false); + Config::set_id(&v); + } + v + } else { + Config::get_id() + } +} + +pub fn get_password() -> String { + if let Ok(Some(v)) = get_config("password") { + Config::set_password(&v); + v + } else { + Config::get_password() + } +} + +pub async fn get_rendezvous_server(ms_timeout: u64) -> SocketAddr { + if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await { + if let Ok(v) = v.parse() { + return v; + } + } + return Config::get_rendezvous_server(); +} + +async fn get_options_(ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Options(None)).await?; + if let Some(Data::Options(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_options(value.clone()); + Ok(value) + } else { + Ok(Config::get_options()) + } +} + +#[tokio::main(basic_scheduler)] +pub async fn get_options() -> HashMap { + get_options_(1000).await.unwrap_or(Config::get_options()) +} + +pub fn get_option(key: &str) -> String { + if let Some(v) = get_options().get(key) { + v.clone() + } else { + "".to_owned() + } +} + +pub fn set_option(key: &str, value: &str) { + let mut options = get_options(); + if value.is_empty() { + options.remove(key); + } else { + options.insert(key.to_owned(), value.to_owned()); + } + set_options(options).ok(); +} + +#[tokio::main(basic_scheduler)] +pub async fn set_options(value: HashMap) -> ResultType<()> { + Config::set_options(value.clone()); + connect(1000, "") + .await? + .send(&Data::Options(Some(value))) + .await?; + Ok(()) +} + +#[inline] +async fn get_nat_type_(ms_timeout: u64) -> ResultType { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::NatType(None)).await?; + if let Some(Data::NatType(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_nat_type(value); + Ok(value) + } else { + Ok(Config::get_nat_type()) + } +} + +pub async fn get_nat_type(ms_timeout: u64) -> i32 { + get_nat_type_(ms_timeout) + .await + .unwrap_or(Config::get_nat_type()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000000..b0cf92bead0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,29 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod platform; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod server; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use self::server::*; +mod client; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod rendezvous_mediator; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use self::rendezvous_mediator::*; +pub mod common; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod ipc; +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub mod ui; +mod version; +pub use version::*; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod mobile; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod mobile_ffi; +use common::*; +#[cfg(feature = "cli")] +pub mod cli; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod port_forward; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000000..53c30cf5ab3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,148 @@ +// Specify the Windows subsystem to eliminate console window. +// Requires Rust 1.18. +//#![windows_subsystem = "windows"] + +use hbb_common::log; +use rustdesk::*; + +#[cfg(any(target_os = "android", target_os = "ios"))] +fn main() { + common::test_rendezvous_server(); + common::test_nat_type(); + #[cfg(target_os = "android")] + crate::common::check_software_update(); + mobile::Session::start(""); +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +fn main() { + let mut args = Vec::new(); + let mut i = 0; + for arg in std::env::args() { + if i > 0 { + args.push(arg); + } + i += 1; + } + if args.len() > 0 && args[0] == "--version" { + println!("{}", crate::VERSION); + return; + } + #[cfg(not(feature = "inline"))] + { + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + } + #[cfg(feature = "inline")] + { + let mut path = hbb_common::config::Config::log_path(); + if args.len() > 0 && args[0].starts_with("--") { + let name = args[0].replace("--", ""); + if !name.is_empty() { + path.push(name); + } + } + use flexi_logger::*; + Logger::with_env_or_str("debug") + .log_to_file() + .format(opt_format) + .rotate( + Criterion::Age(Age::Day), + Naming::Timestamps, + Cleanup::KeepLogFiles(6), + ) + .directory(path) + .start() + .ok(); + } + if args.is_empty() { + std::thread::spawn(move || start_server(false, false)); + } else { + if args[0] == "--uninstall" { + #[cfg(windows)] + { + if let Err(err) = platform::uninstall_me() { + log::error!("Failed to uninstall: {}", err); + } + return; + } + } else if args[0] == "--update" { + #[cfg(windows)] + { + hbb_common::allow_err!(platform::update_me()); + return; + } + } else if args[0] == "--reinstall" { + #[cfg(windows)] + { + hbb_common::allow_err!(platform::uninstall_me()); + hbb_common::allow_err!(platform::install_me("desktopicon startmenu")); + return; + } + } else if args[0] == "--remove" { + if args.len() == 2 { + // sleep a while so that process of removed exe exit + std::thread::sleep(std::time::Duration::from_secs(1)); + std::fs::remove_file(&args[1]).ok(); + return; + } + } else if args[0] == "--service" { + log::info!("start --service"); + start_os_service(); + return; + } else if args[0] == "--server" { + log::info!("start --server"); + start_server(true, true); + return; + } else if args[0] == "--import-config" { + if args.len() == 2 { + hbb_common::config::Config::import(&args[1]); + } + return; + } + } + ui::start(&mut args[..]); +} + +#[cfg(feature = "cli")] +fn main() { + use clap::App; + let args = format!( + "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -s, --server... 'Start server'", + ); + let matches = App::new("rustdesk") + .version(crate::VERSION) + .author("CarrieZ Studio") + .about("RustDesk command line tool") + .args_from_usage(&args) + .get_matches(); + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + if let Some(p) = matches.value_of("port-forward") { + let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); + if options.len() < 3 { + log::error!("Wrong port-forward options"); + return; + } + let mut port = 0; + if let Ok(v) = options[1].parse::() { + port = v; + } else { + log::error!("Wrong local-port"); + return; + } + let mut remote_port = 0; + if let Ok(v) = options[2].parse::() { + remote_port = v; + } else { + log::error!("Wrong remote-port"); + return; + } + let mut remote_host = "localhost".to_owned(); + if options.len() > 3 { + remote_host = options[3].clone(); + } + cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port); + } +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs new file mode 100644 index 00000000000..6798fb1a381 --- /dev/null +++ b/src/platform/linux.rs @@ -0,0 +1,446 @@ +use super::{CursorData, ResultType}; +use hbb_common::{allow_err, bail, log}; +use libc::{c_char, c_int, c_void}; +use std::{ + cell::RefCell, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; +type Xdo = *const c_void; + +pub const PA_SAMPLE_RATE: u32 = 24000; + +thread_local! { + static XDO: RefCell = RefCell::new(unsafe { xdo_new(std::ptr::null()) }); + static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); +} + +extern "C" { + fn xdo_get_mouse_location( + xdo: Xdo, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, + ) -> c_int; + fn xdo_new(display: *const c_char) -> Xdo; +} + +#[link(name = "X11")] +extern "C" { + fn XOpenDisplay(display_name: *const c_char) -> *mut c_void; +// fn XCloseDisplay(d: *mut c_void) -> c_int; +} + +#[link(name = "Xfixes")] +extern "C" { + // fn XFixesQueryExtension(dpy: *mut c_void, event: *mut c_int, error: *mut c_int) -> c_int; + fn XFixesGetCursorImage(dpy: *mut c_void) -> *const xcb_xfixes_get_cursor_image; + fn XFree(data: *mut c_void); +} + +// /usr/include/X11/extensions/Xfixes.h +#[repr(C)] +pub struct xcb_xfixes_get_cursor_image { + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub xhot: u16, + pub yhot: u16, + pub cursor_serial: libc::c_long, + pub pixels: *const libc::c_long, +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + let mut res = None; + XDO.with(|xdo| { + if let Ok(xdo) = xdo.try_borrow_mut() { + if xdo.is_null() { + return; + } + let mut x: c_int = 0; + let mut y: c_int = 0; + unsafe { + xdo_get_mouse_location(*xdo, &mut x as _, &mut y as _, std::ptr::null_mut()); + } + res = Some((x, y)); + } + }); + res +} + +pub fn reset_input_cache() {} + +pub fn get_cursor() -> ResultType> { + let mut res = None; + DISPLAY.with(|conn| { + if let Ok(d) = conn.try_borrow_mut() { + if !d.is_null() { + unsafe { + let img = XFixesGetCursorImage(*d); + if !img.is_null() { + res = Some((*img).cursor_serial as u64); + XFree(img as _); + } + } + } + } + }); + Ok(res) +} + +pub fn get_cursor_data(hcursor: u64) -> ResultType { + let mut res = None; + DISPLAY.with(|conn| { + if let Ok(ref mut d) = conn.try_borrow_mut() { + if !d.is_null() { + unsafe { + let img = XFixesGetCursorImage(**d); + if !img.is_null() && hcursor == (*img).cursor_serial as u64 { + let mut cd: CursorData = Default::default(); + cd.hotx = (*img).xhot as _; + cd.hoty = (*img).yhot as _; + cd.width = (*img).width as _; + cd.height = (*img).height as _; + // to-do: how about if it is 0 + cd.id = (*img).cursor_serial as _; + let pixels = + std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _); + cd.colors.resize(pixels.len() * 4, 0); + for y in 0..cd.height { + for x in 0..cd.width { + let pos = (y * cd.width + x) as usize; + let p = pixels[pos]; + let a = (p >> 24) & 0xff; + let r = (p >> 16) & 0xff; + let g = (p >> 8) & 0xff; + let b = (p >> 0) & 0xff; + if a == 0 { + continue; + } + let pos = pos * 4; + cd.colors[pos] = r as _; + cd.colors[pos + 1] = g as _; + cd.colors[pos + 2] = b as _; + cd.colors[pos + 3] = a as _; + } + } + res = Some(cd); + } + if !img.is_null() { + XFree(img as _); + } + } + } + } + }); + match res { + Some(x) => Ok(x), + _ => bail!("Failed to get cursor image of {}", hcursor), + } +} + +pub fn start_os_service() { + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + let mut uid = "".to_owned(); + let mut server: Option = None; + if let Err(err) = ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) { + println!("Failed to set Ctrl-C handler: {}", err); + } + + let mut cm0 = false; + let mut last_restart = std::time::Instant::now(); + while running.load(Ordering::SeqCst) { + let cm = get_cm(); + let tmp = get_active_userid(); + let mut start_new = false; + if tmp != uid && !tmp.is_empty() { + uid = tmp; + log::info!("uid of seat0: {}", uid); + std::env::set_var("XAUTHORITY", format!("/run/user/{}/gdm/Xauthority", uid)); + std::env::set_var("DISPLAY", get_display()); + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + last_restart = std::time::Instant::now(); + } + } else if !cm + && ((cm0 && last_restart.elapsed().as_secs() > 60) + || last_restart.elapsed().as_secs() > 3600) + { + // restart server if new connections all closed, or every one hour, + // as a workaround to resolve "SpotUdp" (dns resolve) + // and x server get displays failure issue + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + last_restart = std::time::Instant::now(); + log::info!("restart server"); + } + } + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + if start_new { + match crate::run_me(vec!["--server"]) { + Ok(ps) => server = Some(ps), + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } + } + cm0 = cm; + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + } + + if let Some(ps) = server.take().as_mut() { + allow_err!(ps.kill()); + } + println!("Exit"); +} + +fn get_active_userid() -> String { + get_value_of_seat0(1) +} + +fn is_active(sid: &str) -> bool { + if let Ok(output) = std::process::Command::new("loginctl") + .args(vec!["show-session", "-p", "State", sid]) + .output() + { + String::from_utf8_lossy(&output.stdout).contains("active") + } else { + false + } +} + +fn get_cm() -> bool { + if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains(&format!( + "{} --cm", + std::env::current_exe() + .unwrap_or("".into()) + .to_string_lossy() + )) { + return true; + } + } + } + false +} + +fn get_display() -> String { + let user = get_active_username(); + if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut iter = line.split_whitespace(); + let a = iter.nth(1); + let b = iter.next(); + if a == b { + if let Some(b) = b { + if b.starts_with(":") { + return b.to_owned(); + } + } + } + } + } + // above not work for gdm user + if let Ok(output) = std::process::Command::new("ls") + .args(vec!["-l", "/tmp/.X11-unix/"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut iter = line.split_whitespace(); + if iter.nth(2) == Some(&user) { + if let Some(x) = iter.last() { + if x.starts_with("X") { + return x.replace("X", ":").to_owned(); + } + } + } + } + } + "".to_owned() +} + +fn get_value_of_seat0(i: usize) -> String { + if let Ok(output) = std::process::Command::new("loginctl").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("seat0") { + if let Some(sid) = line.split_whitespace().nth(0) { + if is_active(sid) { + if let Some(uid) = line.split_whitespace().nth(i) { + return uid.to_owned(); + } + } + } + } + } + } + return "".to_owned(); +} + +pub fn get_display_server() -> String { + let session = get_value_of_seat0(0); + if let Ok(output) = std::process::Command::new("loginctl") + .args(vec!["show-session", "-p", "Type", &session]) + .output() + { + String::from_utf8_lossy(&output.stdout) + .replace("Type=", "") + .trim_end() + .into() + } else { + "".to_owned() + } +} + +pub fn is_login_wayland() -> bool { + if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { + contents.contains("#WaylandEnable=false") + } else { + false + } +} + +pub fn fix_login_wayland() { + match std::process::Command::new("pkexec") + .args(vec![ + "sed", + "-i", + "s/#WaylandEnable=false/WaylandEnable=false/g", + "/etc/gdm3/custom.conf", + ]) + .output() + { + Ok(x) => { + let x = String::from_utf8_lossy(&x.stderr); + if !x.is_empty() { + log::error!("fix_login_wayland failed: {}", x); + } + } + Err(err) => { + log::error!("fix_login_wayland failed: {}", err); + } + } +} + +// to-do: test the other display manager +fn _get_display_manager() -> String { + if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") { + if let Some(x) = x.split("/").last() { + return x.to_owned(); + } + } + "gdm3".to_owned() +} + +pub fn get_active_username() -> String { + get_value_of_seat0(2) +} + +pub fn is_prelogin() -> bool { + let n = get_active_userid().len(); + n < 4 && n > 1 +} + +pub fn is_root() -> bool { + crate::username() == "root" +} + +pub fn run_as_user(arg: &str) -> ResultType> { + let uid = get_active_userid(); + let cmd = std::env::current_exe()?; + let task = std::process::Command::new("sudo") + .args(vec![ + &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str, + "-u", + &get_active_username(), + cmd.to_str().unwrap_or(""), + arg, + ]) + .spawn()?; + Ok(Some(task)) +} + +pub fn get_pa_monitor() -> String { + get_pa_sources() + .drain(..) + .map(|x| x.0) + .filter(|x| x.contains("monitor")) + .next() + .unwrap_or("".to_owned()) +} + +pub fn get_pa_source_name(desc: &str) -> String { + get_pa_sources() + .drain(..) + .filter(|x| x.1 == desc) + .map(|x| x.0) + .next() + .unwrap_or("".to_owned()) +} + +pub fn get_pa_sources() -> Vec<(String, String)> { + use pulsectl::controllers::*; + let mut out = Vec::new(); + match SourceController::create() { + Ok(mut handler) => { + if let Ok(devices) = handler.list_devices() { + for dev in devices.clone() { + out.push(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + } + Err(err) => { + log::error!("Failed to get_pa_sources: {:?}", err); + } + } + out +} + +pub fn lock_screen() { + std::thread::spawn(move || { + use crate::server::input_service::handle_key; + use hbb_common::message_proto::*; + let mut evt = KeyEvent { + down: true, + modifiers: vec![ControlKey::Meta.into()], + ..Default::default() + }; + evt.set_chr('l' as _); + handle_key(&evt); + evt.down = false; + handle_key(&evt); + }); +} + +pub fn toggle_privacy_mode(_v: bool) { + // https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x +} + +pub fn block_input(_v: bool) { + // +} + +pub fn is_installed() -> bool { + true +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs new file mode 100644 index 00000000000..5834fd024ba --- /dev/null +++ b/src/platform/macos.rs @@ -0,0 +1,335 @@ +// https://developer.apple.com/documentation/appkit/nscursor +// https://github.com/servo/core-foundation-rs +// https://github.com/rust-windowing/winit + +use super::{CursorData, ResultType}; +use cocoa::{ + base::{id, nil, BOOL, NO, YES}, + foundation::{NSDictionary, NSPoint, NSSize, NSString}, +}; +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + dictionary::CFDictionaryRef, + string::CFStringRef, +}; +use core_graphics::{ + display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, + window::{kCGWindowName, kCGWindowOwnerPID}, +}; +use hbb_common::{allow_err, bail, log}; +use objc::{class, msg_send, sel, sel_impl}; +use scrap::{libc::c_void, quartz::ffi::*}; + +static mut LATEST_SEED: i32 = 0; + +extern "C" { + fn CGSCurrentCursorSeed() -> i32; + fn CGEventCreate(r: *const c_void) -> *const c_void; + fn CGEventGetLocation(e: *const c_void) -> CGPoint; + static kAXTrustedCheckOptionPrompt: CFStringRef; + fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; +} + +pub fn is_process_trusted(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + let value: id = msg_send![class!(NSNumber), numberWithBool: value]; + let options = NSDictionary::dictionaryWithObject_forKey_( + nil, + value, + kAXTrustedCheckOptionPrompt as _, + ); + AXIsProcessTrustedWithOptions(options as _) == YES + } +} + +// macOS >= 10.15 +// https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ +// remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk +pub fn is_can_screen_recording(prompt: bool) -> bool { + let mut can_record_screen: bool = false; + unsafe { + let our_pid: i32 = std::process::id() as _; + let our_pid: id = msg_send![class!(NSNumber), numberWithInteger: our_pid]; + let window_list = + CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); + let n = CFArrayGetCount(window_list); + let dock = NSString::alloc(nil).init_str("Dock"); + for i in 0..n { + let w: id = CFArrayGetValueAtIndex(window_list, i) as _; + let name: id = msg_send![w, valueForKey: kCGWindowName as id]; + if name.is_null() { + continue; + } + let pid: id = msg_send![w, valueForKey: kCGWindowOwnerPID as id]; + let is_me: BOOL = msg_send![pid, isEqual: our_pid]; + if is_me == YES { + continue; + } + let pid: i32 = msg_send![pid, intValue]; + let p: id = msg_send![ + class!(NSRunningApplication), + runningApplicationWithProcessIdentifier: pid + ]; + if p.is_null() { + // ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar" + continue; + } + let url: id = msg_send![p, executableURL]; + let exe_name: id = msg_send![url, lastPathComponent]; + if exe_name.is_null() { + continue; + } + let is_dock: BOOL = msg_send![exe_name, isEqual: dock]; + if is_dock == YES { + // ignore the Dock, which provides the desktop picture + continue; + } + can_record_screen = true; + break; + } + } + if !can_record_screen && prompt { + use scrap::{Capturer, Display}; + if let Ok(d) = Display::primary() { + Capturer::new(d, true).ok(); + } + } + can_record_screen +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + unsafe { + let e = CGEventCreate(0 as _); + let point = CGEventGetLocation(e); + CFRelease(e); + Some((point.x as _, point.y as _)) + } + /* + let mut pt: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] }; + let screen: id = unsafe { msg_send![class!(NSScreen), currentScreenForMouseLocation] }; + let frame: NSRect = unsafe { msg_send![screen, frame] }; + pt.x -= frame.origin.x; + pt.y -= frame.origin.y; + Some((pt.x as _, pt.y as _)) + */ +} + +pub fn get_cursor() -> ResultType> { + unsafe { + let seed = CGSCurrentCursorSeed(); + if seed == LATEST_SEED { + return Ok(None); + } + LATEST_SEED = seed; + } + let c = get_cursor_id()?; + Ok(Some(c.1)) +} + +pub fn reset_input_cache() { + unsafe { + LATEST_SEED = 0; + } +} + +fn get_cursor_id() -> ResultType<(id, u64)> { + unsafe { + let c: id = msg_send![class!(NSCursor), currentSystemCursor]; + if c == nil { + bail!("Failed to call [NSCursor currentSystemCursor]"); + } + let hotspot: NSPoint = msg_send![c, hotSpot]; + let img: id = msg_send![c, image]; + if img == nil { + bail!("Failed to call [NSCursor image]"); + } + let size: NSSize = msg_send![img, size]; + let tif: id = msg_send![img, TIFFRepresentation]; + if tif == nil { + bail!("Failed to call [NSImage TIFFRepresentation]"); + } + let rep: id = msg_send![class!(NSBitmapImageRep), imageRepWithData: tif]; + if rep == nil { + bail!("Failed to call [NSBitmapImageRep imageRepWithData]"); + } + let rep_size: NSSize = msg_send![rep, size]; + let mut hcursor = + size.width + size.height + hotspot.x + hotspot.y + rep_size.width + rep_size.height; + let x = (rep_size.width * hotspot.x / size.width) as usize; + let y = (rep_size.height * hotspot.y / size.height) as usize; + for i in 0..2 { + let mut x2 = x + i; + if x2 >= rep_size.width as usize { + x2 = rep_size.width as usize - 1; + } + let mut y2 = y + i; + if y2 >= rep_size.height as usize { + y2 = rep_size.height as usize - 1; + } + let color: id = msg_send![rep, colorAtX:x2 y:y2]; + if color != nil { + let r: f64 = msg_send![color, redComponent]; + let g: f64 = msg_send![color, greenComponent]; + let b: f64 = msg_send![color, blueComponent]; + let a: f64 = msg_send![color, alphaComponent]; + hcursor += (r + g + b + a) * (255 << i) as f64; + } + } + Ok((c, hcursor as _)) + } +} + +// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c +pub fn get_cursor_data(hcursor: u64) -> ResultType { + unsafe { + let (c, hcursor2) = get_cursor_id()?; + if hcursor != hcursor2 { + bail!("cursor changed"); + } + let hotspot: NSPoint = msg_send![c, hotSpot]; + let img: id = msg_send![c, image]; + let size: NSSize = msg_send![img, size]; + let reps: id = msg_send![img, representations]; + if reps == nil { + bail!("Failed to call [NSImage representations]"); + } + let nreps: usize = msg_send![reps, count]; + if nreps == 0 { + bail!("Get empty [NSImage representations]"); + } + let rep: id = msg_send![reps, objectAtIndex: 0]; + /* + let n: id = msg_send![class!(NSNumber), numberWithFloat:1.0]; + let props: id = msg_send![class!(NSDictionary), dictionaryWithObject:n forKey:NSString::alloc(nil).init_str("NSImageCompressionFactor")]; + let image_data: id = msg_send![rep, representationUsingType:2 properties:props]; + let () = msg_send![image_data, writeToFile:NSString::alloc(nil).init_str("cursor.jpg") atomically:0]; + */ + let mut colors: Vec = Vec::new(); + colors.reserve((size.height * size.width) as usize * 4); + // TIFF is rgb colrspace, no need to convert + // let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace]; + for y in 0..(size.height as _) { + for x in 0..(size.width as _) { + let color: id = msg_send![rep, colorAtX:x y:y]; + // let color: id = msg_send![color, colorUsingColorSpace: cs]; + if color == nil { + continue; + } + let r: f64 = msg_send![color, redComponent]; + let g: f64 = msg_send![color, greenComponent]; + let b: f64 = msg_send![color, blueComponent]; + let a: f64 = msg_send![color, alphaComponent]; + colors.push((r * 255.) as _); + colors.push((g * 255.) as _); + colors.push((b * 255.) as _); + colors.push((a * 255.) as _); + } + } + Ok(CursorData { + id: hcursor, + colors, + hotx: hotspot.x as _, + hoty: hotspot.y as _, + width: size.width as _, + height: size.height as _, + ..Default::default() + }) + } +} + +fn get_active_user(t: &str) -> String { + if let Ok(output) = std::process::Command::new("ls") + .args(vec![t, "/dev/console"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(n) = line.split_whitespace().nth(2) { + return n.to_owned(); + } + } + } + "".to_owned() +} + +pub fn get_active_username() -> String { + get_active_user("-l") +} + +pub fn get_active_userid() -> String { + get_active_user("-n") +} + +pub fn is_prelogin() -> bool { + get_active_userid() == "0" +} + +pub fn is_root() -> bool { + crate::username() == "root" +} + +pub fn run_as_user(arg: &str) -> ResultType> { + let uid = get_active_userid(); + let cmd = std::env::current_exe()?; + let task = std::process::Command::new("launchctl") + .args(vec!["asuser", &uid, cmd.to_str().unwrap_or(""), arg]) + .spawn()?; + Ok(Some(task)) +} + +pub fn lock_screen() { + std::process::Command::new( + "/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession", + ) + .arg("-suspend") + .output() + .ok(); +} + +pub fn start_os_service() { + let mut server: Option = None; + let mut uid = "".to_owned(); + loop { + let tmp = get_active_userid(); + let mut start_new = false; + if tmp != uid && !tmp.is_empty() { + uid = tmp; + log::info!("active uid: {}", uid); + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + } + } + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + if start_new { + match crate::run_me(vec!["--server"]) { + Ok(ps) => server = Some(ps), + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } + } + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + } +} + +pub fn toggle_privacy_mode(_v: bool) { + // https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily +} + +pub fn block_input(_v: bool) { + // +} + +pub fn is_installed() -> bool { + true +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 00000000000..0bb979b7c12 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,46 @@ +#[cfg(target_os = "linux")] +pub use linux::*; +#[cfg(target_os = "macos")] +pub use macos::*; +#[cfg(windows)] +pub use windows::*; + +#[cfg(windows)] +pub mod windows; + +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(target_os = "linux")] +pub mod linux; + +use hbb_common::{message_proto::CursorData, ResultType}; +const SERVICE_INTERVAL: u64 = 300; + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_cursor_data() { + for _ in 0..30 { + if let Some(hc) = get_cursor().unwrap() { + let cd = get_cursor_data(hc).unwrap(); + repng::encode( + std::fs::File::create("cursor.png").unwrap(), + cd.width as _, + cd.height as _, + &cd.colors[..], + ) + .unwrap(); + } + #[cfg(target_os = "macos")] + macos::is_process_trusted(false); + } + } + #[test] + fn test_get_cursor_pos() { + for _ in 0..30 { + assert!(!get_cursor_pos().is_none()); + } + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs new file mode 100644 index 00000000000..ac358092fd1 --- /dev/null +++ b/src/platform/windows.rs @@ -0,0 +1,981 @@ +use super::{CursorData, ResultType}; +use crate::ipc; +use hbb_common::{ + allow_err, bail, + config::{Config, APP_NAME}, + futures_util::stream::StreamExt, + log, sleep, timeout, tokio, +}; +use std::io::prelude::*; +use std::{ + ffi::OsString, + io, mem, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; +use winapi::{ + shared::{minwindef::*, ntdef::NULL, windef::*}, + um::{ + errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, + processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*, + }, +}; +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, +}; + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + unsafe { + let mut out = mem::MaybeUninit::uninit().assume_init(); + if GetCursorPos(&mut out) == FALSE { + return None; + } + return Some((out.x, out.y)); + } +} + +pub fn reset_input_cache() {} + +pub fn get_cursor() -> ResultType> { + unsafe { + let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init(); + ci.cbSize = std::mem::size_of::() as _; + if GetCursorInfo(&mut ci) == FALSE { + return Err(io::Error::last_os_error().into()); + } + if ci.flags & CURSOR_SHOWING == 0 { + Ok(None) + } else { + Ok(Some(ci.hCursor as _)) + } + } +} + +struct IconInfo(ICONINFO); + +impl IconInfo { + fn new(icon: HICON) -> ResultType { + unsafe { + let mut ii = mem::MaybeUninit::uninit().assume_init(); + if GetIconInfo(icon, &mut ii) == FALSE { + Err(io::Error::last_os_error().into()) + } else { + let ii = Self(ii); + if ii.0.hbmMask.is_null() { + bail!("Cursor bitmap handle is NULL"); + } + return Ok(ii); + } + } + } + + fn is_color(&self) -> bool { + !self.0.hbmColor.is_null() + } +} + +impl Drop for IconInfo { + fn drop(&mut self) { + unsafe { + if !self.0.hbmColor.is_null() { + DeleteObject(self.0.hbmColor as _); + } + if !self.0.hbmMask.is_null() { + DeleteObject(self.0.hbmMask as _); + } + } + } +} + +// https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp +// https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx +pub fn get_cursor_data(hcursor: u64) -> ResultType { + unsafe { + let mut ii = IconInfo::new(hcursor as _)?; + let bm_mask = get_bitmap(ii.0.hbmMask)?; + let mut width = bm_mask.bmWidth; + let mut height = if ii.is_color() { + bm_mask.bmHeight + } else { + bm_mask.bmHeight / 2 + }; + let cbits_size = width * height * 4; + let mut cbits: Vec = Vec::new(); + cbits.resize(cbits_size as _, 0); + let mut mbits: Vec = Vec::new(); + mbits.resize((bm_mask.bmWidthBytes * bm_mask.bmHeight) as _, 0); + let r = GetBitmapBits(ii.0.hbmMask, mbits.len() as _, mbits.as_mut_ptr() as _); + if r == 0 { + bail!("Failed to copy bitmap data"); + } + if r != (mbits.len() as i32) { + bail!( + "Invalid mask cursor buffer size, got {} bytes, expected {}", + r, + mbits.len() + ); + } + let do_outline; + if ii.is_color() { + get_rich_cursor_data(ii.0.hbmColor, width, height, &mut cbits)?; + do_outline = fix_cursor_mask( + &mut mbits, + &mut cbits, + width as _, + height as _, + bm_mask.bmWidthBytes as _, + ); + } else { + do_outline = handleMask( + cbits.as_mut_ptr(), + mbits.as_ptr(), + width, + height, + bm_mask.bmWidthBytes, + ) > 0; + } + if do_outline { + let mut outline = Vec::new(); + outline.resize(((width + 2) * (height + 2) * 4) as _, 0); + drawOutline(outline.as_mut_ptr(), cbits.as_ptr(), width, height); + cbits = outline; + width += 2; + height += 2; + ii.0.xHotspot += 1; + ii.0.yHotspot += 1; + } + + Ok(CursorData { + id: hcursor, + colors: cbits, + hotx: ii.0.xHotspot as _, + hoty: ii.0.yHotspot as _, + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +#[inline] +fn get_bitmap(handle: HBITMAP) -> ResultType { + unsafe { + let mut bm: BITMAP = mem::zeroed(); + if GetObjectA( + handle as _, + std::mem::size_of::() as _, + &mut bm as *mut BITMAP as *mut _, + ) == FALSE + { + return Err(io::Error::last_os_error().into()); + } + if bm.bmPlanes != 1 { + bail!("Unsupported multi-plane cursor"); + } + if bm.bmBitsPixel != 1 { + bail!("Unsupported cursor mask format"); + } + Ok(bm) + } +} + +struct DC(HDC); + +impl DC { + fn new() -> ResultType { + unsafe { + let dc = GetDC(0 as _); + if dc.is_null() { + bail!("Failed to get a drawing context"); + } + Ok(Self(dc)) + } + } +} + +impl Drop for DC { + fn drop(&mut self) { + unsafe { + if !self.0.is_null() { + ReleaseDC(0 as _, self.0); + } + } + } +} + +struct CompatibleDC(HDC); + +impl CompatibleDC { + fn new(existing: HDC) -> ResultType { + unsafe { + let dc = CreateCompatibleDC(existing); + if dc.is_null() { + bail!("Failed to get a compatible drawing context"); + } + Ok(Self(dc)) + } + } +} + +impl Drop for CompatibleDC { + fn drop(&mut self) { + unsafe { + if !self.0.is_null() { + DeleteDC(self.0); + } + } + } +} + +struct BitmapDC(CompatibleDC, HBITMAP); + +impl BitmapDC { + fn new(hdc: HDC, hbitmap: HBITMAP) -> ResultType { + unsafe { + let dc = CompatibleDC::new(hdc)?; + let oldbitmap = SelectObject(dc.0, hbitmap as _) as HBITMAP; + if oldbitmap.is_null() { + bail!("Failed to select CompatibleDC"); + } + Ok(Self(dc, oldbitmap)) + } + } + + fn dc(&self) -> HDC { + (self.0).0 + } +} + +impl Drop for BitmapDC { + fn drop(&mut self) { + unsafe { + if !self.1.is_null() { + SelectObject((self.0).0, self.1 as _); + } + } + } +} + +#[inline] +fn get_rich_cursor_data( + hbm_color: HBITMAP, + width: i32, + height: i32, + out: &mut Vec, +) -> ResultType<()> { + unsafe { + let dc = DC::new()?; + let bitmap_dc = BitmapDC::new(dc.0, hbm_color)?; + if get_di_bits(out.as_mut_ptr(), bitmap_dc.dc(), hbm_color, width, height) > 0 { + bail!("Failed to get di bits: {}", get_error()); + } + } + Ok(()) +} + +fn fix_cursor_mask( + mbits: &mut Vec, + cbits: &mut Vec, + width: usize, + height: usize, + width_bytes: usize, +) -> bool { + let mut pix_idx = 0; + for _ in 0..height { + for _ in 0..width { + if cbits[pix_idx + 3] != 0 { + return false; + } + pix_idx += 4; + } + } + + let packed_width_bytes = (width + 7) >> 3; + + // Pack and invert bitmap data (mbits) + // borrow from tigervnc + for y in 0..height { + for x in 0..packed_width_bytes { + mbits[y * packed_width_bytes + x] = !mbits[y * width_bytes + x]; + } + } + + // Replace "inverted background" bits with black color to ensure + // cross-platform interoperability. Not beautiful but necessary code. + // borrow from tigervnc + let bytes_row = width << 2; + for y in 0..height { + let mut bitmask: u8 = 0x80; + for x in 0..width { + let mask_idx = y * packed_width_bytes + (x >> 3); + let pix_idx = y * bytes_row + (x << 2); + if (mbits[mask_idx] & bitmask) == 0 { + for b1 in 0..4 { + if cbits[pix_idx + b1] != 0 { + mbits[mask_idx] ^= bitmask; + for b2 in b1..4 { + cbits[pix_idx + b2] = 0x00; + } + break; + } + } + } + bitmask >>= 1; + if bitmask == 0 { + bitmask = 0x80; + } + } + } + + // borrow from noVNC + let mut pix_idx = 0; + for y in 0..height { + for x in 0..width { + let mask_idx = y * packed_width_bytes + (x >> 3); + let alpha = if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 { + 0 + } else { + 255 + }; + let a = cbits[pix_idx + 2]; + let b = cbits[pix_idx + 1]; + let c = cbits[pix_idx]; + cbits[pix_idx] = a; + cbits[pix_idx + 1] = b; + cbits[pix_idx + 2] = c; + cbits[pix_idx + 3] = alpha; + pix_idx += 4; + } + } + return true; +} + +define_windows_service!(ffi_service_main, service_main); + +fn service_main(arguments: Vec) { + if let Err(e) = run_service(arguments) { + log::error!("run_service failed: {}", e); + } +} + +pub fn start_os_service() { + if let Err(e) = windows_service::service_dispatcher::start(APP_NAME, ffi_service_main) { + log::error!("start_service failed: {}", e); + } +} + +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +extern "C" { + fn LaunchProcessWin(cmd: *const u16, session_id: DWORD, as_user: BOOL) -> HANDLE; + fn selectInputDesktop() -> BOOL; + fn inputDesktopSelected() -> BOOL; + fn handleMask(out: *mut u8, mask: *const u8, width: i32, height: i32, bmWidthBytes: i32) + -> i32; + fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32); + fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32; + fn blank_screen(v: BOOL); + fn BlockInput(v: BOOL) -> BOOL; +} + +#[tokio::main(basic_scheduler)] +async fn run_service(_arguments: Vec) -> ResultType<()> { + let event_handler = move |control_event| -> ServiceControlHandlerResult { + log::info!("Got service control event: {:?}", control_event); + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + ServiceControl::Stop => { + send_close(crate::POSTFIX_SERVICE).ok(); + ServiceControlHandlerResult::NoError + } + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler + let status_handle = service_control_handler::register(APP_NAME, event_handler)?; + + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: SERVICE_TYPE, + // The new state + current_state: ServiceState::Running, + // Accept stop events when running + controls_accepted: ServiceControlAccept::STOP, + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + process_id: None, + }; + + // Tell the system that the service is running now + status_handle.set_service_status(next_status)?; + + let mut session_id = unsafe { WTSGetActiveConsoleSessionId() }; + log::info!("session id {}", session_id); + let mut h_process = launch_server(session_id, true).await.unwrap_or(NULL); + let mut incoming = ipc::new_listener(crate::POSTFIX_SERVICE).await?; + loop { + let res = timeout(super::SERVICE_INTERVAL, incoming.next()).await; + match res { + Ok(res) => match res { + Some(Ok(stream)) => { + let mut stream = ipc::Connection::new(stream); + if let Ok(Some(data)) = stream.next_timeout(1000).await { + match data { + ipc::Data::Close => { + log::info!("close received"); + break; + } + ipc::Data::SAS => { + send_sas(); + } + _ => {} + } + } + } + _ => {} + }, + Err(_) => { + // timeout + unsafe { + let tmp = WTSGetActiveConsoleSessionId(); + if tmp == 0xFFFFFFFF { + continue; + } + let mut close_sent = false; + if tmp != session_id { + log::info!("session changed from {} to {}", session_id, tmp); + session_id = tmp; + send_close_async("").await.ok(); + close_sent = true; + } + let mut exit_code: DWORD = 0; + if h_process.is_null() + || (GetExitCodeProcess(h_process, &mut exit_code) == TRUE + && exit_code != STILL_ACTIVE + && CloseHandle(h_process) == TRUE) + { + match launch_server(session_id, !close_sent).await { + Ok(ptr) => { + h_process = ptr; + } + Err(err) => { + log::error!("Failed to launch server: {}", err); + } + } + } + } + } + } + } + + if !h_process.is_null() { + send_close_async("").await.ok(); + unsafe { CloseHandle(h_process) }; + } + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} + +async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType { + if close_first { + // in case started some elsewhere + send_close_async("").await.ok(); + } + let cmd = format!( + "\"{}\" --server", + std::env::current_exe()?.to_str().unwrap_or("") + ); + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(&cmd) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE) }; + if h.is_null() { + log::error!("Failed to luanch server: {}", get_error()); + } + Ok(h) +} + +pub fn run_as_user(arg: &str) -> ResultType> { + let cmd = format!( + "\"{}\" {}", + std::env::current_exe()?.to_str().unwrap_or(""), + arg, + ); + let session_id = unsafe { WTSGetActiveConsoleSessionId() }; + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(&cmd) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE) }; + if h.is_null() { + bail!( + "Failed to launch {} with session id {}: {}", + arg, + session_id, + get_error() + ); + } + Ok(None) +} + +#[tokio::main(basic_scheduler)] +async fn send_close(postfix: &str) -> ResultType<()> { + send_close_async(postfix).await +} + +async fn send_close_async(postfix: &str) -> ResultType<()> { + ipc::connect(1000, postfix) + .await? + .send(&ipc::Data::Close) + .await?; + // sleep a while to wait for closing and exit + sleep(0.1).await; + Ok(()) +} + +// https://docs.microsoft.com/en-us/windows/win32/api/sas/nf-sas-sendsas +// https://www.cnblogs.com/doutu/p/4892726.html +fn send_sas() { + #[link(name = "sas")] + extern "C" { + pub fn SendSAS(AsUser: BOOL); + } + unsafe { + log::info!("SAS received"); + SendSAS(FALSE); + } +} + +lazy_static::lazy_static! { + static ref SUPRESS: Arc> = Arc::new(Mutex::new(Instant::now())); +} + +pub fn desktop_changed() -> bool { + unsafe { inputDesktopSelected() == FALSE } +} + +pub fn try_change_desktop() -> bool { + unsafe { + if inputDesktopSelected() == FALSE { + let res = selectInputDesktop() == TRUE; + if !res { + let mut s = SUPRESS.lock().unwrap(); + if s.elapsed() > std::time::Duration::from_secs(3) { + log::error!("Failed to switch desktop: {}", get_error()); + *s = Instant::now(); + } + } else { + log::info!("Desktop switched"); + } + return res; + } + } + return false; +} + +fn get_error() -> String { + unsafe { + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let errno = GetLastError(); + let chars_copied = FormatMessageW( + FORMAT_MESSAGE_IGNORE_INSERTS + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + std::ptr::null(), + errno, + 0, + buff.as_mut_ptr(), + (buff_size + 1) as u32, + std::ptr::null_mut(), + ); + if chars_copied == 0 { + return "".to_owned(); + } + let mut curr_char: usize = chars_copied as usize; + while curr_char > 0 { + let ch = buff[curr_char]; + + if ch >= ' ' as u16 { + break; + } + curr_char -= 1; + } + let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char); + let err_msg = String::from_utf16(sl); + return err_msg.unwrap_or("".to_owned()); + } +} + +pub fn get_active_username() -> String { + let name = crate::username(); + if name != "SYSTEM" { + return name; + } + if let Ok(output) = std::process::Command::new("query").arg("user").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("Active") { + if let Some(name) = line.split_whitespace().next() { + if name.starts_with(">") { + return name.replace(">", ""); + } else { + return name.to_string(); + } + } + } + } + } + return "".to_owned(); +} + +pub fn is_prelogin() -> bool { + let username = get_active_username(); + username.is_empty() || username == "SYSTEM" +} + +pub fn is_root() -> bool { + crate::username() == "SYSTEM" +} + +pub fn lock_screen() { + extern "C" { + pub fn LockWorkStation() -> BOOL; + } + unsafe { + LockWorkStation(); + } +} + +pub fn get_install_info() -> (String, String, String, String) { + let subkey = format!( + "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + APP_NAME + ); + let mut pf = "C:\\Program Files".to_owned(); + if let Ok(output) = std::process::Command::new("echo") + .arg("%ProgramFiles%") + .output() + { + let tmp = String::from_utf8_lossy(&output.stdout); + if !tmp.starts_with("%") { + pf = tmp.to_string(); + } + } + let path = format!("{}\\{}", pf, APP_NAME); + let start_menu = format!( + "%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\{}", + APP_NAME + ); + let exe = format!("{}\\{}.exe", path, APP_NAME); + (subkey, path, start_menu, exe) +} + +pub fn update_me() -> ResultType<()> { + let (_, _, _, exe) = get_install_info(); + let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + let cmds = format!( + " + sc stop {app_name} + taskkill /F /IM {app_name}.exe + copy /Y \"{src_exe}\" \"{exe}\" + sc start {app_name} + ", + src_exe = src_exe, + exe = exe, + app_name = APP_NAME, + ); + std::thread::sleep(std::time::Duration::from_millis(1000)); + run_cmds(cmds, false)?; + std::thread::sleep(std::time::Duration::from_millis(2000)); + std::process::Command::new(&exe).spawn()?; + std::process::Command::new(&exe) + .args(&["--remove", &src_exe]) + .spawn()?; + Ok(()) +} + +pub fn install_me(options: &str) -> ResultType<()> { + let (subkey, path, start_menu, exe) = get_install_info(); + let mut version_major = "0"; + let mut version_minor = "0"; + let mut version_build = "0"; + let versions: Vec<&str> = crate::VERSION.split(".").collect(); + if versions.len() > 0 { + version_major = versions[0]; + } + if versions.len() > 1 { + version_minor = versions[1]; + } + if versions.len() > 2 { + version_build = versions[2]; + } + + let tmp_path = "C:\\Windows\\temp"; + let mk_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\{app_name}.lnk\" + +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" +oLink.Save + ", + tmp_path = tmp_path, + app_name = APP_NAME, + exe = exe, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + // https://superuser.com/questions/392061/how-to-make-a-shortcut-from-cmd + let uninstall_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\Uninstall {app_name}.lnk\" +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--uninstall\" + oLink.IconLocation = \"msiexec.exe\" +oLink.Save + ", + tmp_path = tmp_path, + app_name = APP_NAME, + exe = exe, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + let mut shortcuts = Default::default(); + if options.contains("desktopicon") { + shortcuts = format!( + "copy /Y \"{}\\{}.lnk\" \"%PUBLIC%\\Desktop\\\"", + tmp_path, APP_NAME + ); + } + if options.contains("startmenu") { + shortcuts = format!( + "{} +md \"{start_menu}\" +copy /Y \"{tmp_path}\\{app_name}.lnk\" \"{start_menu}\\\" +copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" + ", + shortcuts, + start_menu = start_menu, + tmp_path = tmp_path, + app_name = APP_NAME + ); + } + + let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; + let size = meta.len() / 1024; + // save_tmp is for ensuring not copying file while writing + let config_path = Config::save_tmp(); + let ext = APP_NAME.to_lowercase(); + // https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa + // https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10 + // https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html + let cmds = format!( + " +md \"{path}\" +copy /Y \"{src_exe}\" \"{exe}\" +reg add {subkey} /f +reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" +reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\" +reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v InstallLocation /t REG_SZ /d \"{path}\" +reg add {subkey} /f /v Publisher /t REG_SZ /d \"{app_name}\" +reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {major} +reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {minor} +reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {build} +reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\" +reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} +reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0 +reg add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1 +\"{mk_shortcut}\" +\"{uninstall_shortcut}\" +{shortcuts} +del /f \"{mk_shortcut}\" +del /f \"{uninstall_shortcut}\" +del /f \"{tmp_path}\\{app_name}.lnk\" +del /f \"{tmp_path}\\Uninstall {app_name}.lnk\" +reg add HKEY_CLASSES_ROOT\\.{ext} /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\" +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" --play \\\"%%1\\\"\" +sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\" +sc start {app_name} +sc stop {app_name} +sc delete {app_name} +sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\" +netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes +del /f \"{config_path}\" +del /f \"{config2_path}\" +sc start {app_name} + ", + path=path, + src_exe=std::env::current_exe()?.to_str().unwrap_or(""), + exe=exe, + subkey=subkey, + app_name=APP_NAME, + version=crate::VERSION, + major=version_major, + minor=version_minor, + build=version_build, + size=size, + mk_shortcut=mk_shortcut, + uninstall_shortcut=uninstall_shortcut, + tmp_path=tmp_path, + shortcuts=shortcuts, + config_path=config_path, + config2_path=config_path.replace(".toml", "2.toml"), + ext=ext, + ); + run_cmds(cmds, false)?; + std::thread::sleep(std::time::Duration::from_millis(2000)); + std::process::Command::new(exe).spawn()?; + std::thread::sleep(std::time::Duration::from_millis(1000)); + Ok(()) +} + +pub fn uninstall_me() -> ResultType<()> { + let (subkey, path, start_menu, _) = get_install_info(); + let ext = APP_NAME.to_lowercase(); + let cmds = format!( + " +sc stop {app_name} +sc delete {app_name} +taskkill /F /IM {app_name}.exe +reg delete {subkey} /f +reg delete HKEY_CLASSES_ROOT\\.{ext} /f +rd /s /q \"{path}\" +rd /s /q \"{start_menu}\" +del /f /q \"%PUBLIC%\\Desktop\\{app_name}*\" +netsh advfirewall firewall delete rule name=\"{app_name} Service\" + ", + app_name = APP_NAME, + path = path, + subkey = subkey, + start_menu = start_menu, + ext = ext, + ); + run_cmds(cmds, true) +} + +fn write_cmds(cmds: String, ext: &str) -> ResultType { + let mut tmp = std::env::temp_dir(); + tmp.push(format!("{}_{}.{}", APP_NAME, crate::get_time(), ext)); + let mut cmds = cmds; + if ext == "cmd" { + cmds = format!("{}\ndel /f \"{}\"", cmds, tmp.to_str().unwrap_or("")); + } + let mut file = std::fs::File::create(&tmp)?; + file.write_all(cmds.as_bytes())?; + file.sync_all()?; + return Ok(tmp); +} + +fn run_cmds(cmds: String, show: bool) -> ResultType<()> { + let tmp = write_cmds(cmds, "cmd")?; + let res = runas::Command::new(tmp.to_str().unwrap_or("")) + .show(show) + .force_prompt(true) + .status(); + // double confirm delete, because below delete not work if program + // exit immediately such as --uninstall + allow_err!(std::fs::remove_file(tmp)); + let _ = res?; + Ok(()) +} + +pub fn toggle_privacy_mode(v: bool) { + let v = if v { TRUE } else { FALSE }; + unsafe { + blank_screen(v); + BlockInput(v); + } +} + +pub fn block_input(v: bool) { + let v = if v { TRUE } else { FALSE }; + unsafe { + BlockInput(v); + } +} + +pub fn add_recent_document(path: &str) { + extern "C" { + fn AddRecentDocument(path: *const u16); + } + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(path) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + unsafe { + AddRecentDocument(wstr); + } +} + +pub fn is_installed() -> bool { + use windows_service::{ + service::ServiceAccess, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + let (_, _, _, exe) = get_install_info(); + if !std::fs::metadata(exe).is_ok() { + return false; + } + let manager_access = ServiceManagerAccess::CONNECT; + if let Ok(service_manager) = ServiceManager::local_computer(None::<&str>, manager_access) { + if let Ok(_) = service_manager.open_service(APP_NAME, ServiceAccess::QUERY_CONFIG) { + return true; + } + } + return false; +} + +pub fn get_installed_version() -> String { + let (_, _, _, exe) = get_install_info(); + if let Ok(output) = std::process::Command::new(exe).arg("--version").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + return line.to_owned(); + } + } + "".to_owned() +} diff --git a/src/port_forward.rs b/src/port_forward.rs new file mode 100644 index 00000000000..c8950323ec4 --- /dev/null +++ b/src/port_forward.rs @@ -0,0 +1,163 @@ +use crate::client::*; +use hbb_common::{ + allow_err, bail, + config::CONNECT_TIMEOUT, + futures::SinkExt, + log, + message_proto::*, + protobuf::Message as _, + tcp, timeout, + tokio::{self, net::TcpStream, stream::StreamExt, sync::mpsc}, + tokio_util::codec::{BytesCodec, Framed}, + ResultType, Stream, +}; + +fn run_rdp(port: u16) { + std::process::Command::new("mstsc") + .arg(format!("/v:localhost:{}", port)) + .spawn() + .ok(); +} + +pub async fn listen( + id: String, + port: i32, + interface: impl Interface, + ui_receiver: mpsc::UnboundedReceiver, +) -> ResultType<()> { + let mut listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?; + let addr = listener.local_addr()?; + log::info!("listening on port {:?}", addr); + let is_rdp = port == 0; + if is_rdp { + run_rdp(addr.port()); + } + let mut ui_receiver = ui_receiver; + loop { + tokio::select! { + Ok((forward, addr)) = listener.accept() => { + log::info!("new connection from {:?}", addr); + let id = id.clone(); + let mut forward = Framed::new(forward, BytesCodec::new()); + match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward).await { + Ok(Some(stream)) => { + let interface = interface.clone(); + tokio::spawn(async move { + if let Err(err) = run_forward(forward, stream).await { + interface.msgbox("error", "Error", &err.to_string()); + } + log::info!("connection from {:?} closed", addr); + }); + } + Err(err) => { + interface.msgbox("error", "Error", &err.to_string()); + } + _ => {} + } + } + d = ui_receiver.recv() => { + match d { + Some(Data::Close) => { + break; + } + Some(Data::NewRDP) => { + run_rdp(addr.port()); + } + _ => {} + } + } + } + } + Ok(()) +} + +async fn connect_and_login( + id: &str, + ui_receiver: &mut mpsc::UnboundedReceiver, + interface: impl Interface, + forward: &mut Framed, +) -> ResultType> { + let (mut stream, _) = Client::start(&id).await?; + let mut interface = interface; + let mut buffer = Vec::new(); + loop { + tokio::select! { + res = timeout(CONNECT_TIMEOUT, stream.next()) => match res { + Err(_) => { + bail!("Timeout"); + } + Ok(Some(Ok(bytes))) => { + let msg_in = Message::parse_from_bytes(&bytes)?; + match msg_in.union { + Some(message::Union::hash(hash)) => { + interface.handle_hash(hash, &mut stream).await; + } + Some(message::Union::login_response(lr)) => match lr.union { + Some(login_response::Union::error(err)) => { + interface.handle_login_error(&err); + return Ok(None); + } + Some(login_response::Union::peer_info(pi)) => { + interface.handle_peer_info(pi); + break; + } + _ => {} + } + Some(message::Union::test_delay(t)) => { + interface.handle_test_delay(t, &mut stream).await; + } + _ => {} + } + } + _ => { + bail!("Reset by the peer"); + } + }, + d = ui_receiver.recv() => { + match d { + Some(Data::Login((password, remember))) => { + interface.handle_login_from_ui(password, remember, &mut stream).await; + } + _ => {} + } + }, + res = forward.next() => { + if let Some(Ok(bytes)) = res { + buffer.extend(bytes); + } else { + return Ok(None); + } + }, + } + } + stream.set_raw(); + if !buffer.is_empty() { + allow_err!(stream.send_bytes(buffer.into()).await); + } + Ok(Some(stream)) +} + +async fn run_forward(forward: Framed, stream: Stream) -> ResultType<()> { + log::info!("new port forwarding connection started"); + let mut forward = forward; + let mut stream = stream; + loop { + tokio::select! { + res = forward.next() => { + if let Some(Ok(bytes)) = res { + allow_err!(stream.send_bytes(bytes.into()).await); + } else { + break; + } + }, + res = stream.next() => { + if let Some(Ok(bytes)) = res { + allow_err!(forward.send(bytes.into()).await); + } else { + break; + } + }, + } + } + Ok(()) +} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs new file mode 100644 index 00000000000..bcb4cc4acf8 --- /dev/null +++ b/src/rendezvous_mediator.rs @@ -0,0 +1,416 @@ +use crate::server::{check_zombie, new as new_server, ServerPtr}; +use hbb_common::{ + allow_err, + config::{Config, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, + tcp::FramedStream, + tokio::{ + self, select, + time::{interval, Duration}, + }, + udp::FramedSocket, + AddrMangle, ResultType, +}; +use std::{ + net::SocketAddr, + sync::{Arc, Mutex}, + time::SystemTime, +}; +use uuid::Uuid; + +type Message = RendezvousMessage; + +lazy_static::lazy_static! { + pub static ref SOLVING_PK_MISMATCH: Arc> = Default::default(); +} + +#[derive(Clone)] +pub struct RendezvousMediator { + addr: SocketAddr, + host: String, + host_prefix: String, + rendezvous_servers: Vec, + last_id_pk_registery: String, +} + +impl RendezvousMediator { + pub async fn start_all() { + check_zombie(); + let server = new_server(); + loop { + Config::reset_online(); + if Config::get_option("stop-service").is_empty() { + let mut futs = Vec::new(); + let servers = Config::get_rendezvous_servers(); + for host in servers.clone() { + let server = server.clone(); + let servers = servers.clone(); + futs.push(tokio::spawn(async move { + allow_err!(Self::start(server, host, servers).await); + })); + } + join_all(futs).await; + } + sleep(1.).await; + } + } + + pub async fn start( + server: ServerPtr, + host: String, + rendezvous_servers: Vec, + ) -> ResultType<()> { + log::info!("start rendezvous mediator of {}", host); + let host_prefix: String = host + .split(".") + .next() + .map(|x| { + if x.parse::().is_ok() { + host.clone() + } else { + x.to_string() + } + }) + .unwrap_or(host.to_owned()); + let mut rz = Self { + addr: Config::get_any_listen_addr(), + host: host.clone(), + host_prefix, + rendezvous_servers, + last_id_pk_registery: "".to_owned(), + }; + allow_err!(rz.dns_check()); + let mut socket = FramedSocket::new(Config::get_any_listen_addr()).await?; + const TIMER_OUT: Duration = Duration::from_secs(1); + let mut timer = interval(TIMER_OUT); + let mut last_timer = SystemTime::UNIX_EPOCH; + const REG_INTERVAL: i64 = 12_000; + const REG_TIMEOUT: i64 = 3_000; + const MAX_FAILS1: i64 = 3; + const MAX_FAILS2: i64 = 6; + const DNS_INTERVAL: i64 = 60_000; + let mut fails = 0; + let mut last_register_resp = SystemTime::UNIX_EPOCH; + let mut last_register_sent = SystemTime::UNIX_EPOCH; + let mut last_dns_check = SystemTime::UNIX_EPOCH; + let mut old_latency = 0; + let mut ema_latency = 0; + loop { + select! { + Some(Ok((bytes, _))) = socket.next() => { + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::register_peer_response(rpr)) => { + if rpr.request_pk { + log::info!("request_pk received from {}", host); + allow_err!(rz.register_pk(&mut socket).await); + continue; + } + last_register_resp = SystemTime::now(); + let mut latency = last_register_resp.duration_since(last_register_sent).map(|d| d.as_micros() as i64).unwrap_or(0); + if ema_latency == 0 { + ema_latency = latency; + } else { + ema_latency = latency / 30 + (ema_latency * 29 / 30); + latency = ema_latency; + } + let mut n = latency / 5; + if n < 3000 { + n = 3000; + } + if (latency - old_latency).abs() > n || old_latency <= 0 { + Config::update_latency(&host, latency); + log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); + old_latency = latency; + } + fails = 0; + } + Some(rendezvous_message::Union::register_pk_response(rpr)) => { + match rpr.result.enum_value_or_default() { + register_pk_response::Result::OK => { + Config::set_key_confirmed(true); + Config::set_host_key_confirmed(&rz.host_prefix, true); + *SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned(); + last_register_resp = SystemTime::now(); + let latency = last_register_resp.duration_since(last_register_sent).map(|d| d.as_micros() as i64).unwrap_or(0); + Config::update_latency(&host, latency); + log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); + fails = 0; + } + register_pk_response::Result::UUID_MISMATCH => { + allow_err!(rz.handle_uuid_mismatch(&mut socket).await); + } + } + } + Some(rendezvous_message::Union::punch_hole(ph)) => { + let rz = rz.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_punch_hole(ph, server).await); + }); + } + Some(rendezvous_message::Union::request_relay(rr)) => { + let rz = rz.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_request_relay(rr, server).await); + }); + } + Some(rendezvous_message::Union::fetch_local_addr(fla)) => { + let rz = rz.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_intranet(fla, server).await); + }); + } + Some(rendezvous_message::Union::configure_update(cu)) => { + Config::set_option("rendezvous-servers".to_owned(), cu.rendezvous_servers.join(",")); + Config::set_serial(cu.serial); + } + _ => {} + } + } else { + log::debug!("Non-protobuf message bytes received: {:?}", bytes); + } + }, + _ = timer.tick() => { + if Config::get_rendezvous_servers() != rz.rendezvous_servers { + break; + } + if !Config::get_option("stop-service").is_empty() { + break; + } + if rz.addr.port() == 0 { + allow_err!(rz.dns_check()); + if rz.addr.port() == 0 { + continue; + } else { + // have to do this for osx, to avoid "Can't assign requested address" + // when socket created before OS network ready + socket = FramedSocket::new(Config::get_any_listen_addr()).await?; + } + } + let now = SystemTime::now(); + if now.duration_since(last_timer).map(|d| d < TIMER_OUT).unwrap_or(false) { + // a workaround of tokio timer bug + continue; + } + last_timer = now; + let elapsed_resp = now.duration_since(last_register_resp).map(|d| d.as_millis() as i64).unwrap_or(REG_INTERVAL); + let timeout = last_register_sent.duration_since(last_register_resp).map(|d| d.as_millis() as i64).unwrap_or(0) >= REG_TIMEOUT; + if timeout || elapsed_resp >= REG_INTERVAL { + allow_err!(rz.register_peer(&mut socket).await); + last_register_sent = now; + if timeout { + fails += 1; + if fails > MAX_FAILS2 { + Config::update_latency(&host, -1); + old_latency = 0; + if now.duration_since(last_dns_check).map(|d| d.as_millis() as i64).unwrap_or(0) > DNS_INTERVAL { + allow_err!(rz.dns_check()); + last_dns_check = now; + } + } else if fails > MAX_FAILS1 { + Config::update_latency(&host, 0); + old_latency = 0; + } + } + } + } + } + } + Ok(()) + } + + fn dns_check(&mut self) -> ResultType<()> { + self.addr = hbb_common::to_socket_addr(&crate::check_port(&self.host, RENDEZVOUS_PORT))?; + log::debug!("Lookup dns of {}", self.host); + Ok(()) + } + + async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> { + self.create_relay( + rr.socket_addr, + rr.relay_server, + rr.uuid, + server, + rr.secure, + false, + ) + .await + } + + async fn create_relay( + &self, + socket_addr: Vec, + relay_server: String, + uuid: String, + server: ServerPtr, + secure: bool, + initiate: bool, + ) -> ResultType<()> { + let peer_addr = AddrMangle::decode(&socket_addr); + log::info!( + "create_relay requested from from {:?}, relay_server: {}, uuid: {}, secure: {}", + peer_addr, + relay_server, + uuid, + secure, + ); + let mut socket = + FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?; + let mut msg_out = Message::new(); + let mut rr = RelayResponse { + socket_addr, + ..Default::default() + }; + if initiate { + rr.uuid = uuid.clone(); + rr.relay_server = relay_server.clone(); + rr.uuid = uuid.clone(); + rr.set_id(Config::get_id()); + } + msg_out.set_relay_response(rr); + socket.send(&msg_out).await?; + crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure).await; + Ok(()) + } + + async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let peer_addr = AddrMangle::decode(&fla.socket_addr); + log::debug!("Handle intranet from {:?}", peer_addr); + let (mut socket, port) = { + let socket = + FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT) + .await?; + let port = socket.get_ref().local_addr()?.port(); + (socket, port) + }; + let local_addr = socket.get_ref().local_addr()?; + let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), port).parse()?; + let mut msg_out = Message::new(); + let mut relay_server = Config::get_option("relay-server"); + if relay_server.is_empty() { + relay_server = fla.relay_server; + } + msg_out.set_local_addr(LocalAddr { + socket_addr: AddrMangle::encode(peer_addr), + local_addr: AddrMangle::encode(local_addr), + relay_server, + ..Default::default() + }); + let bytes = msg_out.write_to_bytes()?; + socket.send_raw(bytes).await?; + crate::accept_connection(server.clone(), socket, peer_addr, false).await; + Ok(()) + } + + async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> { + let mut relay_server = Config::get_option("relay-server"); + if relay_server.is_empty() { + relay_server = ph.relay_server; + } + if ph.nat_type.enum_value_or_default() == NatType::SYMMETRIC + || Config::get_nat_type() == NatType::SYMMETRIC as i32 + { + let uuid = Uuid::new_v4().to_string(); + return self + .create_relay(ph.socket_addr, relay_server, uuid, server, true, true) + .await; + } + let peer_addr = AddrMangle::decode(&ph.socket_addr); + log::debug!("Punch hole to {:?}", peer_addr); + let mut socket = { + let socket = + FramedStream::new(self.addr, Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT) + .await?; + allow_err!(FramedStream::new(peer_addr, socket.get_ref().local_addr()?, 300).await); + socket + }; + let mut msg_out = Message::new(); + use hbb_common::protobuf::ProtobufEnum; + let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT); + msg_out.set_punch_hole_sent(PunchHoleSent { + socket_addr: ph.socket_addr, + id: Config::get_id(), + relay_server, + nat_type: nat_type.into(), + ..Default::default() + }); + let bytes = msg_out.write_to_bytes()?; + socket.send_raw(bytes).await?; + crate::accept_connection(server.clone(), socket, peer_addr, true).await; + Ok(()) + } + + async fn register_pk(&mut self, socket: &mut FramedSocket) -> ResultType<()> { + let mut msg_out = Message::new(); + let pk = Config::get_key_pair().1; + let uuid = if let Ok(id) = machine_uid::get() { + log::info!("machine uid: {}", id); + id.into() + } else { + pk.clone() + }; + let id = Config::get_id(); + self.last_id_pk_registery = id.clone(); + msg_out.set_register_pk(RegisterPk { + id, + uuid, + pk, + ..Default::default() + }); + socket.send(&msg_out, self.addr).await?; + Ok(()) + } + + async fn handle_uuid_mismatch(&mut self, socket: &mut FramedSocket) -> ResultType<()> { + if self.last_id_pk_registery != Config::get_id() { + return Ok(()); + } + { + let mut solving = SOLVING_PK_MISMATCH.lock().unwrap(); + if solving.is_empty() || *solving == self.host { + log::info!("UUID_MISMATCH received from {}", self.host); + Config::set_key_confirmed(false); + Config::update_id(); + *solving = self.host.clone(); + } else { + return Ok(()); + } + } + self.register_pk(socket).await + } + + async fn register_peer(&mut self, socket: &mut FramedSocket) -> ResultType<()> { + if !SOLVING_PK_MISMATCH.lock().unwrap().is_empty() { + return Ok(()); + } + if !Config::get_key_confirmed() || !Config::get_host_key_confirmed(&self.host_prefix) { + log::info!( + "register_pk of {} due to key not confirmed", + self.host_prefix + ); + return self.register_pk(socket).await; + } + let id = Config::get_id(); + log::trace!( + "Register my id {:?} to rendezvous server {:?}", + id, + self.addr, + ); + let mut msg_out = Message::new(); + let serial = Config::get_serial(); + msg_out.set_register_peer(RegisterPeer { + id, + serial, + ..Default::default() + }); + socket.send(&msg_out, self.addr).await?; + Ok(()) + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 00000000000..bf9745f85e6 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,335 @@ +use crate::ipc::Data; +use connection::{ConnInner, Connection}; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + bail, + config::{Config, CONNECT_TIMEOUT, RELAY_PORT}, + log, + message_proto::*, + protobuf::{Message as _, ProtobufEnum}, + rendezvous_proto::*, + sleep, + sodiumoxide::crypto::{box_, secretbox, sign}, + tcp::FramedStream, + timeout, tokio, ResultType, Stream, +}; +use service::{GenericService, Service, ServiceTmpl, Subscriber}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, +}; + +mod audio_service; +mod clipboard_service; +mod connection; +pub mod input_service; +mod service; +mod video_service; + +use hbb_common::tcp::new_listener; + +pub type Childs = Arc>>; +type ConnMap = HashMap; + +lazy_static::lazy_static! { + pub static ref CHILD_PROCESS: Childs = Default::default(); +} + +pub struct Server { + connections: ConnMap, + services: HashMap<&'static str, Box>, + id_count: i32, +} + +pub type ServerPtr = Arc>; +pub type ServerPtrWeak = Weak>; + +pub fn new() -> ServerPtr { + let mut server = Server { + connections: HashMap::new(), + services: HashMap::new(), + id_count: 0, + }; + server.add_service(Box::new(audio_service::new())); + server.add_service(Box::new(video_service::new())); + server.add_service(Box::new(clipboard_service::new())); + server.add_service(Box::new(input_service::new_cursor())); + server.add_service(Box::new(input_service::new_pos())); + Arc::new(RwLock::new(server)) +} + +async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> { + let local_addr = socket.get_ref().local_addr()?; + drop(socket); + // even we drop socket, below still may fail if not use reuse_addr, + // there is TIME_WAIT before socket really released, so sometimes we + // see “Only one usage of each socket address is normally permitted” on windows sometimes, + let mut listener = new_listener(local_addr, true).await?; + log::info!("Server listening on: {}", &listener.local_addr()?); + if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { + create_tcp_connection_(server, Stream::from(stream), addr, secure).await?; + } + Ok(()) +} + +async fn create_tcp_connection_( + server: ServerPtr, + stream: Stream, + addr: SocketAddr, + secure: bool, +) -> ResultType<()> { + let mut stream = stream; + let id = { + let mut w = server.write().unwrap(); + w.id_count += 1; + w.id_count + }; + let (sk, pk) = Config::get_key_pair(); + if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES { + let mut sk_ = [0u8; sign::SECRETKEYBYTES]; + sk_[..].copy_from_slice(&sk); + let sk = sign::SecretKey(sk_); + let mut msg_out = Message::new(); + let signed_id = sign::sign(Config::get_id().as_bytes(), &sk); + let (our_pk_b, our_sk_b) = box_::gen_keypair(); + msg_out.set_signed_id(SignedId { + id: signed_id, + pk: our_pk_b.0.into(), + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; + match timeout(CONNECT_TIMEOUT, stream.next()).await? { + Some(res) => { + let bytes = res?; + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if let Some(message::Union::public_key(pk)) = msg_in.union { + if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES { + let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); + let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; + pk_[..].copy_from_slice(&pk.asymmetric_value); + let their_pk_b = box_::PublicKey(pk_); + let symmetric_key = + box_::open(&pk.symmetric_value, &nonce, &their_pk_b, &our_sk_b) + .map_err(|_| { + anyhow!("Handshake failed: box decryption failure") + })?; + if symmetric_key.len() != secretbox::KEYBYTES { + bail!("Handshake failed: invalid secret key length from peer"); + } + let mut key = [0u8; secretbox::KEYBYTES]; + key[..].copy_from_slice(&symmetric_key); + stream.set_key(secretbox::Key(key)); + } else if !pk.asymmetric_value.is_empty() { + bail!("Handshake failed: invalid public sign key length from peer"); + } + } else { + log::error!("Handshake failed: invalid message type"); + } + } else { + bail!("Handshake failed: invalid message format"); + } + } + None => { + bail!("Failed to receive public key"); + } + } + } + + Connection::start(addr, stream, id, Arc::downgrade(&server)).await; + Ok(()) +} + +pub async fn accept_connection( + server: ServerPtr, + socket: Stream, + peer_addr: SocketAddr, + secure: bool, +) { + if let Err(err) = accept_connection_(server, socket, secure).await { + log::error!("Failed to accept connection from {}: {}", peer_addr, err); + } +} + +pub async fn create_relay_connection( + server: ServerPtr, + relay_server: String, + uuid: String, + peer_addr: SocketAddr, + secure: bool, +) { + if let Err(err) = + create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await + { + log::error!( + "Failed to create relay connection for {} with uuid {}: {}", + peer_addr, + uuid, + err + ); + } +} + +async fn create_relay_connection_( + server: ServerPtr, + relay_server: String, + uuid: String, + peer_addr: SocketAddr, + secure: bool, +) -> ResultType<()> { + let mut stream = FramedStream::new( + &crate::check_port(relay_server, RELAY_PORT), + Config::get_any_listen_addr(), + CONNECT_TIMEOUT, + ) + .await?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_request_relay(RequestRelay { + uuid, + ..Default::default() + }); + stream.send(&msg_out).await?; + create_tcp_connection_(server, stream, peer_addr, secure).await?; + Ok(()) +} + +impl Server { + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { + for s in self.services.values() { + if !noperms.contains(&s.name()) { + s.on_subscribe(conn.clone()); + } + } + self.connections.insert(conn.id(), conn); + } + + pub fn remove_connection(&mut self, conn: &ConnInner) { + for s in self.services.values() { + s.on_unsubscribe(conn.id()); + } + self.connections.remove(&conn.id()); + } + + fn add_service(&mut self, service: Box) { + let name = service.name(); + self.services.insert(name, service); + } + + pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) { + if let Some(s) = self.services.get(&name) { + if s.is_subed(conn.id()) == sub { + return; + } + if sub { + s.on_subscribe(conn.clone()); + } else { + s.on_unsubscribe(conn.id()); + } + } + } +} + +impl Drop for Server { + fn drop(&mut self) { + for s in self.services.values() { + s.join(); + } + } +} + +pub fn check_zombie() { + std::thread::spawn(|| loop { + let mut lock = CHILD_PROCESS.lock().unwrap(); + let mut i = 0; + while i != lock.len() { + let c = &mut (*lock)[i]; + if let Ok(Some(_)) = c.try_wait() { + lock.remove(i); + } else { + i += 1; + } + } + drop(lock); + std::thread::sleep(std::time::Duration::from_millis(100)); + }); +} + +#[tokio::main] +pub async fn start_server(is_server: bool, _tray: bool) { + #[cfg(target_os = "linux")] + { + log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); + log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); + } + if is_server { + std::thread::spawn(move || { + if let Err(err) = crate::ipc::start("") { + log::error!("Failed to start ipc: {}", err); + std::process::exit(-1); + } + }); + /* + tray is buggy, and not work on win10 2004, also cause crash, disable it + #[cfg(windows)] + if _tray { + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + if !crate::platform::is_prelogin() { + let mut res = Ok(None); + // while switching from prelogin to user screen, run_as_user may fails, + // so we try more times + for _ in 0..10 { + res = crate::platform::run_as_user("--tray"); + if res.is_ok() { + break; + } + std::thread::sleep(std::time::Duration::from_secs(1)); + } + allow_err!(res); + break; + } + }); + } + */ + crate::RendezvousMediator::start_all().await; + } else { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + allow_err!(conn.send(&Data::SystemInfo(None)).await); + if let Ok(Some(data)) = conn.next_timeout(1000).await { + log::info!("server info: {:?}", data); + } + // sync key pair + let mut n = 0; + loop { + if Config::get_key_confirmed() { + // check ipc::get_id(), key_confirmed may change, so give some chance to correct + n += 1; + if n > 3 { + break; + } else { + sleep(1.).await; + } + } else { + allow_err!(conn.send(&Data::ConfirmedKey(None)).await); + if let Ok(Some(Data::ConfirmedKey(Some(pair)))) = + conn.next_timeout(1000).await + { + Config::set_key_pair(pair); + Config::set_key_confirmed(true); + log::info!("key pair synced"); + break; + } else { + sleep(1.).await; + } + } + } + } + Err(err) => { + log::info!("server not started (will try to start): {}", err); + std::thread::spawn(|| start_server(true, false)); + } + } + } +} diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs new file mode 100644 index 00000000000..d903eefe26b --- /dev/null +++ b/src/server/audio_service.rs @@ -0,0 +1,350 @@ +// both soundio and cpal use wasapi on windows and coreaudio on mac, they do not support loopback. +// libpulseaudio support loopback because pulseaudio is a standalone audio service with some +// configuration, but need to install the library and start the service on OS, not a good choice. +// windows: https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording +// mac: https://github.com/mattingalls/Soundflower +// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient +// https://github.com/ExistentialAudio/BlackHole + +// if pactl not work, please run +// sudo apt-get --purge --reinstall install pulseaudio +// https://askubuntu.com/questions/403416/how-to-listen-live-sounds-from-input-from-external-sound-card +// https://wiki.debian.org/audio-loopback +// https://github.com/krruzic/pulsectl + +use super::*; +use magnum_opus::{Application::*, Channels::*, Encoder}; + +pub const NAME: &'static str = "audio"; + +#[cfg(not(target_os = "linux"))] +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.repeat::(33, cpal_impl::run); + sp +} + +#[cfg(target_os = "linux")] +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run(pa_impl::run); + sp +} + +#[cfg(target_os = "linux")] +mod pa_impl { + use super::*; + #[tokio::main(basic_scheduler)] + pub async fn run(sp: GenericService) -> ResultType<()> { + if let Ok(mut stream) = crate::ipc::connect(1000, "_pa").await { + let mut encoder = + Encoder::new(crate::platform::linux::PA_SAMPLE_RATE, Stereo, LowDelay)?; + allow_err!( + stream + .send(&crate::ipc::Data::Config(( + "audio-input".to_owned(), + Some(Config::get_option("audio-input")) + ))) + .await + ); + while sp.ok() { + sp.snapshot(|sps| { + sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2)); + Ok(()) + })?; + if let Some(data) = stream.next_timeout2(1000).await { + match data? { + Some(crate::ipc::Data::RawMessage(bytes)) => { + let data = unsafe { + std::slice::from_raw_parts::( + bytes.as_ptr() as _, + bytes.len() / 4, + ) + }; + send_f32(data, &mut encoder, &sp); + } + _ => {} + } + } + } + } + Ok(()) + } +} + +#[cfg(not(target_os = "linux"))] +mod cpal_impl { + use super::*; + use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, SupportedStreamConfig, + }; + + lazy_static::lazy_static! { + static ref HOST: Host = cpal::default_host(); + } + + #[derive(Default)] + pub struct State { + stream: Option<(Box, Arc)>, + } + + impl super::service::Reset for State { + fn reset(&mut self) { + self.stream.take(); + } + } + + pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> { + sp.snapshot(|sps| { + match &state.stream { + None => { + state.stream = Some(play(&sp)?); + } + _ => {} + } + if let Some((_, format)) = &state.stream { + sps.send_shared(format.clone()); + } + Ok(()) + })?; + Ok(()) + } + + fn send( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, + encoder: &mut Encoder, + sp: &GenericService, + ) { + if data.iter().filter(|x| **x != 0.).next().is_none() { + return; + } + let buffer; + let data = if sample_rate0 != sample_rate { + buffer = crate::common::resample_channels(data, sample_rate0, sample_rate, channels); + &buffer + } else { + data + }; + send_f32(data, encoder, sp); + } + + #[cfg(windows)] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = Config::get_option("audio-input"); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + let device = HOST + .default_output_device() + .with_context(|| "Failed to get default output device for loopback")?; + log::info!( + "Default output device: {}", + device.name().unwrap_or("".to_owned()) + ); + let format = device + .default_output_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get default output format")?; + log::info!("Default output format: {:?}", format); + Ok((device, format)) + } + + #[cfg(not(windows))] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = Config::get_option("audio-input"); + get_audio_input(&audio_input) + } + + fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> { + if audio_input == "Mute" { + bail!("Mute"); + } + let mut device = None; + if !audio_input.is_empty() { + for d in HOST + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + if device.is_none() { + device = Some( + HOST.default_input_device() + .with_context(|| "Failed to get default input device for loopback")?, + ); + } + let device = device.unwrap(); + log::info!("Input device: {}", device.name().unwrap_or("".to_owned())); + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get default input format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + + fn play(sp: &GenericService) -> ResultType<(Box, Arc)> { + let (device, config) = get_device()?; + let sp = sp.clone(); + let err_fn = move |err| { + log::error!("an error occurred on stream: {}", err); + }; + // Sample rate must be one of 8000, 12000, 16000, 24000, or 48000. + let sample_rate_0 = config.sample_rate().0; + let sample_rate = if sample_rate_0 < 12000 { + 8000 + } else if sample_rate_0 < 16000 { + 12000 + } else if sample_rate_0 < 24000 { + 16000 + } else if sample_rate_0 < 48000 { + 24000 + } else { + 48000 + }; + let mut encoder = Encoder::new( + sample_rate, + if config.channels() > 1 { Stereo } else { Mono }, + LowDelay, + )?; + let channels = config.channels(); + let stream = match config.sample_format() { + cpal::SampleFormat::F32 => device.build_input_stream( + &config.into(), + move |data, _: &_| { + send( + data, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + }, + err_fn, + )?, + cpal::SampleFormat::I16 => device.build_input_stream( + &config.into(), + move |data: &[i16], _: &_| { + let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect(); + send( + &buffer, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + }, + err_fn, + )?, + cpal::SampleFormat::U16 => device.build_input_stream( + &config.into(), + move |data: &[u16], _: &_| { + let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect(); + send( + &buffer, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + }, + err_fn, + )?, + }; + stream.play()?; + Ok(( + Box::new(stream), + Arc::new(create_format_msg(sample_rate, channels)), + )) + } +} + +fn create_format_msg(sample_rate: u32, channels: u16) -> Message { + let format = AudioFormat { + sample_rate, + channels: channels as _, + ..Default::default() + }; + let mut misc = Misc::new(); + misc.set_audio_format(format); + let mut msg = Message::new(); + msg.set_misc(misc); + msg +} + +fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { + if data.iter().filter(|x| **x != 0.).next().is_some() { + match encoder.encode_vec_float(data, data.len() * 6) { + Ok(data) => { + let mut msg_out = Message::new(); + msg_out.set_audio_frame(AudioFrame { + data, + ..Default::default() + }); + sp.send(msg_out); + } + Err(_) => {} + } + } +} + +#[cfg(test)] +mod tests { + #[cfg(target_os = "linux")] + #[test] + fn test_pulse() { + use libpulse_binding as pulse; + use libpulse_simple_binding as psimple; + let spec = pulse::sample::Spec { + format: pulse::sample::SAMPLE_FLOAT32NE, + channels: 2, + rate: 24000, + }; + let hspec = hound::WavSpec { + channels: spec.channels as _, + sample_rate: spec.rate as _, + bits_per_sample: (4 * 8) as _, + sample_format: hound::SampleFormat::Float, + }; + const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav"); + let mut writer = + hound::WavWriter::create(PATH, hspec).expect("Could not create hsound writer"); + let device = crate::platform::linux::get_pa_monitor(); + let s = psimple::Simple::new( + None, // Use the default server + "Test", // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + "Test", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) + .expect("Could not create simple pulse"); + let mut out: Vec = Vec::with_capacity(1024); + unsafe { + out.set_len(out.capacity()); + } + for _ in 0..600 { + s.read(&mut out).expect("Could not read pcm"); + let out2 = + unsafe { std::slice::from_raw_parts::(out.as_ptr() as _, out.len() / 4) }; + for v in out2 { + writer.write_sample(*v).ok(); + } + } + println!("{:?} {}", device, out.len()); + writer.finalize().expect("Could not finalize writer"); + } +} diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs new file mode 100644 index 00000000000..5393d1f7db1 --- /dev/null +++ b/src/server/clipboard_service.rs @@ -0,0 +1,53 @@ +use super::*; +pub use crate::common::{ + check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, + CONTENT, +}; + +struct State { + ctx: Option, + initialized: bool, +} + +impl Default for State { + fn default() -> Self { + let ctx = match ClipboardContext::new() { + Ok(ctx) => Some(ctx), + Err(err) => { + log::error!("Failed to start {}: {}", NAME, err); + None + } + }; + Self { + ctx, + initialized: false, + } + } +} + +impl super::service::Reset for State { + fn reset(&mut self) { + *CONTENT.lock().unwrap() = Default::default(); + self.initialized = false; + } +} + +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, false); + sp.repeat::(INTERVAL, run); + sp +} + +fn run(sp: GenericService, state: &mut State) -> ResultType<()> { + if let Some(ctx) = state.ctx.as_mut() { + if let Some(msg) = check_clipboard(ctx, None) { + if !state.initialized { + state.initialized = true; + // ignore clipboard update before service start + return Ok(()); + } + sp.send(msg); + } + } + Ok(()) +} diff --git a/src/server/connection.rs b/src/server/connection.rs new file mode 100644 index 00000000000..e40d5bab369 --- /dev/null +++ b/src/server/connection.rs @@ -0,0 +1,979 @@ +use super::input_service::*; +use super::*; +use crate::common::update_clipboard; +use crate::ipc; +use hbb_common::{ + config::Config, + fs, + futures::SinkExt, + sleep, timeout, + tokio::{ + net::TcpStream, + stream::StreamExt, + sync::mpsc, + time::{self, Duration, Instant, Interval}, + }, + tokio_util::codec::{BytesCodec, Framed}, +}; +use sha2::{Digest, Sha256}; + +pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; + +lazy_static::lazy_static! { + static ref CLICK_TIME: Arc::> = Default::default(); + static ref LOGIN_FAILURES: Arc::>> = Default::default(); +} + +#[derive(Clone, Default)] +pub struct ConnInner { + id: i32, + tx: Option, +} + +pub struct Connection { + inner: ConnInner, + stream: super::Stream, + server: super::ServerPtrWeak, + hash: Hash, + read_jobs: Vec, + timer: Interval, + file_transfer: Option<(String, bool)>, + port_forward_socket: Option>, + port_forward_address: String, + tx_to_cm: mpsc::UnboundedSender, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + last_test_delay: i64, + image_quality: i32, + lock_after_session_end: bool, + show_remote_cursor: bool, // by peer + privacy_mode: bool, + ip: String, + disable_clipboard: bool, // by peer + disable_audio: bool, // by peer +} + +impl Subscriber for ConnInner { + #[inline] + fn id(&self) -> i32 { + self.id + } + + #[inline] + fn send(&mut self, msg: Arc) { + self.tx.as_mut().map(|tx| { + allow_err!(tx.send((Instant::now(), msg))); + }); + } +} + +const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(3); +const SEC30: Duration = Duration::from_secs(30); +const H1: Duration = Duration::from_secs(3600); +const MILLI1: Duration = Duration::from_millis(1); + +impl Connection { + pub async fn start( + addr: SocketAddr, + stream: super::Stream, + id: i32, + server: super::ServerPtrWeak, + ) { + let hash = Hash { + salt: Config::get_salt(), + challenge: Config::get_auto_password(), + ..Default::default() + }; + let (tx_from_cm, mut rx_from_cm) = mpsc::unbounded_channel::(); + let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); + let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); + let mut conn = Self { + inner: ConnInner { id, tx: Some(tx) }, + stream, + server, + hash, + read_jobs: Vec::new(), + timer: time::interval(SEC30), + file_transfer: None, + port_forward_socket: None, + port_forward_address: "".to_owned(), + tx_to_cm, + authorized: false, + keyboard: Config::get_option("enable-keyboard").is_empty(), + clipboard: Config::get_option("enable-clipboard").is_empty(), + audio: Config::get_option("audio-input") != "Mute", + last_test_delay: 0, + image_quality: ImageQuality::Balanced.value(), + lock_after_session_end: false, + show_remote_cursor: false, + privacy_mode: false, + ip: "".to_owned(), + disable_audio: false, + disable_clipboard: false, + }; + tokio::spawn(async move { + if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await { + log::error!("ipc to connection manager exit: {}", err); + } + }); + if !conn.on_open(addr).await { + return; + } + if !conn.keyboard { + conn.send_permisssion(Permission::Keyboard, false).await; + } + if !conn.clipboard { + conn.send_permisssion(Permission::Clipboard, false).await; + } + if !conn.audio { + conn.send_permisssion(Permission::Audio, false).await; + } + let mut test_delay_timer = + time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT); + let mut last_recv_time = Instant::now(); + loop { + tokio::select! { + Some(data) = rx_from_cm.recv() => { + match data { + ipc::Data::Authorize => { + conn.send_logon_response().await; + if conn.port_forward_socket.is_some() { + break; + } + } + ipc::Data::Close => { + let mut misc = Misc::new(); + misc.set_close_reason("Closed manually by the peer".into()); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + conn.send(msg_out).await; + conn.on_close("Close requested from connection manager", false); + break; + } + ipc::Data::ChatMessage{text} => { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + conn.send(msg_out).await; + } + ipc::Data::SwitchPermission{name, enabled} => { + log::info!("Change permission {} -> {}", name, enabled); + if &name == "keyboard" { + conn.keyboard = enabled; + conn.send_permisssion(Permission::Keyboard, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + conn.inner.clone(), enabled || conn.show_remote_cursor); + } + } else if &name == "clipboard" { + conn.clipboard = enabled; + conn.send_permisssion(Permission::Clipboard, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.clipboard_enabled() && conn.keyboard); + } + } else if &name == "audio" { + conn.audio = enabled; + conn.send_permisssion(Permission::Audio, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } + } + ipc::Data::RawMessage(bytes) => { + allow_err!(conn.stream.send_raw(bytes).await); + } + _ => {} + } + } + Some((instant, value)) = rx.recv() => { + let latency = instant.elapsed().as_millis() as i64; + super::video_service::update_internal_latency(id, latency); + let msg: &Message = &value; + if latency > 1000 { + match &msg.union { + Some(message::Union::video_frame(_)) => { + continue; + } + Some(message::Union::audio_frame(_)) => { + continue; + } + _ => {} + } + } + if let Err(err) = conn.stream.send(msg).await { + conn.on_close(&err.to_string(), false); + break; + } + }, + res = conn.stream.next() => { + if let Some(res) = res { + match res { + Err(err) => { + conn.on_close(&err.to_string(), true); + break; + }, + Ok(bytes) => { + last_recv_time = Instant::now(); + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if !conn.on_message(msg_in).await { + break; + } + } + } + } + } else { + conn.on_close("Reset by the peer", true); + break; + } + }, + _ = conn.timer.tick() => { + if !conn.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { + conn.on_close(&err.to_string(), false); + break; + } + } else { + conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + _ = test_delay_timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + conn.on_close("Timeout", true); + break; + } + let time = crate::get_time(); + if time > 0 && conn.last_test_delay == 0 { + conn.last_test_delay = time; + let mut msg_out = Message::new(); + msg_out.set_test_delay(TestDelay{ + time, + ..Default::default() + }); + conn.inner.send(msg_out.into()); + } + } + } + } + super::video_service::update_internal_latency(id, 0); + super::video_service::update_test_latency(id, 0); + super::video_service::update_image_quality(id, None); + if let Some(forward) = conn.port_forward_socket.as_mut() { + log::info!("Running port forwarding loop"); + conn.stream.set_raw(); + loop { + tokio::select! { + Some(data) = rx_from_cm.recv() => { + match data { + ipc::Data::Close => { + conn.on_close("Close requested from connection manager", false); + break; + } + _ => {} + } + } + res = forward.next() => { + if let Some(res) = res { + match res { + Err(err) => { + conn.on_close(&err.to_string(), false); + break; + }, + Ok(bytes) => { + last_recv_time = Instant::now(); + if let Err(err) = conn.stream.send_bytes(bytes.into()).await { + conn.on_close(&err.to_string(), false); + break; + } + } + } + } else { + conn.on_close("Forward reset by the peer", false); + break; + } + }, + res = conn.stream.next() => { + if let Some(res) = res { + match res { + Err(err) => { + conn.on_close(&err.to_string(), false); + break; + }, + Ok(bytes) => { + last_recv_time = Instant::now(); + if let Err(err) = forward.send(bytes.into()).await { + conn.on_close(&err.to_string(), false); + break; + } + } + } + } else { + conn.on_close("Stream reset by the peer", false); + break; + } + }, + _ = conn.timer.tick() => { + if last_recv_time.elapsed() >= H1 { + conn.on_close("Timeout", false); + break; + } + } + } + } + } + } + + async fn send_permisssion(&mut self, permission: Permission, enabled: bool) { + let mut misc = Misc::new(); + misc.set_permission_info(PermissionInfo { + permission: permission.into(), + enabled, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + + async fn on_open(&mut self, addr: SocketAddr) -> bool { + log::debug!("#{} Connection opened from {}.", self.inner.id, addr); + let whitelist: Vec = Config::get_option("whitelist") + .split(",") + .filter(|x| !x.is_empty()) + .map(|x| x.to_owned()) + .collect(); + if !whitelist.is_empty() + && whitelist + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() + && whitelist + .iter() + .filter(|x| x.parse() == Ok(addr.ip())) + .next() + .is_none() + { + self.send_login_error("Your ip is blocked by the peer") + .await; + sleep(1.).await; + return false; + } + self.ip = addr.ip().to_string(); + let mut msg_out = Message::new(); + msg_out.set_hash(self.hash.clone()); + self.send(msg_out).await; + true + } + + async fn send_logon_response(&mut self) { + if self.authorized { + return; + } + #[allow(unused_mut)] + let mut username = crate::platform::get_active_username(); + let mut res = LoginResponse::new(); + if self.port_forward_socket.is_some() { + let mut msg_out = Message::new(); + res.set_peer_info(PeerInfo { + hostname: whoami::hostname(), + username, + platform: whoami::platform().to_string(), + version: crate::VERSION.to_owned(), + ..Default::default() + }); + msg_out.set_login_response(res); + self.send(msg_out).await; + return; + } + #[cfg(target_os = "linux")] + if !self.file_transfer.is_some() { + let dtype = crate::platform::linux::get_display_server(); + if dtype != "x11" { + res.set_error(format!( + "Unsupported display server type {}, x11 expected", + dtype + )); + let mut msg_out = Message::new(); + msg_out.set_login_response(res); + self.send(msg_out).await; + return; + } + } + #[allow(unused_mut)] + let mut sas_enabled = false; + #[cfg(windows)] + if crate::platform::is_root() { + sas_enabled = true; + } + if self.file_transfer.is_some() { + if crate::platform::is_prelogin() || self.tx_to_cm.send(ipc::Data::Test).is_err() { + username = "".to_owned(); + } + } + self.authorized = true; + let mut pi = PeerInfo { + hostname: whoami::hostname(), + username, + platform: whoami::platform().to_string(), + version: crate::VERSION.to_owned(), + sas_enabled, + ..Default::default() + }; + let mut sub_service = false; + if self.file_transfer.is_some() { + res.set_peer_info(pi); + } else { + try_activate_screen(); + match super::video_service::get_displays() { + Err(err) => { + res.set_error(err.to_string()); + } + Ok((current, displays)) => { + pi.displays = displays.into(); + pi.current_display = current as _; + res.set_peer_info(pi); + sub_service = true; + } + } + } + let mut msg_out = Message::new(); + msg_out.set_login_response(res); + self.send(msg_out).await; + if let Some((dir, show_hidden)) = self.file_transfer.clone() { + let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { + &dir + } else { + "" + }; + self.read_dir(dir, show_hidden); + } else if sub_service { + if let Some(s) = self.server.upgrade() { + let mut noperms = Vec::new(); + if !self.keyboard && !self.show_remote_cursor { + noperms.push(NAME_CURSOR); + } + if !self.show_remote_cursor { + noperms.push(NAME_POS); + } + if !self.clipboard_enabled() || !self.keyboard { + noperms.push(super::clipboard_service::NAME); + } + if !self.audio_enabled() { + noperms.push(super::audio_service::NAME); + } + s.write() + .unwrap() + .add_connection(self.inner.clone(), &noperms); + } + } + } + + fn clipboard_enabled(&self) -> bool { + self.clipboard && !self.disable_clipboard + } + + fn audio_enabled(&self) -> bool { + self.audio && !self.disable_audio + } + + async fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { + self.send_to_cm(ipc::Data::Login { + id: self.inner.id(), + is_file_transfer: self.file_transfer.is_some(), + port_forward: self.port_forward_address.clone(), + peer_id, + name, + authorized, + keyboard: self.keyboard, + clipboard: self.clipboard, + audio: self.audio, + }); + } + + #[inline] + fn send_to_cm(&mut self, data: ipc::Data) { + self.tx_to_cm.send(data).ok(); + } + + #[inline] + fn send_fs(&mut self, data: ipc::FS) { + self.send_to_cm(ipc::Data::FS(data)); + } + + async fn send_login_error(&mut self, err: T) { + let mut msg_out = Message::new(); + let mut res = LoginResponse::new(); + res.set_error(err.to_string()); + msg_out.set_login_response(res); + self.send(msg_out).await; + } + + async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::login_request(lr)) = msg.union { + if let Some(o) = lr.option.as_ref() { + self.update_option(o); + } + if self.authorized { + return true; + } + match lr.union { + Some(login_request::Union::file_transfer(ft)) => { + if !Config::get_option("enable-file-transfer").is_empty() { + self.send_login_error("No permission of file transfer") + .await; + sleep(1.).await; + return false; + } + self.file_transfer = Some((ft.dir, ft.show_hidden)); + } + Some(login_request::Union::port_forward(mut pf)) => { + if !Config::get_option("enable-tunnel").is_empty() { + self.send_login_error("No permission of IP tunneling").await; + sleep(1.).await; + return false; + } + let mut is_rdp = false; + if pf.host == "RDP" && pf.port == 0 { + pf.host = "localhost".to_owned(); + pf.port = 3389; + is_rdp = true; + } + if pf.host.is_empty() { + pf.host = "localhost".to_owned(); + } + let mut addr = format!("{}:{}", pf.host, pf.port); + self.port_forward_address = addr.clone(); + match timeout(3000, TcpStream::connect(&addr)).await { + Ok(Ok(sock)) => { + self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new())); + } + _ => { + if is_rdp { + addr = "RDP".to_owned(); + } + self.send_login_error(format!( + "Failed to access remote {}, please make sure if it is open", + addr + )) + .await; + } + } + } + _ => {} + } + if lr.username != Config::get_id() { + self.send_login_error("Offline").await; + } else if lr.password.is_empty() { + self.try_start_cm(lr.my_id, lr.my_name, false).await; + } else { + let mut hasher = Sha256::new(); + hasher.update(&Config::get_password()); + hasher.update(&self.hash.salt); + let mut hasher2 = Sha256::new(); + hasher2.update(&hasher.finalize()[..]); + hasher2.update(&self.hash.challenge); + let mut failure = LOGIN_FAILURES + .lock() + .unwrap() + .get(&self.ip) + .map(|x| x.clone()) + .unwrap_or((0, 0, 0)); + let time = (crate::get_time() / 60_000) as i32; + if failure.2 > 30 { + self.send_login_error("Too many wrong password attempts") + .await; + } else if time == failure.0 && failure.1 > 6 { + self.send_login_error("Please try 1 minute later").await; + } else if hasher2.finalize()[..] != lr.password[..] { + if failure.0 == time { + failure.1 += 1; + failure.2 += 1; + } else { + failure.0 = time; + failure.1 = 1; + failure.2 += 1; + } + LOGIN_FAILURES + .lock() + .unwrap() + .insert(self.ip.clone(), failure); + self.send_login_error("Wrong Password").await; + self.try_start_cm(lr.my_id, lr.my_name, false).await; + } else { + if failure.0 != 0 { + LOGIN_FAILURES.lock().unwrap().remove(&self.ip); + } + self.send_logon_response().await; + self.try_start_cm(lr.my_id, lr.my_name, true).await; + if self.port_forward_socket.is_some() { + return false; + } + } + } + } else if let Some(message::Union::test_delay(t)) = msg.union { + if t.from_client { + let mut msg_out = Message::new(); + msg_out.set_test_delay(t); + self.inner.send(msg_out.into()); + } else { + self.last_test_delay = 0; + let latency = crate::get_time() - t.time; + if latency > 0 { + super::video_service::update_test_latency(self.inner.id(), latency); + } + } + } else if self.authorized { + match msg.union { + Some(message::Union::mouse_event(me)) => { + if self.keyboard { + handle_mouse(&me, self.inner.id()); + if is_left_up(&me) { + *CLICK_TIME.lock().unwrap() = crate::get_time(); + } + } + } + Some(message::Union::key_event(mut me)) => { + if self.keyboard { + if me.press { + if let Some(key_event::Union::unicode(_)) = me.union { + handle_key(&me); + } else if let Some(key_event::Union::seq(_)) = me.union { + handle_key(&me); + } else { + me.down = true; + handle_key(&me); + me.down = false; + handle_key(&me); + } + } else { + handle_key(&me); + } + if is_enter(&me) { + *CLICK_TIME.lock().unwrap() = crate::get_time(); + } + } + } + Some(message::Union::clipboard(cb)) => { + if self.clipboard { + update_clipboard(cb, None); + } + } + Some(message::Union::file_action(fa)) => { + if self.file_transfer.is_some() { + match fa.union { + Some(file_action::Union::read_dir(rd)) => { + self.read_dir(&rd.path, rd.include_hidden); + } + Some(file_action::Union::all_files(f)) => { + match fs::get_recursive_files(&f.path, f.include_hidden) { + Err(err) => { + self.send(fs::new_error(f.id, err, -1)).await; + } + Ok(files) => { + self.send(fs::new_dir(f.id, files)).await; + } + } + } + Some(file_action::Union::send(s)) => { + let id = s.id; + match fs::TransferJob::new_read(id, s.path, s.include_hidden) { + Err(err) => { + self.send(fs::new_error(id, err, 0)).await; + } + Ok(job) => { + self.send(fs::new_dir(id, job.files().to_vec())).await; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + Some(file_action::Union::receive(r)) => { + self.send_fs(ipc::FS::NewWrite { + path: r.path, + id: r.id, + files: r + .files + .to_vec() + .drain(..) + .map(|f| (f.name, f.modified_time)) + .collect(), + }); + } + Some(file_action::Union::remove_dir(d)) => { + self.send_fs(ipc::FS::RemoveDir { + path: d.path, + id: d.id, + recursive: d.recursive, + }); + } + Some(file_action::Union::remove_file(f)) => { + self.send_fs(ipc::FS::RemoveFile { + path: f.path, + id: f.id, + file_num: f.file_num, + }); + } + Some(file_action::Union::create(c)) => { + self.send_fs(ipc::FS::CreateDir { + path: c.path, + id: c.id, + }); + } + Some(file_action::Union::cancel(c)) => { + self.send_fs(ipc::FS::CancelWrite { id: c.id }); + fs::remove_job(c.id, &mut self.read_jobs); + } + _ => {} + } + } + } + Some(message::Union::file_response(fr)) => match fr.union { + Some(file_response::Union::block(block)) => { + self.send_fs(ipc::FS::WriteBlock { + id: block.id, + file_num: block.file_num, + data: block.data, + compressed: block.compressed, + }); + } + Some(file_response::Union::done(d)) => { + self.send_fs(ipc::FS::WriteDone { + id: d.id, + file_num: d.file_num, + }); + } + _ => {} + }, + Some(message::Union::misc(misc)) => match misc.union { + Some(misc::Union::switch_display(s)) => { + super::video_service::switch_display(s.display); + } + Some(misc::Union::chat_message(c)) => { + self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); + } + Some(misc::Union::option(o)) => { + self.update_option(&o); + } + Some(misc::Union::refresh_video(r)) => { + if r { + super::video_service::refresh(); + } + } + _ => {} + }, + _ => {} + } + } + true + } + + fn update_option(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); + if let Ok(q) = o.image_quality.enum_value() { + self.image_quality = q.value(); + super::video_service::update_image_quality(self.inner.id(), Some(q.value())); + } + let q = o.custom_image_quality; + if q > 0 { + self.image_quality = q; + super::video_service::update_image_quality(self.inner.id(), Some(q)); + } + if let Ok(q) = o.lock_after_session_end.enum_value() { + if q != BoolOption::NotSet { + self.lock_after_session_end = q == BoolOption::Yes; + } + } + if let Ok(q) = o.show_remote_cursor.enum_value() { + if q != BoolOption::NotSet { + self.show_remote_cursor = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.keyboard || self.show_remote_cursor, + ); + s.write().unwrap().subscribe( + NAME_POS, + self.inner.clone(), + self.show_remote_cursor, + ); + } + } + } + if let Ok(q) = o.disable_audio.enum_value() { + if q != BoolOption::NotSet { + self.disable_audio = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.audio_enabled(), + ); + } + } + } + if let Ok(q) = o.disable_clipboard.enum_value() { + if q != BoolOption::NotSet { + self.disable_clipboard = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.clipboard_enabled() && self.keyboard, + ); + } + } + } + if let Ok(q) = o.privacy_mode.enum_value() { + if q != BoolOption::NotSet { + self.privacy_mode = q == BoolOption::Yes; + if self.privacy_mode && self.keyboard { + crate::platform::toggle_privacy_mode(true); + } + } + } + if self.keyboard { + if let Ok(q) = o.block_input.enum_value() { + if q != BoolOption::NotSet { + crate::platform::block_input(q == BoolOption::Yes); + } + } + } + } + + fn on_close(&mut self, reason: &str, lock: bool) { + if let Some(s) = self.server.upgrade() { + s.write().unwrap().remove_connection(&self.inner); + } + log::info!("#{} Connection closed: {}", self.inner.id(), reason); + if lock && self.lock_after_session_end && self.keyboard { + crate::platform::lock_screen(); + super::video_service::switch_to_primary(); + } + if self.privacy_mode { + crate::platform::toggle_privacy_mode(false); + } + self.port_forward_socket.take(); + } + + fn read_dir(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadDir { + dir, + include_hidden, + }); + } + + #[inline] + async fn send(&mut self, msg: Message) { + allow_err!(self.stream.send(&msg).await); + } +} + +async fn start_ipc( + mut rx_to_cm: mpsc::UnboundedReceiver, + tx_from_cm: mpsc::UnboundedSender, +) -> ResultType<()> { + loop { + if !crate::platform::is_prelogin() { + break; + } + sleep(1.).await; + } + let mut stream = None; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + } else { + let run_done; + if crate::platform::is_root() { + let mut res = Ok(None); + for _ in 0..10 { + res = crate::platform::run_as_user("--cm"); + if res.is_ok() { + break; + } + sleep(1.).await; + } + if let Some(task) = res? { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + super::CHILD_PROCESS + .lock() + .unwrap() + .push(crate::run_me(vec!["--cm"])?); + } + for _ in 0..10 { + sleep(0.3).await; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + break; + } + } + if stream.is_none() { + bail!("Failed to connect to connection manager"); + } + } + let mut stream = stream.unwrap(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + return Err(err.into()); + } + Ok(Some(data)) => { + match data { + ipc::Data::ClickTime(_)=> { + unsafe { + let ct = *CLICK_TIME.lock().unwrap(); + let data = ipc::Data::ClickTime(ct); + stream.send(&data).await?; + } + } + _ => { + tx_from_cm.send(data)?; + } + } + } + _ => {} + } + } + res = rx_to_cm.recv() => { + match res { + Some(data) => { + stream.send(&data).await?; + } + None => { + bail!("expected"); + } + } + } + } + } +} + +// in case screen is sleep and blank, here to activate it +fn try_activate_screen() { + #[cfg(windows)] + std::thread::spawn(|| { + mouse_move_relative(-6, -6); + std::thread::sleep(std::time::Duration::from_millis(30)); + mouse_move_relative(6, 6); + }); +} diff --git a/src/server/input_service.rs b/src/server/input_service.rs new file mode 100644 index 00000000000..fef52fd5961 --- /dev/null +++ b/src/server/input_service.rs @@ -0,0 +1,499 @@ +use super::*; +#[cfg(target_os = "macos")] +use dispatch::Queue; +use enigo::{Enigo, KeyboardControllable, MouseButton, MouseControllable}; +use hbb_common::config::COMPRESS_LEVEL; +use std::convert::TryFrom; + +#[derive(Default)] +struct StateCursor { + hcursor: u64, + cursor_data: Arc, + cached_cursor_data: HashMap>, +} + +impl super::service::Reset for StateCursor { + fn reset(&mut self) { + *self = Default::default(); + crate::platform::reset_input_cache(); + } +} + +#[derive(Default)] +struct StatePos { + cursor_pos: (i32, i32), +} + +impl super::service::Reset for StatePos { + fn reset(&mut self) { + self.cursor_pos = (0, 0); + } +} + +#[derive(Default)] +struct Input { + conn: i32, + time: i64, +} + +static mut LATEST_INPUT: Input = Input { conn: 0, time: 0 }; + +#[derive(Clone, Default)] +pub struct MouseCursorSub { + inner: ConnInner, + cached: HashMap>, +} + +impl From for MouseCursorSub { + fn from(inner: ConnInner) -> Self { + Self { + inner, + cached: HashMap::new(), + } + } +} + +impl Subscriber for MouseCursorSub { + #[inline] + fn id(&self) -> i32 { + self.inner.id() + } + + #[inline] + fn send(&mut self, msg: Arc) { + if let Some(message::Union::cursor_data(cd)) = &msg.union { + if let Some(msg) = self.cached.get(&cd.id) { + self.inner.send(msg.clone()); + } else { + self.inner.send(msg.clone()); + let mut tmp = Message::new(); + // only send id out, require client side cache also + tmp.set_cursor_id(cd.id); + self.cached.insert(cd.id, Arc::new(tmp)); + } + } else { + self.inner.send(msg); + } + } +} + +pub const NAME_CURSOR: &'static str = "mouse_cursor"; +pub const NAME_POS: &'static str = "mouse_pos"; +pub type MouseCursorService = ServiceTmpl; + +pub fn new_cursor() -> MouseCursorService { + let sp = MouseCursorService::new(NAME_CURSOR, true); + sp.repeat::(33, run_cursor); + sp +} + +pub fn new_pos() -> GenericService { + let sp = GenericService::new(NAME_POS, false); + sp.repeat::(33, run_pos); + sp +} + +fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { + if let Some((x, y)) = crate::get_cursor_pos() { + if state.cursor_pos.0 != x || state.cursor_pos.1 != y { + state.cursor_pos = (x, y); + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x, + y, + ..Default::default() + }); + let exclude = unsafe { + if crate::get_time() - LATEST_INPUT.time < 300 { + LATEST_INPUT.conn + } else { + 0 + } + }; + sp.send_without(msg_out, exclude); + } + } + + sp.snapshot(|sps| { + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x: state.cursor_pos.0, + y: state.cursor_pos.1, + ..Default::default() + }); + sps.send(msg_out); + Ok(()) + })?; + Ok(()) +} + +fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> { + if let Some(hcursor) = crate::get_cursor()? { + if hcursor != state.hcursor { + let msg; + if let Some(cached) = state.cached_cursor_data.get(&hcursor) { + super::log::trace!("Cursor data cached, hcursor: {}", hcursor); + msg = cached.clone(); + } else { + let mut data = crate::get_cursor_data(hcursor)?; + data.colors = hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL); + let mut tmp = Message::new(); + tmp.set_cursor_data(data); + msg = Arc::new(tmp); + state.cached_cursor_data.insert(hcursor, msg.clone()); + super::log::trace!("Cursor data updated, hcursor: {}", hcursor); + } + state.hcursor = hcursor; + sp.send_shared(msg.clone()); + state.cursor_data = msg; + } + } + sp.snapshot(|sps| { + sps.send_shared(state.cursor_data.clone()); + Ok(()) + })?; + Ok(()) +} + +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); +} + +// mac key input must be run in main thread, otherwise crash on >= osx 10.15 +#[cfg(target_os = "macos")] +lazy_static::lazy_static! { + static ref QUEUE: Queue = Queue::main(); + static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); +} + +pub fn is_left_up(evt: &MouseEvent) -> bool { + let buttons = evt.mask >> 3; + let evt_type = evt.mask & 0x7; + return buttons == 1 && evt_type == 2; +} + +#[cfg(windows)] +pub fn mouse_move_relative(x: i32, y: i32) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let mut en = ENIGO.lock().unwrap(); + en.mouse_move_relative(x, y); +} + +#[cfg(not(target_os = "macos"))] +fn modifier_sleep() { + // sleep for a while, this is only for keying in rdp in peer so far + #[cfg(windows)] + std::thread::sleep(std::time::Duration::from_nanos(1)); +} + +pub fn handle_mouse(evt: &MouseEvent, conn: i32) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let buttons = evt.mask >> 3; + let evt_type = evt.mask & 0x7; + if evt_type == 0 { + unsafe { + let time = crate::get_time(); + LATEST_INPUT = Input { time, conn }; + } + } + let mut en = ENIGO.lock().unwrap(); + #[cfg(not(target_os = "macos"))] + let mut to_release = Vec::new(); + #[cfg(target_os = "macos")] + en.reset_flag(); + for ref ck in evt.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + if evt_type == 1 || evt_type == 2 { + #[cfg(target_os = "macos")] + en.add_flag(key); + #[cfg(not(target_os = "macos"))] + if key != &enigo::Key::CapsLock && key != &enigo::Key::NumLock { + if !en.get_key_state(key.clone()) { + en.key_down(key.clone()).ok(); + modifier_sleep(); + to_release.push(key); + } + } + } + } + } + match evt_type { + 0 => { + en.mouse_move_to(evt.x, evt.y); + } + 1 => match buttons { + 1 => { + allow_err!(en.mouse_down(MouseButton::Left)); + } + 2 => { + allow_err!(en.mouse_down(MouseButton::Right)); + } + 4 => { + allow_err!(en.mouse_down(MouseButton::Middle)); + } + _ => {} + }, + 2 => match buttons { + 1 => { + en.mouse_up(MouseButton::Left); + } + 2 => { + en.mouse_up(MouseButton::Right); + } + 4 => { + en.mouse_up(MouseButton::Middle); + } + _ => {} + }, + 3 => { + #[allow(unused_mut)] + let mut x = evt.x; + #[allow(unused_mut)] + let mut y = evt.y; + #[cfg(not(windows))] + { + x = -x; + y = -y; + } + if x != 0 { + en.mouse_scroll_x(x); + } + if y != 0 { + en.mouse_scroll_y(y); + } + } + _ => {} + } + #[cfg(not(target_os = "macos"))] + for key in to_release { + en.key_up(key.clone()); + } +} + +pub fn is_enter(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value() + { + return true; + } + } + return false; +} + +lazy_static::lazy_static! { + static ref KEY_MAP: HashMap = + [ + (ControlKey::Alt, enigo::Key::Alt), + (ControlKey::Backspace, enigo::Key::Backspace), + (ControlKey::CapsLock, enigo::Key::CapsLock), + (ControlKey::Control, enigo::Key::Control), + (ControlKey::Delete, enigo::Key::Delete), + (ControlKey::DownArrow, enigo::Key::DownArrow), + (ControlKey::End, enigo::Key::End), + (ControlKey::Escape, enigo::Key::Escape), + (ControlKey::F1, enigo::Key::F1), + (ControlKey::F10, enigo::Key::F10), + (ControlKey::F11, enigo::Key::F11), + (ControlKey::F12, enigo::Key::F12), + (ControlKey::F2, enigo::Key::F2), + (ControlKey::F3, enigo::Key::F3), + (ControlKey::F4, enigo::Key::F4), + (ControlKey::F5, enigo::Key::F5), + (ControlKey::F6, enigo::Key::F6), + (ControlKey::F7, enigo::Key::F7), + (ControlKey::F8, enigo::Key::F8), + (ControlKey::F9, enigo::Key::F9), + (ControlKey::Home, enigo::Key::Home), + (ControlKey::LeftArrow, enigo::Key::LeftArrow), + (ControlKey::Meta, enigo::Key::Meta), + (ControlKey::Option, enigo::Key::Option), + (ControlKey::PageDown, enigo::Key::PageDown), + (ControlKey::PageUp, enigo::Key::PageUp), + (ControlKey::Return, enigo::Key::Return), + (ControlKey::RightArrow, enigo::Key::RightArrow), + (ControlKey::Shift, enigo::Key::Shift), + (ControlKey::Space, enigo::Key::Space), + (ControlKey::Tab, enigo::Key::Tab), + (ControlKey::UpArrow, enigo::Key::UpArrow), + (ControlKey::Numpad0, enigo::Key::Numpad0), + (ControlKey::Numpad1, enigo::Key::Numpad1), + (ControlKey::Numpad2, enigo::Key::Numpad2), + (ControlKey::Numpad3, enigo::Key::Numpad3), + (ControlKey::Numpad4, enigo::Key::Numpad4), + (ControlKey::Numpad5, enigo::Key::Numpad5), + (ControlKey::Numpad6, enigo::Key::Numpad6), + (ControlKey::Numpad7, enigo::Key::Numpad7), + (ControlKey::Numpad8, enigo::Key::Numpad8), + (ControlKey::Numpad9, enigo::Key::Numpad9), + (ControlKey::Cancel, enigo::Key::Cancel), + (ControlKey::Clear, enigo::Key::Clear), + (ControlKey::Menu, enigo::Key::Menu), + (ControlKey::Pause, enigo::Key::Pause), + (ControlKey::Kana, enigo::Key::Kana), + (ControlKey::Hangul, enigo::Key::Hangul), + (ControlKey::Junja, enigo::Key::Junja), + (ControlKey::Final, enigo::Key::Final), + (ControlKey::Hanja, enigo::Key::Hanja), + (ControlKey::Kanji, enigo::Key::Kanji), + (ControlKey::Convert, enigo::Key::Convert), + (ControlKey::Select, enigo::Key::Select), + (ControlKey::Print, enigo::Key::Print), + (ControlKey::Execute, enigo::Key::Execute), + (ControlKey::Snapshot, enigo::Key::Snapshot), + (ControlKey::Insert, enigo::Key::Insert), + (ControlKey::Help, enigo::Key::Help), + (ControlKey::Sleep, enigo::Key::Sleep), + (ControlKey::Separator, enigo::Key::Separator), + (ControlKey::Scroll, enigo::Key::Scroll), + (ControlKey::NumLock, enigo::Key::NumLock), + (ControlKey::RWin, enigo::Key::RWin), + (ControlKey::Apps, enigo::Key::Apps), + (ControlKey::Multiply, enigo::Key::Multiply), + (ControlKey::Add, enigo::Key::Add), + (ControlKey::Subtract, enigo::Key::Subtract), + (ControlKey::Decimal, enigo::Key::Decimal), + (ControlKey::Divide, enigo::Key::Divide), + (ControlKey::Equals, enigo::Key::Equals), + (ControlKey::NumpadEnter, enigo::Key::NumpadEnter), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); + static ref NUMPAD_KEY_MAP: HashMap = + [ + (ControlKey::Home, true), + (ControlKey::UpArrow, true), + (ControlKey::PageUp, true), + (ControlKey::LeftArrow, true), + (ControlKey::RightArrow, true), + (ControlKey::End, true), + (ControlKey::DownArrow, true), + (ControlKey::PageDown, true), + (ControlKey::Insert, true), + (ControlKey::Delete, true), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); +} + +pub fn handle_key(evt: &KeyEvent) { + #[cfg(target_os = "macos")] + if !*IS_SERVER { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_key_(&evt)); + return; + } + handle_key_(evt); +} + +fn handle_key_(evt: &KeyEvent) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let mut en = ENIGO.lock().unwrap(); + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + #[cfg(windows)] + let mut disable_numlock = false; + #[cfg(target_os = "macos")] + en.reset_flag(); + #[cfg(not(target_os = "macos"))] + let mut to_release = Vec::new(); + #[cfg(not(target_os = "macos"))] + let mut has_cap = false; + #[cfg(windows)] + let mut has_numlock = false; + for ref ck in evt.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + #[cfg(target_os = "macos")] + en.add_flag(key); + #[cfg(not(target_os = "macos"))] + { + if key == &enigo::Key::CapsLock { + has_cap = true; + } else if key == &enigo::Key::NumLock { + #[cfg(windows)] + { + has_numlock = true; + } + } else { + if !en.get_key_state(key.clone()) { + en.key_down(key.clone()).ok(); + modifier_sleep(); + to_release.push(key); + } + } + } + } + } + #[cfg(not(target_os = "macos"))] + if crate::common::valid_for_capslock(evt) { + if has_cap != en.get_key_state(enigo::Key::CapsLock) { + en.key_down(enigo::Key::CapsLock).ok(); + en.key_up(enigo::Key::CapsLock); + } + } + #[cfg(windows)] + if crate::common::valid_for_numlock(evt) { + if has_numlock != en.get_key_state(enigo::Key::NumLock) { + en.key_down(enigo::Key::NumLock).ok(); + en.key_up(enigo::Key::NumLock); + } + } + match evt.union { + Some(key_event::Union::control_key(ck)) => { + if let Some(key) = KEY_MAP.get(&ck.value()) { + #[cfg(windows)] + if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) { + disable_numlock = en.get_key_state(enigo::Key::NumLock); + if disable_numlock { + en.key_down(enigo::Key::NumLock).ok(); + en.key_up(enigo::Key::NumLock); + } + } + if evt.down { + allow_err!(en.key_down(key.clone())); + } else { + en.key_up(key.clone()); + } + } else if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + } else if ck.value() == ControlKey::LockScreen.value() { + crate::platform::lock_screen(); + super::video_service::switch_to_primary(); + } + } + Some(key_event::Union::chr(chr)) => { + if evt.down { + allow_err!(en.key_down(enigo::Key::Layout(chr as u8 as _))); + } else { + en.key_up(enigo::Key::Layout(chr as u8 as _)); + } + } + Some(key_event::Union::unicode(chr)) => { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } + } + Some(key_event::Union::seq(ref seq)) => { + en.key_sequence(&seq); + } + _ => {} + } + #[cfg(not(target_os = "macos"))] + for key in to_release { + en.key_up(key.clone()); + } + #[cfg(windows)] + if disable_numlock { + en.key_down(enigo::Key::NumLock).ok(); + en.key_up(enigo::Key::NumLock); + } +} + +#[tokio::main(basic_scheduler)] +async fn send_sas() -> ResultType<()> { + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; + Ok(()) +} diff --git a/src/server/service.rs b/src/server/service.rs new file mode 100644 index 00000000000..b60f5a8d459 --- /dev/null +++ b/src/server/service.rs @@ -0,0 +1,249 @@ +use super::*; +use std::{ + thread::{self, JoinHandle}, + time, +}; + +pub trait Service: Send + Sync { + fn name(&self) -> &'static str; + fn on_subscribe(&self, sub: ConnInner); + fn on_unsubscribe(&self, id: i32); + fn is_subed(&self, id: i32) -> bool; + fn join(&self); +} + +pub trait Subscriber: Default + Send + Sync + 'static { + fn id(&self) -> i32; + fn send(&mut self, msg: Arc); +} + +#[derive(Default)] +pub struct ServiceInner> { + name: &'static str, + handle: Option>, + subscribes: HashMap, + new_subscribes: HashMap, + active: bool, + need_snapshot: bool, +} + +pub trait Reset { + fn reset(&mut self); +} + +pub struct ServiceTmpl>(Arc>>); +pub struct ServiceSwap>(ServiceTmpl); +pub type GenericService = ServiceTmpl; +pub const HIBERATE_TIMEOUT: u64 = 30; +pub const MAX_ERROR_TIMEOUT: u64 = 1_000; + +impl> ServiceInner { + fn send_new_subscribes(&mut self, msg: Arc) { + for s in self.new_subscribes.values_mut() { + s.send(msg.clone()); + } + } + + fn swap_new_subscribes(&mut self) { + for (_, s) in self.new_subscribes.drain() { + self.subscribes.insert(s.id(), s); + } + assert!(self.new_subscribes.is_empty()); + } + + #[inline] + fn has_subscribes(&self) -> bool { + self.subscribes.len() > 0 || self.new_subscribes.len() > 0 + } +} + +impl> Service for ServiceTmpl { + #[inline] + fn name(&self) -> &'static str { + self.0.read().unwrap().name + } + + fn is_subed(&self, id: i32) -> bool { + self.0.read().unwrap().subscribes.get(&id).is_some() + } + + fn on_subscribe(&self, sub: ConnInner) { + let mut lock = self.0.write().unwrap(); + if lock.subscribes.get(&sub.id()).is_some() { + return; + } + if lock.need_snapshot { + lock.new_subscribes.insert(sub.id(), sub.into()); + } else { + lock.subscribes.insert(sub.id(), sub.into()); + } + } + + fn on_unsubscribe(&self, id: i32) { + let mut lock = self.0.write().unwrap(); + if let None = lock.subscribes.remove(&id) { + lock.new_subscribes.remove(&id); + } + } + + fn join(&self) { + self.0.write().unwrap().active = false; + self.0.write().unwrap().handle.take().map(JoinHandle::join); + } +} + +impl> Clone for ServiceTmpl { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl> ServiceTmpl { + pub fn new(name: &'static str, need_snapshot: bool) -> Self { + Self(Arc::new(RwLock::new(ServiceInner:: { + name, + active: true, + need_snapshot, + ..Default::default() + }))) + } + + #[inline] + pub fn has_subscribes(&self) -> bool { + self.0.read().unwrap().has_subscribes() + } + + #[inline] + pub fn ok(&self) -> bool { + let lock = self.0.read().unwrap(); + lock.active && lock.has_subscribes() + } + + pub fn snapshot(&self, callback: F) -> ResultType<()> + where + F: FnMut(ServiceSwap) -> ResultType<()>, + { + if self.0.read().unwrap().new_subscribes.len() > 0 { + log::info!("Call snapshot of {} service", self.name()); + let mut callback = callback; + callback(ServiceSwap::(self.clone()))?; + } + Ok(()) + } + + #[inline] + pub fn send(&self, msg: Message) { + self.send_shared(Arc::new(msg)); + } + + pub fn send_shared(&self, msg: Arc) { + let mut lock = self.0.write().unwrap(); + for s in lock.subscribes.values_mut() { + s.send(msg.clone()); + } + } + + pub fn send_without(&self, msg: Message, sub: i32) { + let mut lock = self.0.write().unwrap(); + let msg = Arc::new(msg); + for s in lock.subscribes.values_mut() { + if sub != s.id() { + s.send(msg.clone()); + } + } + } + + pub fn repeat(&self, interval_ms: u64, callback: F) + where + F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send, + S: 'static + Default + Reset, + { + let interval = time::Duration::from_millis(interval_ms); + let mut callback = callback; + let sp = self.clone(); + let thread = thread::spawn(move || { + let mut state = S::default(); + while sp.active() { + let now = time::Instant::now(); + if sp.has_subscribes() { + if let Err(err) = callback(sp.clone(), &mut state) { + log::error!("Error of {} service: {}", sp.name(), err); + thread::sleep(time::Duration::from_millis(MAX_ERROR_TIMEOUT)); + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + } + } else { + state.reset(); + } + let elapsed = now.elapsed(); + if elapsed < interval { + thread::sleep(interval - elapsed); + } + } + }); + self.0.write().unwrap().handle = Some(thread); + } + + pub fn run(&self, callback: F) + where + F: 'static + FnMut(Self) -> ResultType<()> + Send, + { + let sp = self.clone(); + let mut callback = callback; + let thread = thread::spawn(move || { + let mut error_timeout = HIBERATE_TIMEOUT; + while sp.active() { + if sp.has_subscribes() { + log::debug!("Enter {} service inner loop", sp.name()); + let tm = time::Instant::now(); + if let Err(err) = callback(sp.clone()) { + log::error!("Error of {} service: {}", sp.name(), err); + if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) { + error_timeout = HIBERATE_TIMEOUT; + } else { + error_timeout *= 2; + } + if error_timeout > MAX_ERROR_TIMEOUT { + error_timeout = MAX_ERROR_TIMEOUT; + } + thread::sleep(time::Duration::from_millis(error_timeout)); + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + } else { + log::debug!("Exit {} service inner loop", sp.name()); + } + } + thread::sleep(time::Duration::from_millis(HIBERATE_TIMEOUT)); + } + }); + self.0.write().unwrap().handle = Some(thread); + } + + #[inline] + pub fn active(&self) -> bool { + self.0.read().unwrap().active + } +} + +impl> ServiceSwap { + #[inline] + pub fn send(&self, msg: Message) { + self.send_shared(Arc::new(msg)); + } + + #[inline] + pub fn send_shared(&self, msg: Arc) { + (self.0).0.write().unwrap().send_new_subscribes(msg); + } + + #[inline] + pub fn has_subscribes(&self) -> bool { + (self.0).0.read().unwrap().subscribes.len() > 0 + } +} + +impl> Drop for ServiceSwap { + fn drop(&mut self) { + (self.0).0.write().unwrap().swap_new_subscribes(); + } +} diff --git a/src/server/video_service.rs b/src/server/video_service.rs new file mode 100644 index 00000000000..e3f943789f6 --- /dev/null +++ b/src/server/video_service.rs @@ -0,0 +1,384 @@ +// 24FPS (actually 23.976FPS) is what video professionals ages ago determined to be the +// slowest playback rate that still looks smooth enough to feel real. +// Our eyes can see a slight difference and even though 30FPS actually shows +// more information and is more realistic. +// 60FPS is commonly used in game, teamviewer 12 support this for video editing user. + +// how to capture with mouse cursor: +// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/desktop-dup-api?redirectedfrom=MSDN + +// 实现了硬件编解码和音频抓取,还绘制了鼠标 +// https://github.com/PHZ76/DesktopSharing + +// dxgi memory leak issue +// https://stackoverflow.com/questions/47801238/memory-leak-in-creating-direct2d-device +// but per my test, it is more related to AcquireNextFrame, +// https://forums.developer.nvidia.com/t/dxgi-outputduplication-memory-leak-when-using-nv-but-not-amd-drivers/108582 + +// to-do: +// https://slhck.info/video/2017/03/01/rate-control.html + +use super::*; +use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, VideoCodecId, STRIDE_ALIGN}; +use std::{ + io::ErrorKind::WouldBlock, + time::{self, Instant}, +}; + +const WAIT_BASE: i32 = 17; +pub const NAME: &'static str = "video"; + +lazy_static::lazy_static! { + static ref CURRENT_DISPLAY: Arc> = Arc::new(Mutex::new(usize::MAX)); + static ref LAST_ACTIVE: Arc> = Arc::new(Mutex::new(Instant::now())); + static ref SWITCH: Arc> = Default::default(); + static ref INTERNAL_LATENCIES: Arc>> = Default::default(); + static ref TEST_LATENCIES: Arc>> = Default::default(); + static ref IMAGE_QUALITIES: Arc>> = Default::default(); +} + +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run(run); + sp +} + +fn run(sp: GenericService) -> ResultType<()> { + let fps = 30; + let spf = time::Duration::from_secs_f32(1. / (fps as f32)); + let (n, current, display) = get_current_display()?; + let (origin, width, height) = (display.origin(), display.width(), display.height()); + log::debug!( + "#displays={}, current={}, origin: {:?}, width={}, height={}", + n, + current, + &origin, + width, + height + ); + // Capturer object is expensive, avoiding to create it frequently. + let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?; + + let q = get_image_quality(); + let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q); + log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer); + let mut wait = WAIT_BASE; + let cfg = Config { + width: width as _, + height: height as _, + timebase: [1, 1000], // Output timestamp precision + bitrate, + codec: VideoCodecId::VP9, + rc_min_quantizer, + rc_max_quantizer, + speed, + }; + let mut vpx = Encoder::new(&cfg, 1).with_context(|| "Failed to create encoder")?; + + if *SWITCH.lock().unwrap() { + log::debug!("Broadcasting display switch"); + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display: current as _, + x: origin.0 as _, + y: origin.1 as _, + width: width as _, + height: height as _, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + *SWITCH.lock().unwrap() = false; + sp.send(msg_out); + } + + #[cfg(windows)] + if !c.is_gdi() { + // dxgi duplicateoutput has no output if display no change, so we use gdi this as workaround + if c.set_gdi() { + // dgx capture release has memory leak somehow, so use gdi always before fixing it, just sacrificing some cpu + /* + if let Ok(frame) = c.frame(wait as _) { + handle_one_frame(&sp, &frame, 0, &mut vpx)?; + } + c.cancel_gdi(); + */ + } + } + + let start = time::Instant::now(); + let mut crc = (0, 0); + let mut last_sent = time::Instant::now(); + while sp.ok() { + if *SWITCH.lock().unwrap() { + bail!("SWITCH"); + } + if current != *CURRENT_DISPLAY.lock().unwrap() { + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + if get_image_quality() != q { + bail!("SWITCH"); + } + #[cfg(windows)] + { + if crate::platform::windows::desktop_changed() { + bail!("Desktop changed"); + } + } + let now = time::Instant::now(); + *LAST_ACTIVE.lock().unwrap() = now; + if get_latency() < 1000 || last_sent.elapsed().as_millis() > 1000 { + match c.frame(wait as _) { + Ok(frame) => { + let time = now - start; + let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; + handle_one_frame(&sp, &frame, ms, &mut crc, &mut vpx)?; + last_sent = now; + } + Err(ref e) if e.kind() == WouldBlock => { + // https://github.com/NVIDIA/video-sdk-samples/tree/master/nvEncDXGIOutputDuplicationSample + wait = WAIT_BASE - now.elapsed().as_millis() as i32; + if wait < 0 { + wait = 0 + } + continue; + } + Err(err) => { + return Err(err.into()); + } + } + } + let elapsed = now.elapsed(); + // may need to enable frame(timeout) + log::trace!("{:?} {:?}", time::Instant::now(), elapsed); + if elapsed < spf { + std::thread::sleep(spf - elapsed); + } + } + Ok(()) +} + +#[inline] +fn create_msg(vp9s: Vec) -> Message { + let mut msg_out = Message::new(); + let mut vf = VideoFrame::new(); + vf.set_vp9s(VP9s { + frames: vp9s.into(), + ..Default::default() + }); + msg_out.set_video_frame(vf); + msg_out +} + +#[inline] +fn create_frame(frame: &EncodeFrame) -> VP9 { + VP9 { + data: frame.data.to_vec(), + key: frame.key, + pts: frame.pts, + ..Default::default() + } +} + +#[inline] +fn handle_one_frame( + sp: &GenericService, + frame: &[u8], + ms: i64, + crc: &mut (u32, u32), + vpx: &mut Encoder, +) -> ResultType<()> { + sp.snapshot(|sps| { + // so that new sub and old sub share the same encoder after switch + if sps.has_subscribes() { + bail!("SWITCH"); + } + Ok(()) + })?; + let mut hasher = crc32fast::Hasher::new(); + hasher.update(frame); + let checksum = hasher.finalize(); + if checksum != crc.0 { + crc.0 = checksum; + crc.1 = 0; + } else { + crc.1 += 1; + } + if crc.1 <= 180 && crc.1 % 5 == 0 { + let mut frames = Vec::new(); + for ref frame in vpx + .encode(ms, frame, STRIDE_ALIGN) + .with_context(|| "Failed to encode")? + { + frames.push(create_frame(frame)); + } + for ref frame in vpx.flush().with_context(|| "Failed to flush")? { + frames.push(create_frame(frame)); + } + // to-do: flush periodically, e.g. 1 second + if frames.len() > 0 { + sp.send(create_msg(frames)); + } + } + Ok(()) +} + +pub fn get_displays() -> ResultType<(usize, Vec)> { + // switch to primary display if long time (30 seconds) no users + if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 { + *CURRENT_DISPLAY.lock().unwrap() = usize::MAX; + } + let mut displays = Vec::new(); + let mut primary = 0; + for (i, d) in Display::all()?.iter().enumerate() { + if d.is_primary() { + primary = i; + } + displays.push(DisplayInfo { + x: d.origin().0 as _, + y: d.origin().1 as _, + width: d.width() as _, + height: d.height() as _, + name: d.name(), + online: d.is_online(), + ..Default::default() + }); + } + let mut lock = CURRENT_DISPLAY.lock().unwrap(); + if *lock >= displays.len() { + *lock = primary + } + Ok((*lock, displays)) +} + +pub fn switch_display(i: i32) { + let i = i as usize; + if let Ok((_, displays)) = get_displays() { + if i < displays.len() { + *CURRENT_DISPLAY.lock().unwrap() = i; + } + } +} + +pub fn refresh() { + *SWITCH.lock().unwrap() = true; +} + +fn get_primary() -> usize { + if let Ok(all) = Display::all() { + for (i, d) in all.iter().enumerate() { + if d.is_primary() { + return i; + } + } + } + 0 +} + +pub fn switch_to_primary() { + switch_display(get_primary() as _); +} + +fn get_current_display() -> ResultType<(usize, usize, Display)> { + let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize; + let mut displays = Display::all()?; + if displays.len() == 0 { + bail!("No displays"); + } + let n = displays.len(); + if current >= n { + current = 0; + for (i, d) in displays.iter().enumerate() { + if d.is_primary() { + current = i; + break; + } + } + *CURRENT_DISPLAY.lock().unwrap() = current; + } + return Ok((n, current, displays.remove(current))); +} + +#[inline] +fn update_latency(id: i32, latency: i64, latencies: &mut HashMap) { + if latency <= 0 { + latencies.remove(&id); + } else { + latencies.insert(id, latency); + } +} + +pub fn update_test_latency(id: i32, latency: i64) { + update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap()); +} + +pub fn update_internal_latency(id: i32, latency: i64) { + update_latency(id, latency, &mut *INTERNAL_LATENCIES.lock().unwrap()); +} + +pub fn get_latency() -> i64 { + INTERNAL_LATENCIES + .lock() + .unwrap() + .values() + .max() + .unwrap_or(&0) + .clone() +} + +fn convert_quality(q: i32) -> i32 { + let q = { + if q == ImageQuality::Balanced.value() { + (100 * 2 / 3, 12) + } else if q == ImageQuality::Low.value() { + (100 / 2, 18) + } else if q == ImageQuality::Best.value() { + (100, 12) + } else { + let bitrate = q >> 8 & 0xFF; + let quantizer = q & 0xFF; + (bitrate * 2, (100 - quantizer) * 36 / 100) + } + }; + if q.0 <= 0 { + 0 + } else { + q.0 << 8 | q.1 + } +} + +pub fn update_image_quality(id: i32, q: Option) { + match q { + Some(q) => { + let q = convert_quality(q); + if q > 0 { + IMAGE_QUALITIES.lock().unwrap().insert(id, q); + } else { + IMAGE_QUALITIES.lock().unwrap().remove(&id); + } + } + None => { + IMAGE_QUALITIES.lock().unwrap().remove(&id); + } + } +} + +fn get_image_quality() -> i32 { + IMAGE_QUALITIES + .lock() + .unwrap() + .values() + .min() + .unwrap_or(&convert_quality(ImageQuality::Balanced.value())) + .clone() +} + +#[inline] +fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) { + // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/ + let bitrate = q >> 8 & 0xFF; + let quantizer = q & 0xFF; + let b = ((w * h) / 1000) as u32; + (bitrate as u32 * b / 100, quantizer as _, 56, 7) +} diff --git a/src/tray-icon.ico b/src/tray-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4df961d5d91237b10de4a3e1b0e982b5573281be GIT binary patch literal 4286 zcmc(jJ7^U_6oy9vHZ~T)`Zhv9u(7qUu@khlvP)xg1tB07;wu5j=6*Yd{?e@IOSzFT00Jp9;jO?dHrG~H!5c--UL zj;;Fs(>%^o=hk!1i}(3{k*PtEXQLuF4i%gk<9fO&a&>?1K30;n$M3uQBI|3iX>TVA ztcRyJhc^EB*cQ2dFqcPs{=`X<)t_YyuHZvV9-rHKZ|%3p%$2y$)L5cv%--BB{CAu4 z>Q3(*#!r;>4tacfU5@|Uqxjs}k$ev1)XHLrSo}?FHSes3ml{P(>?(5R=HWaJaJELg z&+e}F(S&|ZD!oT<;y$la9n&u@jRL>6A8t+T#u4u6?T^JDk_`9s<| zi~n-0w|$Unq5b-nR(BR3y+hM}ZRgo*o*mZcg?3)IYx);?8`{&0=|0H0(1=}1?@>-R zhWN_qfwl%&Pd=w`;CX3lKQ;H1KR90(KrP$+llVIZJpzrb?s&L;G+le;fL@(^F-Cu- z$1xXNYK=VYWcr*|zd7f7r?zUT80wcfF+;CjdS}Rycy`(w&wS?DdQ~p;+yBK@9=!kA zcMR?47N5MytyjgVI$t|b-^rT%6s0wTUouW%)QEg)qSvc q^!w}YH{i$)XfSNPfzbgy>dc4Y``c@-*lp+fF7PjcZ8k959{UfehX0lT literal 0 HcmV?d00001 diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 00000000000..729b62b91a0 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,647 @@ +mod cm; +#[cfg(feature = "inline")] +mod inline; +#[cfg(target_os = "macos")] +mod macos; +mod remote; +use crate::common::SOFTWARE_UPDATE_URL; +use crate::ipc; +use hbb_common::{ + allow_err, + config::{Config, PeerConfig, APP_NAME, ICON}, + log, sleep, + tokio::{self, time}, +}; +use sciter::Value; +use std::{ + collections::HashMap, + iter::FromIterator, + process::Child, + sync::{Arc, Mutex}, +}; + +pub type Childs = Arc)>>; + +#[derive(Default)] +struct UI( + Childs, + Arc>, + Arc>>, +); + +fn get_msgbox() -> String { + #[cfg(feature = "inline")] + return inline::get_msgbox(); + #[cfg(not(feature = "inline"))] + return "".to_owned(); +} + +pub fn start(args: &mut [String]) { + #[cfg(windows)] + if args.len() > 0 && args[0] == "--tray" { + let mut res; + // while switching from prelogin to user screen, start_tray may fails, + // so we try more times + loop { + res = start_tray(); + if res.is_ok() { + log::info!("tray started with username {}", crate::username()); + break; + } + std::thread::sleep(std::time::Duration::from_secs(1)); + } + allow_err!(res); + return; + } + use sciter::SCRIPT_RUNTIME_FEATURES::*; + allow_err!(sciter::set_options(sciter::RuntimeOptions::ScriptFeatures( + ALLOW_FILE_IO as u8 | ALLOW_SOCKET_IO as u8 | ALLOW_EVAL as u8 | ALLOW_SYSINFO as u8 + ))); + let mut frame = sciter::WindowBuilder::main_window().create(); + #[cfg(windows)] + allow_err!(sciter::set_options(sciter::RuntimeOptions::UxTheming(true))); + frame.set_title(APP_NAME); + #[cfg(target_os = "macos")] + macos::make_menubar(); + let page; + if args.len() > 1 && args[0] == "--play" { + args[0] = "--connect".to_owned(); + let path: std::path::PathBuf = (&args[1]).into(); + let id = path + .file_stem() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + args[1] = id; + } + if args.is_empty() { + let childs: Childs = Default::default(); + let cloned = childs.clone(); + std::thread::spawn(move || check_zombie(cloned)); + crate::common::check_software_update(); + frame.event_handler(UI::new(childs)); + page = "index.html"; + } else if args[0] == "--install" { + let childs: Childs = Default::default(); + frame.event_handler(UI::new(childs)); + page = "install.html"; + } else if args[0] == "--cm" { + frame.register_behavior("connection-manager", move || { + Box::new(cm::ConnectionManager::new()) + }); + page = "cm.html"; + } else if (args[0] == "--connect" + || args[0] == "--file-transfer" + || args[0] == "--port-forward" + || args[0] == "--rdp") + && args.len() > 1 + { + let mut iter = args.iter(); + let cmd = iter.next().unwrap().clone(); + let id = iter.next().unwrap().clone(); + let args: Vec = iter.map(|x| x.clone()).collect(); + frame.set_title(&id); + frame.register_behavior("native-remote", move || { + Box::new(remote::Handler::new(cmd.clone(), id.clone(), args.clone())) + }); + page = "remote.html"; + } else { + log::error!("Wrong command: {:?}", args); + return; + } + #[cfg(feature = "inline")] + { + let html = if page == "index.html" { + inline::get_index() + } else if page == "cm.html" { + inline::get_cm() + } else if page == "install.html" { + inline::get_install() + } else { + inline::get_remote() + }; + frame.load_html(html.as_bytes(), Some(page)); + } + #[cfg(not(feature = "inline"))] + frame.load_file(&format!( + "file://{}/src/ui/{}", + std::env::current_dir() + .map(|c| c.display().to_string()) + .unwrap_or("".to_owned()), + page + )); + frame.run_app(); +} + +#[cfg(windows)] +fn start_tray() -> hbb_common::ResultType<()> { + let mut app = systray::Application::new()?; + let icon = include_bytes!("./tray-icon.ico"); + app.set_icon_from_buffer(icon, 32, 32).unwrap(); + app.add_menu_item("Open Window", |_| { + crate::run_me(Vec::<&str>::new()).ok(); + Ok::<_, systray::Error>(()) + })?; + let options = check_connect_status(false).1; + let idx_stopped = Arc::new(Mutex::new((0, 0))); + app.set_timer(std::time::Duration::from_millis(1000), move |app| { + let stopped = if let Some(v) = options.lock().unwrap().get("stop-service") { + !v.is_empty() + } else { + false + }; + let stopped = if stopped { 2 } else { 1 }; + let mut old = *idx_stopped.lock().unwrap(); + if stopped != old.1 { + if old.0 > 0 { + app.remove_menu_item(old.0) + } + if stopped == 1 { + old.0 = app.add_menu_item("Stop Service", |_| { + ipc::set_option("stop-service", "Y"); + Ok::<_, systray::Error>(()) + })?; + } else { + old.0 = app.add_menu_item("Start Service", |_| { + ipc::set_option("stop-service", ""); + Ok::<_, systray::Error>(()) + })?; + } + old.1 = stopped; + *idx_stopped.lock().unwrap() = old; + } + Ok::<_, systray::Error>(()) + })?; + allow_err!(app.wait_for_message()); + Ok(()) +} + +impl UI { + fn new(childs: Childs) -> Self { + let res = check_connect_status(true); + Self(childs, res.0, res.1) + } + + fn recent_sessions_updated(&mut self) -> bool { + let mut lock = self.0.lock().unwrap(); + if lock.0 { + lock.0 = false; + true + } else { + false + } + } + + fn get_id(&mut self) -> String { + ipc::get_id() + } + + fn get_password(&mut self) -> String { + ipc::get_password() + } + + fn update_password(&mut self, password: String) { + if password.is_empty() { + allow_err!(ipc::set_password(Config::get_auto_password())); + } else { + allow_err!(ipc::set_password(password)); + } + } + + fn get_remote_id(&mut self) -> String { + Config::get_remote_id() + } + + fn set_remote_id(&mut self, id: String) { + Config::set_remote_id(&id); + } + + fn get_msgbox(&mut self) -> String { + get_msgbox() + } + + fn goto_install(&mut self) { + allow_err!(crate::run_me(vec!["--install"])); + } + + fn install_me(&mut self, _options: String) { + #[cfg(windows)] + std::thread::spawn(move || { + allow_err!(crate::platform::windows::install_me(&_options)); + std::process::exit(0); + }); + } + + fn update_me(&self, _path: String) { + #[cfg(target_os = "linux")] + { + std::process::Command::new("pkexec") + .args(&["apt", "install", "-f", &_path]) + .spawn() + .ok(); + std::fs::remove_file(&_path).ok(); + crate::run_me(Vec::<&str>::new()).ok(); + } + #[cfg(windows)] + { + let mut path = _path; + if path.is_empty() { + if let Ok(tmp) = std::env::current_exe() { + path = tmp.to_string_lossy().to_string(); + } + } + std::process::Command::new(path) + .arg("--update") + .spawn() + .ok(); + std::process::exit(0); + } + } + + fn get_option(&self, key: String) -> String { + if let Some(v) = self.2.lock().unwrap().get(&key) { + v.to_owned() + } else { + "".to_owned() + } + } + + fn get_options(&self) -> Value { + let mut m = Value::map(); + for (k, v) in self.2.lock().unwrap().iter() { + m.set_item(k, v); + } + m + } + + fn test_if_valid_server(&self, host: String) -> String { + crate::common::test_if_valid_server(host) + } + + fn get_sound_inputs(&self) -> Value { + let mut a = Value::array(0); + #[cfg(windows)] + { + let inputs = Arc::new(Mutex::new(Vec::new())); + let cloned = inputs.clone(); + // can not call below in UI thread, because conflict with sciter sound com initialization + std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs()) + .join() + .ok(); + for name in inputs.lock().unwrap().drain(..) { + a.push(name); + } + } + #[cfg(not(windows))] + for name in get_sound_inputs() { + a.push(name); + } + a + } + + fn set_options(&self, v: Value) { + let mut m = HashMap::new(); + for (k, v) in v.items() { + if let Some(k) = k.as_string() { + if let Some(v) = v.as_string() { + if !v.is_empty() { + m.insert(k, v); + } + } + } + } + ipc::set_options(m).ok(); + } + + fn install_path(&mut self) -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_info().1; + #[cfg(not(windows))] + return "".to_owned(); + } + + fn is_installed(&mut self) -> bool { + crate::platform::is_installed() + } + + fn is_installed_lower_version(&self) -> bool { + #[cfg(not(windows))] + return false; + #[cfg(windows)] + { + let installed_version = crate::platform::windows::get_installed_version(); + let a = crate::common::get_version_number(crate::VERSION); + let b = crate::common::get_version_number(&installed_version); + return a > b; + } + } + + fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { + Config::set_size(x, y, w, h); + } + + fn get_size(&mut self) -> Value { + let s = Config::get_size(); + let mut v = Value::array(0); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + v + } + + fn get_connect_status(&mut self) -> Value { + let mut v = Value::array(0); + let x = *self.1.lock().unwrap(); + v.push(x.0); + v.push(x.1); + v + } + + fn get_recent_sessions(&mut self) -> Value { + let peers: Vec = PeerConfig::peers() + .iter() + .map(|p| { + let values = vec![ + p.0.clone(), + p.2.username.clone(), + p.2.hostname.clone(), + p.2.platform.clone(), + ]; + Value::from_iter(values) + }) + .collect(); + Value::from_iter(peers) + } + + fn get_icon(&mut self) -> String { + ICON.to_owned() + } + + fn remove_peer(&mut self, id: String) { + PeerConfig::remove(&id); + } + + fn new_remote(&mut self, id: String, remote_type: String) { + let mut lock = self.0.lock().unwrap(); + let args = vec![format!("--{}", remote_type), id.clone()]; + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } + } + + fn is_process_trusted(&mut self, _prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_process_trusted(_prompt); + #[cfg(not(target_os = "macos"))] + return true; + } + + fn is_can_screen_recording(&mut self, _prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_screen_recording(_prompt); + #[cfg(not(target_os = "macos"))] + return true; + } + + fn get_error(&mut self) -> String { + #[cfg(target_os = "linux")] + { + let dtype = crate::platform::linux::get_display_server(); + if dtype != "x11" { + return format!("Unsupported display server type {}, x11 expected!", dtype); + } + } + return "".to_owned(); + } + + fn is_login_wayland(&mut self) -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::is_login_wayland(); + #[cfg(not(target_os = "linux"))] + return false; + } + + fn fix_login_wayland(&mut self) { + #[cfg(target_os = "linux")] + return crate::platform::linux::fix_login_wayland(); + } + + fn get_software_update_url(&self) -> String { + SOFTWARE_UPDATE_URL.lock().unwrap().clone() + } + + fn get_new_version(&self) -> String { + hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) + } + + fn get_version(&self) -> String { + crate::VERSION.to_owned() + } + + fn get_app_name(&self) -> String { + APP_NAME.to_owned() + } + + fn get_software_ext(&self) -> String { + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() + } + + fn get_software_store_path(&self) -> String { + let mut p = std::env::temp_dir(); + let name = SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(APP_NAME.to_owned()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) + } + + fn open_url(&self, url: String) { + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = "xdg-open"; + allow_err!(std::process::Command::new(p).arg(url).spawn()); + } +} + +impl sciter::EventHandler for UI { + sciter::dispatch_script_call! { + fn get_id(); + fn get_password(); + fn update_password(String); + fn get_remote_id(); + fn set_remote_id(String); + fn save_size(i32, i32, i32, i32); + fn get_size(); + fn new_remote(String, bool); + fn remove_peer(String); + fn get_connect_status(); + fn get_recent_sessions(); + fn recent_sessions_updated(); + fn get_icon(); + fn get_msgbox(); + fn install_me(String); + fn is_installed(); + fn is_installed_lower_version(); + fn install_path(); + fn goto_install(); + fn is_process_trusted(bool); + fn is_can_screen_recording(bool); + fn get_error(); + fn is_login_wayland(); + fn fix_login_wayland(); + fn get_options(); + fn get_option(String); + fn test_if_valid_server(String); + fn get_sound_inputs(); + fn set_options(Value); + fn get_software_update_url(); + fn get_new_version(); + fn get_version(); + fn update_me(String); + fn get_app_name(); + fn get_software_store_path(); + fn get_software_ext(); + fn open_url(String); + } +} + +pub fn check_zombie(childs: Childs) { + let mut deads = Vec::new(); + loop { + let mut lock = childs.lock().unwrap(); + let mut n = 0; + for (id, c) in lock.1.iter_mut() { + if let Ok(Some(_)) = c.try_wait() { + deads.push(id.clone()); + n += 1; + } + } + for ref id in deads.drain(..) { + lock.1.remove(id); + } + if n > 0 { + lock.0 = true; + } + drop(lock); + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +// notice: avoiding create ipc connecton repeatly, +// because windows named pipe has serious memory leak issue. +#[tokio::main(basic_scheduler)] +async fn check_connect_status_( + reconnect: bool, + status: Arc>, + options: Arc>>, +) { + let mut key_confirmed = false; + loop { + if let Ok(mut c) = ipc::connect(1000, "").await { + let mut timer = time::interval(time::Duration::from_secs(1)); + loop { + tokio::select! { + res = c.next() => { + match res { + Err(err) => { + log::error!("ipc connection closed: {}", err); + break; + } + Ok(Some(ipc::Data::Options(Some(v)))) => { + *options.lock().unwrap() = v + } + Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { + if x > 0 { + x = 1 + } + key_confirmed = c; + *status.lock().unwrap() = (x as _, key_confirmed); + } + _ => {} + } + } + _ = timer.tick() => { + c.send(&ipc::Data::OnlineStatus(None)).await.ok(); + c.send(&ipc::Data::Options(None)).await.ok(); + } + } + } + } + if !reconnect { + std::process::exit(0); + } + *status.lock().unwrap() = (-1, key_confirmed); + sleep(1.).await; + } +} + +#[cfg(not(target_os = "linux"))] +fn get_sound_inputs() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out +} + +#[cfg(target_os = "linux")] +fn get_sound_inputs() -> Vec { + crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect() +} + +fn check_connect_status( + reconnect: bool, +) -> (Arc>, Arc>>) { + let status = Arc::new(Mutex::new((0, false))); + let options = Arc::new(Mutex::new(HashMap::new())); + let cloned = status.clone(); + let cloned_options = options.clone(); + std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options)); + (status, options) +} diff --git a/src/ui/chatbox.html b/src/ui/chatbox.html new file mode 100644 index 00000000000..86a335458d0 --- /dev/null +++ b/src/ui/chatbox.html @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/src/ui/cm.css b/src/ui/cm.css new file mode 100644 index 00000000000..afb2455c0be --- /dev/null +++ b/src/ui/cm.css @@ -0,0 +1,218 @@ +body { + behavior: connection-manager; +} + +div.content { + flow: horizontal; + size: *; +} + +div.left-panel { + size: *; + padding: 1em; + border-spacing: 1em; + overflow-x: scroll-indicator; + position: relative; +} + +div.chaticon svg { + size: 24px; + margin: 4px; +} + +div.chaticon { + position: absolute; + right: 0; + top: 0; + size: 32px; +} + +div.chaticon.active { + opacity: 0.5; +} + +div.chaticon:active { + background: white; +} + +div.right-panel { + background: white; + border-left: color(border) 1px solid; + size: *; +} + +div.icon-and-id { + flow: horizontal; + border-spacing: 1em; +} + +div.icon { + size: 96px; + text-align: center; + font-size: 96px; + line-height: 96px; + color: white; + font-weight: bold; +} + +div.id { + color: color(green-blue); +} + +div.permissions { + flow: horizontal; + border-spacing: 0.5em; +} + +div.permissions > div { + size: 48px; + background: color(accent); +} + +div.permissions icon { + margin: *; + size: 32px; + background-size: cover; + background-repeat: no-repeat; + display: block; +} + +div.permissions > div.disabled { + background: #ddd; +} + +div.permissions > div:active { + opacity: 0.5; +} + +icon.keyboard { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII='); +} + +icon.clipboard { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='); +} + +icon.audio { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='); +} + +div.buttons { + width: *; + border-spacing: 0.5em; + text-align: center; +} + +div.buttons button { + width: 80px; + height: 40px; + margin: 0.5em; +} + +button#disconnect { + width: 160px; + background: color(blood-red); + border: none; +} + +button#disconnect:active { + opacity: 0.5; +} + +@media platform != "OSX" { +header .window-toolbar { + left: 40px; + top: 8px; +} +} + +@media platform == "OSX" { +header .tabs-wrapper { + margin-left: 80px; + margin-top: 8px; +} +} + +div.tabs-wrapper { + size: *; + position: relative; + overflow: hidden; +} + +div.tabs { + size: *; + flow: horizontal; + white-space: nowrap; + overflow: hidden; +} + +header { + height: 32px; + border-bottom: none; +} + +div.border-bottom { + position: absolute; + bottom: 0; + left: 0; + width: *; + height: 1px; + background: color(border) 1px solid; +} + +header div.window-icon { + size: 32px; +} + +div.tabs > div { + display: inline-block; + height: 24px; + line-height: 24px; +} + +div.tab { + width: 70px; + @ELLIPSIS; + text-align: center; + position: relative; + padding: 0 5px; +} + +div.active-tab { + background: color(gray-bg); + border: color(border) 1px solid; + border-bottom: none; + font-weight: bold; +} + +span.unreaded { + position: absolute; + font-size: 11px; + size: 15px; + border-radius: 15px; + line-height: 15px; + background: color(blood-red); + display: inline-block; + color: white; +} + +div.left-panel { + background: color(gray-bg); +} + +button.window#minimize { + right: 0px!important; +} + +div.tab-arrows { + position: absolute; + right: 2px; + font-weight: bold; +} + +div.tab-arrows span { + display: inline-block; + height: *; + margin: 0; + padding: 6px 2px; +} \ No newline at end of file diff --git a/src/ui/cm.html b/src/ui/cm.html new file mode 100644 index 00000000000..4edb4a76211 --- /dev/null +++ b/src/ui/cm.html @@ -0,0 +1,21 @@ + + + + + +

+ + + + + +
+ + + \ No newline at end of file diff --git a/src/ui/cm.rs b/src/ui/cm.rs new file mode 100644 index 00000000000..ed0a9288d26 --- /dev/null +++ b/src/ui/cm.rs @@ -0,0 +1,465 @@ +use crate::ipc::{self, new_listener, Connection, Data}; +#[cfg(windows)] +use hbb_common::futures_util::stream::StreamExt; +use hbb_common::{ + allow_err, + config::{Config, ICON}, + fs, log, + message_proto::*, + protobuf::Message as _, + tokio::{self, sync::mpsc, task::spawn_blocking}, +}; +use sciter::{make_args, Element, Value, HELEMENT}; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, RwLock}, +}; + +pub struct ConnectionManagerInner { + root: Option, + senders: HashMap>, +} + +#[derive(Clone)] +pub struct ConnectionManager(Arc>); + +impl Deref for ConnectionManager { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ConnectionManager { + pub fn new() -> Self { + #[cfg(target_os = "linux")] + std::thread::spawn(start_pa); + let inner = ConnectionManagerInner { + root: None, + senders: HashMap::new(), + }; + let cm = Self(Arc::new(RwLock::new(inner))); + #[cfg(target_os = "macos")] + { + let cloned = cm.clone(); + *super::macos::SHOULD_OPEN_UNTITLED_FILE_CALLBACK + .lock() + .unwrap() = Some(Box::new(move || { + cloned.call("awake", &make_args!()); + })); + } + let cloned = cm.clone(); + std::thread::spawn(move || start_ipc(cloned)); + cm + } + + fn get_icon(&mut self) -> String { + ICON.to_owned() + } + + fn check_click_time(&mut self, id: i32) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::ClickTime(crate::get_time()))); + } + } + + #[inline] + fn call(&self, func: &str, args: &[Value]) { + let r = self.read().unwrap(); + if let Some(ref e) = r.root { + allow_err!(e.call_method(func, args)); + } + } + + fn add_connection( + &self, + id: i32, + is_file_transfer: bool, + port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + tx: mpsc::UnboundedSender, + ) { + self.call( + "addConnection", + &make_args!( + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio + ), + ); + self.write().unwrap().senders.insert(id, tx); + } + + fn remove_connection(&self, id: i32) { + self.write().unwrap().senders.remove(&id); + self.call("removeConnection", &make_args!(id)); + } + + async fn handle_data( + &self, + id: i32, + data: Data, + write_jobs: &mut Vec, + conn: &mut Connection, + ) { + match data { + Data::ChatMessage { text } => { + self.call("newMessage", &make_args!(id, text)); + } + Data::ClickTime(ms) => { + self.call("resetClickCallback", &make_args!(ms as f64)); + } + Data::FS(v) => match v { + ipc::FS::ReadDir { + dir, + include_hidden, + } => { + Self::read_dir(&dir, include_hidden, conn).await; + } + ipc::FS::RemoveDir { + path, + id, + recursive, + } => { + Self::remove_dir(path, id, recursive, conn).await; + } + ipc::FS::RemoveFile { path, id, file_num } => { + Self::remove_file(path, id, file_num, conn).await; + } + ipc::FS::CreateDir { path, id } => { + Self::create_dir(path, id, conn).await; + } + ipc::FS::NewWrite { + path, + id, + mut files, + } => { + write_jobs.push(fs::TransferJob::new_write( + id, + path, + files + .drain(..) + .map(|f| FileEntry { + name: f.0, + modified_time: f.1, + ..Default::default() + }) + .collect(), + )); + } + ipc::FS::CancelWrite { id } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.remove_download_file(); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteDone { id, file_num } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.modify_time(); + Self::send(fs::new_done(id, file_num), conn).await; + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteBlock { + id, + file_num, + data, + compressed, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + if let Err(err) = job + .write(FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }) + .await + { + Self::send(fs::new_error(id, err, file_num), conn).await; + } + } + } + }, + _ => {} + } + } + + async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) { + let path = { + if dir.is_empty() { + Config::get_home() + } else { + fs::get_path(dir) + } + }; + if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_dir(fd); + msg_out.set_file_response(file_response); + Self::send(msg_out, conn).await; + } + } + + async fn handle_result( + res: std::result::Result, S>, + id: i32, + file_num: i32, + conn: &mut Connection, + ) { + match res { + Err(err) => { + Self::send(fs::new_error(id, err, file_num), conn).await; + } + Ok(Err(err)) => { + Self::send(fs::new_error(id, err, file_num), conn).await; + } + Ok(Ok(())) => { + Self::send(fs::new_done(id, file_num), conn).await; + } + } + } + + async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) { + Self::handle_result( + spawn_blocking(move || fs::remove_file(&path)).await, + id, + file_num, + conn, + ) + .await; + } + + async fn create_dir(path: String, id: i32, conn: &mut Connection) { + Self::handle_result( + spawn_blocking(move || fs::create_dir(&path)).await, + id, + 0, + conn, + ) + .await; + } + + async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) { + let path = fs::get_path(&path); + Self::handle_result( + spawn_blocking(move || { + if recursive { + fs::remove_all_empty_dir(&path) + } else { + std::fs::remove_dir(&path).map_err(|err| err.into()) + } + }) + .await, + id, + 0, + conn, + ) + .await; + } + + async fn send(msg: Message, conn: &mut Connection) { + match msg.write_to_bytes() { + Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await), + err => allow_err!(err), + } + } + + fn switch_permission(&self, id: i32, name: String, enabled: bool) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::SwitchPermission { name, enabled })); + } + } + + fn close(&self, id: i32) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::Close)); + } + } + + fn send_msg(&self, id: i32, text: String) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::ChatMessage { text })); + } + } + + fn authorize(&self, id: i32) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::Authorize)); + } + } + + fn exit(&self) { + std::process::exit(0); + } +} + +impl sciter::EventHandler for ConnectionManager { + fn attached(&mut self, root: HELEMENT) { + self.write().unwrap().root = Some(Element::from(root)); + } + + sciter::dispatch_script_call! { + fn check_click_time(i32); + fn get_icon(); + fn close(i32); + fn authorize(i32); + fn switch_permission(i32, String, bool); + fn send_msg(i32, String); + fn exit(); + } +} + +#[tokio::main(basic_scheduler)] +async fn start_ipc(cm: ConnectionManager) { + match new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let cm = cm.clone(); + tokio::spawn(async move { + let mut conn_id: i32 = 0; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut write_jobs: Vec = Vec::new(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio} => { + conn_id = id; + cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, tx.clone()); + } + _ => { + cm.handle_data(conn_id, data, &mut write_jobs, &mut stream).await; + } + } + } + _ => {} + } + } + Some(data) = rx.recv() => { + allow_err!(stream.send(&data).await); + } + } + } + cm.remove_connection(conn_id); + }); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + std::process::exit(-1); +} + +#[cfg(target_os = "linux")] +#[tokio::main(basic_scheduler)] +async fn start_pa() { + use hbb_common::config::APP_NAME; + use libpulse_binding as pulse; + use libpulse_simple_binding as psimple; + match new_listener("_pa").await { + Ok(mut incoming) => { + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let mut device: String = "".to_owned(); + if let Some(Ok(Some(Data::Config((_, Some(x)))))) = + stream.next_timeout2(1000).await + { + device = x; + } + if device == "Mute" { + break; + } + if !device.is_empty() { + device = crate::platform::linux::get_pa_source_name(&device); + } + if device.is_empty() { + device = crate::platform::linux::get_pa_monitor(); + } + if device.is_empty() { + break; + } + let spec = pulse::sample::Spec { + format: pulse::sample::Format::F32be, + channels: 2, + rate: crate::platform::linux::PA_SAMPLE_RATE, + }; + log::info!("pa monitor: {:?}", device); + if let Ok(s) = psimple::Simple::new( + None, // Use the default server + APP_NAME, // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + APP_NAME, // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) { + loop { + if let Some(Err(_)) = stream.next_timeout2(1).await { + break; + } + let mut out: Vec = Vec::with_capacity(480 * 4); + unsafe { + out.set_len(out.capacity()); + } + if let Ok(_) = s.read(&mut out) { + if out.iter().filter(|x| **x != 0).next().is_some() { + allow_err!(stream.send(&Data::RawMessage(out)).await); + } + } + } + } else { + log::error!("Could not create simple pulse"); + } + } + Err(err) => { + log::error!("Couldn't get pa client: {:?}", err); + } + } + } + } + } + Err(err) => { + log::error!("Failed to start pa ipc server: {}", err); + } + } +} diff --git a/src/ui/cm.tis b/src/ui/cm.tis new file mode 100644 index 00000000000..60462cd1361 --- /dev/null +++ b/src/ui/cm.tis @@ -0,0 +1,409 @@ +view.windowFrame = is_osx ? #extended : #solid; + +var body; +var connections = []; +var show_chat = false; +var click_callback; +var click_callback_time = 0; + +class Body: Reactor.Component +{ + this var cur = 0; + + function this() { + body = this; + } + + function render() { + if (connections.length == 0) return
; + var c = connections[this.cur]; + this.connection = c; + this.cid = c.id; + var auth = c.authorized; + var me = this; + var callback = function(msg) { + me.sendMsg(msg); + }; + self.timer(1ms, adaptSize); + var right_style = show_chat ? "" : "display: none"; + return
+
+
+
+ {c.name[0].toUpperCase()} +
+
+
{c.name}
+
({c.peer_id})
+
Connected {getElaspsed(c.time)}
+
+
+
+ {c.is_file_transfer || c.port_forward ? "" :
Permissions
} + {c.is_file_transfer || c.port_forward ? "" :
+
+
+
+
} + {c.port_forward ?
Port Forwarding: {c.port_forward}
: ""} +
+
+ {auth ? "" : } + {auth ? "" : } + {auth ? : ""} +
+ {c.is_file_transfer || c.port_forward ? "" :
{svg_chat}
} +
+
+ {c.is_file_transfer || c.port_forward ? "" : } +
+
; + } + + function sendMsg(text) { + if (!text) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.msgs.push({ name: "me", text: text, time: getNowStr()}); + handler.send_msg(cid, text); + body.update(); + }); + } + + event click $(icon.keyboard) (e) { + var { cid, connection } = this; + checkClickTime(function() { + connection.keyboard = !connection.keyboard; + body.update(); + handler.switch_permission(cid, "keyboard", connection.keyboard); + }); + } + + event click $(icon.clipboard) { + var { cid, connection } = this; + checkClickTime(function() { + connection.clipboard = !connection.clipboard; + body.update(); + handler.switch_permission(cid, "clipboard", connection.clipboard); + }); + } + + event click $(icon.audio) { + var { cid, connection } = this; + checkClickTime(function() { + connection.audio = !connection.audio; + body.update(); + handler.switch_permission(cid, "audio", connection.audio); + }); + } + + event click $(button#accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + body.update(); + handler.authorize(cid); + }); + } + + event click $(button#dismiss) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } + + event click $(button#disconnect) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } +} + +$(body).content(); + +var header; + +class Header: Reactor.Component +{ + function this() { + header = this; + } + + function render() { + var me = this; + var conn = connections[body.cur]; + if (conn && conn.unreaded > 0) {; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "inline-block", + }; + self.timer(300ms, function() { + conn.unreaded = 0; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "none", + }; + }); + } + var tabs = connections.map(function(c, i) { return me.renderTab(c, i) }); + return
+ {tabs} +
+
+ < + > +
+
; + } + + function renderTab(c, i) { + var cur = body.cur; + return
+ {c.name} + {c.unreaded > 0 ? {c.unreaded} : ""} +
; + } + + function update_cur(idx) { + checkClickTime(function() { + body.cur = idx; + update(); + self.timer(1ms, adjustHeader); + }); + } + + event click $(div.tab) (_, me) { + var idx = me.index; + if (idx == body.cur) return; + this.update_cur(idx); + } + + event click $(span.left-arrow) { + var cur = body.cur; + if (cur == 0) return; + this.update_cur(cur - 1); + } + + event click $(span.right-arrow) { + var cur = body.cur; + if (cur == connections.length - 1) return; + this.update_cur(cur + 1); + } +} + +if (is_osx) { + $(header).content(
); + $(header).attributes["role"] = "window-caption"; +} else { + $(div.window-toolbar).content(
); + setWindowButontsAndIcon(true); +} + +event click $(div.chaticon) { + checkClickTime(function() { + show_chat = !show_chat; + adaptSize(); + }); +} + +handler.resetClickCallback = function(ms) { + if (click_callback_time - ms < 120) + click_callback = null; +} + +function checkClickTime(callback) { + click_callback_time = getTime(); + click_callback = callback; + handler.check_click_time(body.cid); + self.timer(120ms, function() { + if (click_callback) { + click_callback(); + click_callback = null; + } + }); +} + +function adaptSize() { + $(div.right-panel).style.set { + display: show_chat ? "block" : "none", + }; + var el = $(div.chaticon); + if (el) el.attributes.toggleClass("active", show_chat); + var (x, y, w, h) = view.box(#rectw, #border, #screen); + if (show_chat && w < 600) { + view.move(x - (600 - w), y, 600, h); + } else if (!show_chat && w > 450) { + view.move(x + (w - 300), y, 300, h); + } +} + +function update() { + header.update(); + body.update(); +} + +function bring_to_top(idx=-1) { + if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) { + view.windowState = View.WINDOW_SHOWN; + if (idx >= 0) body.cur = idx; + } else { + view.windowTopmost = true; + view.windowTopmost = false; + } +} + +handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio) { + var conn; + connections.map(function(c) { + if (c.id == id) conn = c; + }); + if (conn) { + conn.authorized = authorized; + update(); + return; + } + if (!name) name = "NA"; + connections.push({ + id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, + port_forward: port_forward, + name: name, authorized: authorized, time: new Date(), + keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, + audio: audio, + }); + body.cur = connections.length - 1; + bring_to_top(); + update(); + self.timer(1ms, adjustHeader); + if (authorized) { + self.timer(3s, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + } +} + +handler.removeConnection = function(id) { + var i = -1; + connections.map(function(c, idx) { + if (c.id == id) i = idx; + }); + connections.splice(i, 1); + if (connections.length == 0) { + handler.exit(); + } else { + if (body.cur >= i && body.cur > 0) body.cur -= 1; + update(); + } +} + +handler.newMessage = function(id, text) { + var idx = -1; + connections.map(function(c, i) { + if (c.id == id) idx = i; + }); + var conn = connections[idx]; + if (!conn) return; + conn.msgs.push({name: conn.name, text: text, time: getNowStr()}); + bring_to_top(idx); + if (idx == body.cur) show_chat = true; + conn.unreaded += 1; + update(); +} + +handler.awake = function() { + view.windowState = View.WINDOW_SHOWN; + view.focus = self; +} + +view << event statechange { + adjustBorder(); +} + +function self.ready() { + adjustBorder(); + var (sw, sh) = view.screenBox(#workarea, #dimension); + var w = 300; + var h = 400; + view.move(sw - w, 0, w, h); +} + +function getElaspsed(time) { + var now = new Date(); + var seconds = Date.diff(time, now, #seconds); + var hours = seconds / 3600; + var days = hours / 24; + hours = hours % 24; + var minutes = seconds % 3600 / 60; + seconds = seconds % 60; + var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds); + if (days > 0) { + out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out); + } + return out; +} + +function updateTime() { + self.timer(1s, function() { + var el = $(#time); + if (el) { + var c = connections[body.cur]; + if (c) { + el.text = getElaspsed(c.time); + } + } + updateTime(); + }); +} + +updateTime(); + +function self.closing() { + view.windowState = View.WINDOW_HIDDEN; + return false; +} + + +function adjustHeader() { + var hw = $(header).box(#width); + var tabswrapper = $(div.tabs-wrapper); + var tabs = $(div.tabs); + var arrows = $(div.tab-arrows); + if (!arrows) return; + var n = connections.length; + var wtab = 80; + var max = hw - 98; + var need_width = n * wtab + 2; // include border of active tab + if (need_width < max) { + arrows.style.set { + display: "none", + }; + tabs.style.set { + width: need_width, + margin-left: 0, + }; + tabswrapper.style.set { + width: need_width, + }; + } else { + var margin = (body.cur + 1) * wtab - max + 30; + if (margin < 0) margin = 0; + arrows.style.set { + display: "block", + }; + tabs.style.set { + width: (max - 20 + margin) + 'px', + margin-left: -margin + 'px' + }; + tabswrapper.style.set { + width: (max + 10) + 'px', + }; + } +} + +view.on("size", adjustHeader); + +// handler.addConnection(0, false, 0, "", "test1", true, false, false, false); +// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false); +// handler.addConnection(2, false, 0, "", "test3", true, false, false, false); +// handler.newMessage(0, 'h'); diff --git a/src/ui/common.css b/src/ui/common.css new file mode 100644 index 00000000000..5a8615d6f75 --- /dev/null +++ b/src/ui/common.css @@ -0,0 +1,319 @@ +html { + var(accent): #0071ff; + var(button): #2C8CFF; + var(gray-bg): #eee; + var(bg): white; + var(border): #ccc; + var(text): #222; + var(placeholder): #aaa; + var(lighter-text): #888; + var(light-text): #666; + var(dark-red): #A72145; + var(dark-yellow): #FBC732; + var(dark-blue): #2E2459; + var(green-blue): #197260; + var(gray-blue): #2B3439; + var(blue-green): #4299bf; + var(light-green): #D4EAB7; + var(dark-green): #5CB85C; + var(blood-red): #F82600; +} + +body { + margin: 0; + color: color(text); +} + +button.button { + height: 2em; + border-radius: 0.5em; + background: color(button); + color: color(bg); + border-color: color(button); + min-width: 40px; +} + +button[type=checkbox], button[type=checkbox]:active { + background: none; + border: none; + color: unset; + height: 1.4em; +} + +button.outline { + border: color(border) solid 1px; + background: transparent; + color: color(text); +} + +button.button:active, button.active { + background: color(accent); + color: color(bg); + border-color: color(accent); +} + +input[type=text], input[type=password], input[type=number] { + width: *; + font-size: 1.5em; + border-color: color(border); + border-radius: 0; + color: black; + padding-left: 0.5em; +} + +input:empty { + color: color(placeholder); +} + +input.outline-focus:focus { + outline: color(button) solid 3px; +} + +@set my-scrollbar +{ + .prev { display:none; } + .next { display:none; } + .base, .next-page, .prev-page { background: white;} + .slider { background: #bbb; border: white solid 4px; } + .base:disabled { background: transparent; } + .slider:hover { background: grey; } + .slider:active { background: grey; } + .base { size: 16px; } + .corner { background: white; } +} + +@mixin ELLIPSIS { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ellipsis { + text-overflow: ellipsis; + white-space: nowrap; +} + +div.password svg { + padding-left: 1em; + size: 16px; + color: #ddd; + background: none; +} + +div.password input { + font-family: Consolas, Menlo, Monaco, 'Courier New'; + font-size: 1.2em; +} + +svg { + background: none; +} + +header { + border-bottom: color(border) solid 1px; + height: 22px; + flow: horizontal; + overflow-x: hidden; + position: relative; +} + +@media platform == "OSX" { + header { + background: linear-gradient(top,#E4E4E4,#D1D1D1); + } +} + +header div.window-icon { + size: 22px; +} + +@media platform != "OSX" { +header { + background: white; + height: 30px; +} + +header div.window-icon { + size: 30px; +} +} + +header div.window-icon icon { + display: block; + margin: *; + size: 16px; + background-size: cover; + background-repeat: no-repeat; +} + +header caption { + size: *; +} + +@media platform != "OSX" { + button.window { + top: 0; + padding: 0 10px; + width: 22px; + height: *; + position: absolute; + margin: 0; + color: black; + border: none; + background: none; + border-radius: 0; + } + button.window div { + size: 10px; + margin: *; + background-size: cover; + background-repeat: no-repeat; + } + button.window:hover { + background: color(gray-bg); + } + button.window#minimize { + right: 84px; + } + button.window#maximize { + right: 42px; + } + button.window#close { + right: 0px; + } + button.window#minimize div { + height: 3px; + border-bottom: black solid 1px; + width: 12px; + } + button.window#maximize div { + border: black solid 1px; + } + button.window#close:hover { + background: #F82600; + } + button.window#close:hover div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + } + button.window#close div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + size: 12px; + } + button.window#maximize.restore div { + border: none; + size: 12px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB1JREFUCNdjsP/AoCDA8P8CQ0MABipgaHBg+H8AAMfSC36WAZteAAAAAElFTkSuQmCC'); +} +} + +div.msgbox { + size: *; +} + +div.msgbox div.send svg { + size: 16px; +} + +div.msgbox div.send span:active { + opacity: 0.5; +} + +div.msgbox div.send span { + display: inline-block; + padding: 6px; +} + +div.msgbox .msgs { + border: none; + size: *; + border-bottom: color(border) 1px solid; + overflow-x: hidden; + overflow-y: scroll-indicator; + border-spacing: 1em; + padding: 1em; +} + +div.msgbox div.send { + flow: horizontal; + height: 30px; + padding: 5px; +} + +div.msgbox div.send input { + height: 20px !important; +} + +div.msgbox div.name { + color: color(dark-green); +} + +div.msgbox div.right-side div { + text-align: right; +} + +div.msgbox div.text { + margin-top: 0.5em; + word-wrap: break-word; + word-break: break-all; +} + +@media platform != "OSX" { +header .window-toolbar { + width: max-content; + background: transparent; + position: absolute; + bottom: 4px; + height: 24px; +} +} + +header svg, menu svg { + size: 14px; +} + +header span, menu span { + padding: 4px 8px; + margin: * 0.5em; + color: color(light-text); +} + +progress { + display: inline-block; + aspect: Progress; + border: none; + margin-right: 1em; + height: 0.25em; + background: transparent; +} + +menu div.separator { + height: 1px; + width: *; + margin: 5px 0; + background: color(gray-bg); + border: none; +} + +menu li { + position: relative; +} + +menu li span { + display: none; +} + +menu li.selected span { + display: inline-block; + position: absolute; + left: -10px; + top: 2px; +} + +.link { + cursor: pointer; + text-decoration: underline; +} + +.link:active { + opacity: 0.5; +} \ No newline at end of file diff --git a/src/ui/common.tis b/src/ui/common.tis new file mode 100644 index 00000000000..2f6987337a6 --- /dev/null +++ b/src/ui/common.tis @@ -0,0 +1,297 @@ +include "sciter:reactor.tis"; + +var handler = $(#handler) || view; +try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {} +var OS = view.mediaVar("platform"); +var is_osx = OS == "OSX"; +var is_win = OS == "Windows"; +var is_linux = OS == "Linux"; +var is_file_transfer; + +function hashCode(str) { + var hash = 160 << 16 + 114 << 8 + 91; + for (var i = 0; i < str.length; i += 1) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash % 16777216; +} + +function intToRGB(i, a = 1) { + return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0xFF) + + ',' + (i & 0xFF) + ',' + a + ')'; +} + +function string2RGB(s, a = 1) { + return intToRGB(hashCode(s), a); +} + +function getTime() { + var now = new Date(); + return now.valueOf(); +} + +function platformSvg(platform, color) { + platform = (platform || "").toLowerCase(); + if (platform == "linux") { + return + + + + + ; + } + if (platform == "mac os") { + return + + ; + } + return + + ; +} + +function centerize(w, h) { + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (w > sw) w = sw; + if (h > sh) h = sh; + var x = (sx + sw - w) / 2; + var y = (sy + sh - h) / 2; + view.move(x, y, w, h); +} + +function setWindowButontsAndIcon(only_min=false) { + if (only_min) { + $(div.window-buttons).content(
+
+
); + } else { + $(div.window-buttons).content(
+
+
+
+
); + } + $(div.window-icon>icon).style.set { + "background-image": "url('" + handler.get_icon() + "')", + }; +} + +function adjustBorder() { + if (is_osx) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + $(header).style.set { + display: "none", + }; + } else { + $(header).style.set { + display: "block", + padding: "0", + }; + } + return; + } + if (view.windowState == view.WINDOW_MAXIMIZED) { + self.style.set { + border: "window-frame-width solid transparent", + }; + } else if (view.windowState == view.WINDOW_FULL_SCREEN) { + self.style.set { + border: "none", + }; + } else { + self.style.set { + border: "black solid 1px", + }; + } + var el = $(button#maximize); + if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED); + el = $(span#fullscreen); + if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN); +} + +var svg_checkmark = ; +var svg_edit = + +; +var svg_eye = + + +; +var svg_send = + +; +var svg_chat = + +; + +function scrollToBottom(el) { + var y = el.box(#height, #content) - el.box(#height, #client); + el.scrollTo(0, y); +} + +function getNowStr() { + var now = new Date(); + return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second); +} + +/******************** start of chatbox ****************************************/ +class ChatBox: Reactor.Component { + this var msgs = []; + this var callback; + + function this(params) { + if (params) { + this.msgs = params.msgs || []; + this.callback = params.callback; + } + } + + function renderMsg(msg) { + var cls = msg.name == "me" ? "right-side msg" : "left-side msg"; + return
+ {msg.name == "me" ? +
{msg.time + " "} me
: +
{msg.name} {" " + msg.time}
+ } +
{msg.text}
+
; + } + + function render() { + var me = this; + var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); }); + self.timer(1ms, function() { + scrollToBottom(me.msgs); + }); + return
+ + {msgs} + +
+ + {svg_send} +
+
; + } + + function send() { + var el = this.$(input); + var value = (el.value || "").trim(); + el.value = ""; + if (!value) return; + if (this.callback) this.callback(value); + } + + event keydown $(input) (evt) { + if (!evt.shortcutKey) { + if (evt.keyCode == Event.VK_ENTER || + (view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) { + this.send(); + } + } + } + + event click $(div.send span) { + this.send(); + view.focus = $(input); + } +} +/******************** end of chatbox ****************************************/ + +/******************** start of msgbox ****************************************/ +var remember_password = false; +var msgbox_params; +function getMsgboxParams() { + return msgbox_params; +} + +function msgbox(type, title, text, callback, height, width) { + var has_msgbox = msgbox_params != null; + if (!has_msgbox && !type) return; + var remember = false; + try { + remember = handler.get_remember(); + } catch(e) {} + msgbox_params = { + remember: remember, type: type, text: text, title: title, + getParams: getMsgboxParams, + callback: callback + }; + if (has_msgbox) return; + var dialog = { + client: true, + parameters: msgbox_params, + width: width, + height: height, + }; + var html = handler.get_msgbox(); + if (html) dialog.html = html; + else dialog.url = self.url("msgbox.html"); + var res = view.dialog(dialog); + msgbox_params = null; + stdout.printf("msgbox return, type: %s, res: %s\n", type, res); + if (type.indexOf("custom") >= 0) { + // + } else if (!res) { + if (!is_port_forward) view.close(); + } else if (res == "!alive") { + // do nothing + } else if (res.type == "input-password") { + if (!is_port_forward) connecting(); + handler.login(res.password, res.remember); + } else if (res.reconnect) { + if (!is_port_forward) connecting(); + handler.reconnect(); + } +} + +function connecting() { + handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait."); +} + +handler.msgbox = function(type, title, text, callback=null, height=180, width=500) { + // directly call view.Dialog from native may crash, add timer here, seem safe + // too short time, msgbox won't get focus, per my test, 150 is almost minimun + self.timer(150ms, function() { msgbox(type, title, text, callback, height, width); }); +} +/******************** end of msgbox ****************************************/ + +function Progress() +{ + var _val; + var pos = -0.25; + + function step() { + if( _val !== undefined ) { this.refresh(); return false; } + pos += 0.02; + if( pos > 1.25) + pos = -0.25; + this.refresh(); + return true; + } + + function paintNoValue(gfx) + { + var (w,h) = this.box(#dimension,#inner); + var x = pos * w; + w = w * 0.25; + gfx.fillColor( this.style#color ) + .pushLayer(#inner-box) + .rectangle(x,0,w,h) + .popLayer(); + return true; + } + + this[#value] = property(v) { + get return _val; + set { + _val = undefined; + pos = -0.25; + this.paintContent = paintNoValue; + this.animate(step); + this.refresh(); + } + } + + this.value = ""; +} diff --git a/src/ui/file_transfer.css b/src/ui/file_transfer.css new file mode 100644 index 00000000000..8acde0623a8 --- /dev/null +++ b/src/ui/file_transfer.css @@ -0,0 +1,255 @@ +div#file-transfer-wrapper { + size:*; + display: none; +} + +div#file-transfer { + size: *; + margin: 0; + flow: horizontal; + background: color(gray-bg); + padding: 0.5em; +} + +table +{ + font: system; + border: 1px solid color(border); + flow: table-fixed; + prototype: Grid; + size: *; + padding:0; + border-spacing: 0; + overflow-x: auto; + overflow-y: hidden; +} + +table > thead { + behavior: column-resizer; + border-bottom: color(border) solid 1px; +} + +table > tbody { + overflow-y: scroll-indicator; + size: *; + background: white; +} + +table th { + background-color: color(gray-bg); +} + +table th +{ + padding: 4px; + foreground-repeat: no-repeat; + foreground-position: 50% 3px auto auto; + border-left: color(border) solid 1px; +} + +table th.sortable[sort=asc] +{ + foreground-image: url(stock:arrow-down); +} + +table th.sortable[sort=desc] +{ + foreground-image: url(stock:arrow-up); +} + +table th:nth-child(1) { + width: 32px; +} + +table th:nth-child(2) { + width: *; +} + +table th:nth-child(3) { + width: *; +} + +table th:nth-child(4) { + width: 45px; +} + +table.has_current thead th:current { + font-weight: bold; +} + +table tr:nth-child(odd) { background-color: white; } /* each odd row */ +table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ + +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} + +table td +{ + padding: 4px; + text-align: left; + font-size: 1em; + height: 1.4em; + @ELLIPSIS; +} + +table.folder-view td:nth-child(1) { + behavior:shell-icon; +} + +table td:nth-child(3), table td:nth-child(4) { + color: color(lighter-text); + font-size: 0.9em; +} + +table.has_current tr:current td { + color: white; +} + +table td:nth-child(4) { + text-align: right; +} + +section { + size: *; + margin: 1em; + border-spacing: 0.5em; +} + +table td:nth-child(1) { + foreground-repeat: no-repeat; + foreground-position: 50% 50% +} + +div.toolbar { + flow: horizontal; +} + +div.toolbar svg { + size: 16px; +} + +div.toolbar .spacer { + width: *; +} + +div.toolbar > div.button { + padding: 4px 8px; + opacity: 0.66; +} + +div.toolbar > div.button:active { + opacity: 1; + background-color: #ddd; +} + +div.toolbar > div.button:hover { + opacity: 1; +} + +div.toolbar > div.send { + flow: horizontal; + border-spacing: 0.5em; +} + +div.remote > div.send svg { + transform: scale(-1, 1); +} + +div.navbar { + border: color(border) solid 1px; + padding: 4px 0; +} + +select.select-dir { + width: *; + padding: 0 4px; +} + +div.title { + flow: horizontal; + border-spacing: 1em; + position: relative; +} + +div.title svg.computer { + size: 48px; +} + +div.title div { + margin: * 0; + color: color(light-text); +} + +div.title div.platform { + position: absolute; + left: 12px; + top: 7px; +} + +div.title div.platform svg { + size: 24px; +} + +table.job-table tr td { + width: *; + padding: 0.5em 1em; + border-bottom: color(border) 1px solid; + flow: horizontal; + border-spacing: 1em; + height: 3em; + overflow-x: hidden; +} + +table.job-table tr svg { + size: 16px; +} + +table.job-table tr.is_remote svg { + transform: scale(-1, 1); +} + +table.job-table tr td div.text { + width: *; + overflow-x: hidden; +} + +table.job-table tr td div.path { + width: *; + color: color(light-text); + @ELLIPSIS; +} + +table.job-table tr:current td div.path { + color: white; +} + +table#port-forward thead tr th { + padding-left: 1em; + size: *; +} + +table#port-forward tr td { + height: 3em; + text-align: left; +} + +table#port-forward input[type=text], table#port-forward input[type=number] { + font-size: 1.2em; +} + +table#port-forward td.right-arrow svg { + size: 1.2em; + transform: rotate(180deg); +} + +table#port-forward td.remove svg { + size: 0.8em; +} + +table#port-forward tr.value td { + padding-left: 1em; + font-size: 1.5em; + color: black; +} diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis new file mode 100644 index 00000000000..043a4c3f94f --- /dev/null +++ b/src/ui/file_transfer.tis @@ -0,0 +1,617 @@ +var remote_home_dir; + +var svg_add_folder = + + +; +var svg_trash = + + + +; +var svg_arrow = + +; +var svg_home = + +; +var svg_refresh = + +; +var svg_cancel = ; +var svg_computer = + + + +; + +function getSize(type, size) { + if (!size) { + if (type <= 3) return ""; + return "0B"; + } + size = size.toFloat(); + var toFixed = function(size) { + size = (size * 100).toInteger(); + var a = (size / 100).toInteger(); + if (size % 100 == 0) return a; + if (size % 10 == 0) return a + '.' + (size % 10); + var b = size % 100; + if (b < 10) b = '0' + b; + return a + '.' + b; + } + if (size < 1024) return size.toInteger() + "B"; + if (size < 1024 * 1024) return toFixed(size / 1024) + "K"; + if (size < 1024 * 1024 * 1024) return toFixed(size / (1024 * 1024)) + "M"; + return toFixed(size / (1024 * 1024 * 1024)) + "G"; +} + +function getParentPath(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + if (res <= 0) return "/"; + return path.substr(0, res); +} + +function getFileName(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + return path.substr(res + 1); +} + +function getExt(name) { + if (name.indexOf(".") == 0) { + return ""; + } + var i = name.lastIndexOf("."); + if (i > 0) return name.substr(i + 1); + return ""; +} + +var jobIdCounter = 1; + +class JobTable: Reactor.Component { + this var jobs = []; + this var job_map = {}; + + function render() { + var me = this; + var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); }); + return
+ + {rows} + +
; + } + + event click $(svg.cancel) (_, me) { + var job = this.jobs[me.parent.parent.index]; + var id = job.id; + handler.cancel_job(id); + delete this.job_map[id]; + var i = -1; + this.jobs.map(function(job, idx) { + if (job.id == id) i = idx; + }); + this.jobs.splice(i, 1); + this.update(); + var is_remote = job.is_remote; + if (job.type != "del-dir") is_remote = !is_remote; + refreshDir(is_remote); + } + + function send(path, is_remote) { + var to; + var show_hidden; + if (is_remote) { + to = file_transfer.local_folder_view.fd.path; + show_hidden = file_transfer.remote_folder_view.show_hidden; + } else { + to = file_transfer.remote_folder_view.fd.path; + show_hidden = file_transfer.local_folder_view.show_hidden; + } + if (!to) return; + to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); + var id = jobIdCounter; + jobIdCounter += 1; + this.jobs.push({ type: "transfer", + id: id, path: path, to: to, + include_hidden: show_hidden, + is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.send_files(id, path, to, show_hidden, is_remote); + this.update(); + } + + function addDelDir(path, is_remote) { + var id = jobIdCounter; + jobIdCounter += 1; + this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.remove_dir_all(id, path, is_remote); + this.update(); + } + + function getSvg(job) { + if (job.type == "transfer") { + return svg_send; + } else if (job.type == "del-dir") { + return svg_trash; + } + } + + function getStatus(job) { + if (!job.entries) return "Waiting"; + var i = job.file_num + 1; + var n = job.num_entries || job.entries.length; + if (i > n) i = n; + var res = i + ' / ' + n + " files"; + if (job.total_size > 0) res += ", " + getSize(0, job.finished_size) + ' / ' + getSize(0, job.total_size); + // below has problem if some file skipped + var percent = (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); + if (job.finished) percent = '100'; + res += ", " + percent + "%"; + if (job.finished) res = "Finished " + res; + if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; + return res; + } + + function updateJob(job) { + var el = this.select("div[id=s" + job.id + "]"); + if (el) el.text = this.getStatus(job); + } + + function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) { + var job = this.job_map[id]; + if (!job) return; + if (file_num < job.file_num) return; + job.file_num = file_num; + var n = job.num_entries || job.entries.length; + job.finished = job.file_num >= n - 1 || err == "cancel"; + job.finished_size = finished_size; + job.speed = speed || 0; + this.updateJob(job); + if (job.type == "del-dir") { + if (job.finished) { + if (!err) { + handler.remove_dir(job.id, job.path, job.is_remote); + refreshDir(job.is_remote); + } + } else if (!job.no_confirm) { + handler.confirm_delete_files(id, job.file_num + 1); + } + } else if (job.finished || file_num == -1) { + refreshDir(!job.is_remote); + } + } + + function renderRow(job, i) { + var svg = this.getSvg(job); + return + {svg} +
+
{job.path}
+
{this.getStatus(job)}
+
+ {svg_cancel} + ; + } +} + +class FolderView : Reactor.Component { + this var fd = {}; + this var history = []; + this var show_hidden = false; + + function sep() { + return handler.get_path_sep(this.is_remote); + } + + function this(params) { + this.is_remote = params.is_remote; + if (this.is_remote) { + this.show_hidden = !!handler.get_option("remote_show_hidden"); + } else { + this.show_hidden = !!handler.get_option("local_show_hidden"); + } + if (!this.is_remote) { + var dir = handler.get_option("local_dir"); + if (dir) { + this.fd = handler.read_dir(dir, this.show_hidden); + if (this.fd) return; + } + this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden); + } + } + + // sort predicate + function foldersFirst(a, b) { + if (a.type <= 3 && b.type > 3) return -1; + if (a.type > 3 && b.type <= 3) return +1; + if (a.name == b.name) return 0; + return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase()); + } + + function render() + { + return
+ {this.renderTitle()} + {this.renderNavBar()} + {this.renderOpBar()} + {this.renderTable()} +
; + } + + function renderTitle() { + return
+ {svg_computer} +
{platformSvg(handler.get_platform(this.is_remote), "white")}
+
{this.is_remote ? "Remote Computer" : "Local Computer"}
+
+ } + + function renderNavBar() { + return
+
{svg_home}
+
{svg_arrow}
+
{svg_arrow}
+ {this.renderSelect()} +
{svg_refresh}
+
; + } + + function renderSelect() { + return ; + } + + function renderOpBar() { + if (this.is_remote) { + return
+
{svg_send}Receive
+
+
{svg_add_folder}
+
{svg_trash}
+
; + } + return
+
{svg_add_folder}
+
{svg_trash}
+
+
Send{svg_send}
+
; + } + + function get_updated() { + this.table.sortRows(false); + if (this.fd && this.fd.path) this.select_dir.value = this.fd.path; + } + + function renderTable() { + var fd = this.fd; + var entries = fd.entries || []; + var table = this.table; + if (!table || !table.sortBy) { + entries.sort(this.foldersFirst); + } + var me = this; + var path = fd.path; + if (path != "/" && path) { + entries = [{ name: "..", type: 1 }].concat(entries); + } + var rows = entries.map(function(e) { return me.renderRow(e); }); + var id = (this.is_remote ? "remote" : "local") + "-folder-view"; + return + + + + + {rows} + + + +
  • {svg_checkmark}Show Hidden Files
  • + +
    +
    NameModifiedSize
    ; + } + + function joinPath(name) { + var path = this.fd.path; + if (path == "/") { + if (this.sep() == "/") return this.sep() + name; + else return name; + } + return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name; + } + + function attached() { + var me = this; + this.table.onRowDoubleClick = function (row) { + var type = row[0].attributes["type"]; + if (type > 3) return; + var name = row[1].text; + var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name); + me.goto(path, true); + } + this.get_updated(); + } + + function goto(path, push) { + if (!path) return; + if (this.sep() == "\\" && path.length == 2) { // windows drive + path += "\\"; + } + if (push) this.pushHistory(); + if (this.is_remote) { + handler.read_remote_dir(path, this.show_hidden); + } else { + var fd = handler.read_dir(path, this.show_hidden); + this.refresh({ fd: fd }); + } + } + + function refresh(data) { + if (!data.fd || !data.fd.path) return; + if (this.is_remote && !remote_home_dir) { + remote_home_dir = data.fd.path; + } + this.update(data); + var me = this; + self.timer(1ms, function() { me.get_updated(); }); + } + + function renderRow(entry) { + var path; + if (this.is_remote) { + path = handler.get_icon_path(entry.type, getExt(entry.name)); + } else { + path = this.joinPath(entry.name); + } + var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0; + return + + {entry.name} + {tm || ""} + {getSize(entry.type, entry.size)} + ; + } + + event click $(#switch-hidden) { + this.show_hidden = !this.show_hidden; + this.refreshDir(); + } + + event click $(.goup) () { + var path = this.fd.path; + if (!path || path == "/") return; + path = getParentPath(this.is_remote, path); + this.goto(path, true); + } + + event click $(.goback) () { + var path = this.history.pop(); + if (!path) return; + this.goto(path, false); + } + + event click $(.trash) () { + var row = this.getCurrentRow(); + if (!row) return; + var path = row[0]; + var type = row[1]; + var new_history = []; + for (var i = 0; i < this.history.length; ++i) { + var h = this.history[i]; + if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h); + } + this.history = new_history; + if (type == 1) { + file_transfer.job_table.addDelDir(path, this.is_remote); + } else { + confirmDelete(path, this.is_remote); + } + } + + event click $(.add-folder) () { + var me = this; + handler.msgbox("custom", "Create Folder", "
    \ +
    Please enter the folder name:
    \ +
    \ +
    ", function(res=null) { + if (!res) return; + if (!res.name) return; + var name = res.name.trim(); + if (!name) return; + if (name.indexOf(me.sep()) >= 0) { + handler.msgbox("custom-error", "Create Folder", "Invalid folder name"); + return; + } + var path = me.joinPath(name); + handler.create_dir(jobIdCounter, path, me.is_remote); + create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path }; + jobIdCounter += 1; + }); + } + + function refreshDir() { + this.goto(this.fd.path, false); + } + + event click $(.refresh) () { + this.refreshDir(); + } + + event click $(.home) () { + var path = this.is_remote ? remote_home_dir : handler.get_home_dir(); + if (!path) return; + if (path == this.fd.path) return; + this.goto(path, true); + } + + function getCurrentRow() { + var row = this.table.getCurrentRow(); + if (!row) return; + var name = row[1].text; + if (!name || name == "..") return; + var type = row[0].attributes["type"]; + return [this.joinPath(name), type]; + } + + event click $(.send) () { + var cur = this.getCurrentRow(); + if (!cur) return; + file_transfer.job_table.send(cur[0], this.is_remote); + } + + event change $(.select-dir) (_, el) { + var x = getTime() - last_key_time; + if (x < 1000) return; + if (this.fd.path != el.value) { + this.goto(el.value, true); + } + } + + event keydown $(.select-dir) (evt, me) { + if (evt.keyCode == Event.VK_ENTER || + (view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) { + this.goto(me.value, true); + } + } + + function pushHistory() { + var path = this.fd.path; + if (!path) return; + if (path != this.history[this.history.length - 1]) this.history.push(path); + } +} + +var file_transfer; + +class FileTransfer: Reactor.Component { + function this(params) { + file_transfer = this; + } + + function render() { + return
    + + + +
    ; + } +} + +function initializeFileTransfer() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} + +handler.updateFolderFiles = function(fd) { + fd.entries = fd.entries || []; + if (fd.id > 0) { + var jt = file_transfer.job_table; + var job = jt.job_map[fd.id]; + if (job) { + job.file_num = -1; + job.total_size = fd.total_size; + job.entries = fd.entries; + job.num_entries = fd.num_entries; + file_transfer.job_table.updateJobStatus(job.id); + } + } else { + file_transfer.remote_folder_view.refresh({ fd: fd }); + } +} + +handler.jobProgress = function(id, file_num, speed, finished_size) { + file_transfer.job_table.updateJobStatus(id, file_num, null, speed, finished_size); +} + +handler.jobDone = function(id, file_num = -1) { + var job = deleting_single_file_jobs[id] || create_dir_jobs[id]; + if (job) { + refreshDir(job.is_remote); + return; + } + file_transfer.job_table.updateJobStatus(id, file_num); +} + +handler.jobError = function(id, err, file_num = -1) { + var job = deleting_single_file_jobs[id]; + if (job) { + handler.msgbox("custom-error", "Delete File", err); + return; + } + job = create_dir_jobs[id]; + if (job) { + handler.msgbox("custom-error", "Create Folder", err); + return; + } + if (file_num < 0) { + handler.msgbox("custom-error", "Failed", err); + } + file_transfer.job_table.updateJobStatus(id, file_num, err); +} + +function refreshDir(is_remote) { + if (is_remote) file_transfer.remote_folder_view.refreshDir(); + else file_transfer.local_folder_view.refreshDir(); +} + +var deleting_single_file_jobs = {}; +var create_dir_jobs = {} + +function confirmDelete(path, is_remote) { + handler.msgbox("custom-skip", "Confirm Delete", "
    \ +
    Are you sure you want to deelte this file?
    \ + " + path + "
    \ +
    ", function(res=null) { + if (res) { + handler.remove_file(jobIdCounter, path, 0, is_remote); + deleting_single_file_jobs[jobIdCounter] = { is_remote: is_remote, path: path }; + jobIdCounter += 1; + } + }); +} + +handler.confirmDeleteFiles = function(id, i, name) { + var jt = file_transfer.job_table; + var job = jt.job_map[id]; + if (!job) return; + var n = job.num_entries; + if (i >= n) return; + var file_path = job.path; + if (name) file_path += handler.get_path_sep(job.is_remote) + name; + handler.msgbox("custom-skip", "Confirm Delete", "
    \ +
    Deleting #" + (i + 1) + " of " + n + " files.
    \ +
    Are you sure you want to deelte this file?
    \ + " + name + "
    \ +
    Do this for all conflicts
    \ +
    ", function(res=null) { + if (!res) { + jt.updateJobStatus(id, i - 1, "cancel"); + } else if (res.skip) { + if (res.remember) jt.updateJobStatus(id, i, "cancel"); + else handler.jobDone(id, i); + } else { + job.no_confirm = res.remember; + if (job.no_confirm) handler.set_no_confirm(id); + handler.remove_file(id, file_path, i, job.is_remote); + } + }); +} + +function save_file_transfer_close_state() { + var local_dir = file_transfer.local_folder_view.fd.path || ""; + var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : ""; + var remote_dir = file_transfer.remote_folder_view.fd.path || ""; + var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : ""; + handler.save_close_state("local_dir", local_dir); + handler.save_close_state("local_show_hidden", local_show_hidden); + handler.save_close_state("remote_dir", remote_dir); + handler.save_close_state("remote_show_hidden", remote_show_hidden); +} diff --git a/src/ui/grid.tis b/src/ui/grid.tis new file mode 100644 index 00000000000..f58d6526163 --- /dev/null +++ b/src/ui/grid.tis @@ -0,0 +1,234 @@ +class Grid: Behavior { + const TABLE_HEADER_CLICK = 0x81; + const TABLE_ROW_CLICK = 0x82; + const TABLE_ROW_DBL_CLICK = 0x83; + function onHeaderClick(headerCell) + { + this.postEvent(TABLE_HEADER_CLICK, headerCell.index, headerCell); + return true; + } + + function onRowClick(row , reason) + { + this.postEvent(TABLE_ROW_CLICK, row.index, row); + return true; + } + + function onRowDoubleClick(row) + { + this.postEvent(TABLE_ROW_DBL_CLICK, row.index, row); + return true; + } + + function getCurrentRow() + { + return this.$(tbody>tr:current); + } + + function getCurrentColumn() + { + return this.$(thead>:current); // return current cell in header row + } + + function setCurrentRow(row, reason = #by_code, doubleClick = false) + { + if (!row) return; + // get previously selected row: + var prev = this.getCurrentRow(); + if (prev) + { + if (prev === row && !doubleClick) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + } + row.state.current = true; + row.scrollToView(); + + if (doubleClick) + this.onRowDoubleClick(row,reason); + else + this.onRowClick(row,reason); + } + + function setCurrentColumn(col) + { + // get previously selected column: + var prev = this.getCurrentColumn(); + if (prev) + { + if (prev === col) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + } + col.state.current = true; // set state flag + col.scrollToView(); + this.onHeaderClick(col); + } + + function sortRows(sortClicked) + { + var col = this.sortBy; + if (!col) return; + var byColumn = col.index; + var nowDesc = (col.attributes["sort"] || "desc") == "desc"; + if (sortClicked) (this.$(thead [sort]) || col).attributes["sort"] = undefined; // drop any other sort order. + var getValue = function(x) { + var value = x.attributes["value"]; + if (value == undefined) return x.text.toLowerCase(); + return value.toInteger(); + } + var sort = function(r1, r2, asc) { + if (r1[1].text == "..") { + return -1; + } + if (r2[1].text == "..") { + return 1; + } + if (!asc) + return getValue(r1[byColumn]) < getValue(r2[byColumn]) ? -1 : 1; + else + return getValue(r1[byColumn]) > getValue(r2[byColumn]) ? -1 : 1; + } + if (nowDesc) + { + if (sortClicked) col.attributes["sort"] = "asc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? true : false)); + } else { + if (sortClicked) col.attributes["sort"] = "desc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? false : true)); + } + } + + function attached() + { + assert this.tag == "table" : "wrong element type for grid, table expected"; + this.body = this.$(:root>tbody); + assert this.body : "Grid require element"; + } + + function onMouse(evt) + { + if ((evt.type != Event.MOUSE_DOWN) && (evt.type != Event.MOUSE_DCLICK)) + return false; + + if (!evt.mainButton) + return false; + + // auxiliary function, returns row this target element belongs to + function targetRow(target) { return target.$p(tbody>tr); } + + // auxiliary function, returns row this target element belongs to + function targetHeaderCell(target) { return target.$p(thead>tr>th); } + + if (var row = targetRow(evt.target)) // click on the row + this.setCurrentRow(row, #by_mouse, evt.type == Event.MOUSE_DCLICK); + else if (var headerCell = targetHeaderCell(evt.target)) + { + this.setCurrentColumn(headerCell); // click on the header cell + if (evt.type != Event.MOUSE_DCLICK && headerCell.$is(.sortable)) { + this.sortBy = headerCell; + this.sortRows(true); + } + } + + //return true; // as it is always ours then stop event bubbling + } + + function onFocus(evt) + { + return (evt.type == Event.GOT_FOCUS || evt.type == Event.LOST_FOCUS); + } + + function onKey(evt) + { + + if (evt.type != Event.KEY_DOWN) + return false; + + switch(evt.keyCode) + { + case Event.VK_DOWN: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + if (idx < this.body.length) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_UP: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index - 1 : this.length - 1; + if (idx >= 0) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_PRIOR: + { + var y = this.body.scroll(#top) - this.body.scroll(#height); + var r; + for(var i = this.body.length - 1; i >= 0; --i) + { + var pr = r; r = this.body[i]; + if (r.box(#top, #inner, #content) < y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + case Event.VK_NEXT: + { + var y = this.body.scroll(#top) + 2 * this.body.scroll(#height); + var lastScrollable = this.body.length - 1; + var r; + for(var i = 0; i <= lastScrollable; ++i) + { + var pr = r; r = this.body[i]; + if (r.box(#bottom, #inner, #content) > y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + + case Event.VK_HOME: + { + if (this.body.length) + this.setCurrentRow(this.body.first,#by_key); + } + return true; + + case Event.VK_END: + { + if (this.body.length) + this.setCurrentRow(this.body.last,#by_key); + } + return true; + } + var char = handler.get_char(keymap[evt.keyCode] || "", evt.keyCode); + if (char) { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + while (idx < this.body.length) { + var el = this.body[idx]; + var text = el[1].text; + if (text && text[0].toLowerCase() == char) { + this.setCurrentRow(el, #by_key); + return true; + } + idx += 1; + } + } + if (evt.keyCode == Event.VK_ENTER || + (view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) { + this.onRowDoubleClick(this.getCurrentRow()); + } + return false; + } +} diff --git a/src/ui/header.css b/src/ui/header.css new file mode 100644 index 00000000000..d0329008e54 --- /dev/null +++ b/src/ui/header.css @@ -0,0 +1,67 @@ +header #screens { + background: white; + border: #A9A9A9 1px solid; + height: 22px; + border-radius: 4px; + flow: horizontal; + border-spacing: 0.5em; + padding-right: 1em; + position: relative; +} + +header #screen { + text-align: center; + margin: 3px 0; + width: 18px; + height: 14px; + border: color(border) solid 1px; + font-size: 11px; + color: color(light-text); +} + +header #secure { + position: absolute; + left: -10px; + top: -2px; +} + +header #secure svg { + size: 18px; +} + +header .remote-id { + width: *; + padding-left: 30px; + padding-right: 4em; + margin: * 0; +} + +header span:active, header #screen:active { + color: black; + background: color(gray-bg); +} + +div#global-screens { + position: relative; + margin: 2px 0; +} + +div#global-screens > div { + position: absolute; + border: color(border) solid 1px; + text-align: center; + color: color(light-text); +} + +header #screen.current, div#global-screens > div.current { + background: #666; + color: white; +} + +span#fullscreen.active { + border: color(border) solid 1px; +} + +button:disabled { + opacity: 0.3; +} \ No newline at end of file diff --git a/src/ui/header.tis b/src/ui/header.tis new file mode 100644 index 00000000000..ae55faf30dc --- /dev/null +++ b/src/ui/header.tis @@ -0,0 +1,377 @@ +var pi = handler.get_default_pi(); // peer information +var chat_msgs = []; + +var svg_fullscreen = + +; +var svg_action = ; +var svg_display = + +; +var svg_secure = + +; +var svg_insecure = ; +var svg_insecure_relay = ; +var svg_secure_relay = ; + +view << event statechange { + adjustBorder(); + adaptDisplay(); + view.focus = handler; + var fs = view.windowState == View.WINDOW_FULL_SCREEN; + var el = $(#fullscreen); + if (el) el.attributes.toggleClass("active", fs); + el = $(#maximize); + if (el) { + el.state.disabled = fs; + } +} + +var header; +var old_window_state = View.WINDOW_SHOWN; +var input_blocked; + +class Header: Reactor.Component { + function this(params) { + header = this; + } + + function render() { + var icon_conn; + var title_conn; + if (this.secure_connection && this.direct_connection) { + icon_conn = svg_secure; + title_conn = "Direct and secure connection"; + } else if (this.secure_connection && !this.direct_connection) { + icon_conn = svg_secure_relay; + title_conn = "Relayed and secure connection"; + } else if (!this.secure_connection && this.direct_connection) { + icon_conn = svg_insecure; + title_conn = "Direct and insecure connection"; + } else { + icon_conn = svg_insecure_relay; + title_conn = "Relayed and insecure connection"; + } + var title = handler.get_id(); + if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; + if ((pi.displays || []).length == 0) { + return
    {title}
    ; + } + var screens = pi.displays.map(function(d, i) { + return
    + {i+1} +
    ; + }); + updateWindowToolbarPosition(); + var style = "flow: horizontal;"; + if (is_osx) style += "margin: *"; + self.timer(1ms, toggleMenuState); + return
    + {is_osx ? "" : {svg_fullscreen}} +
    + {icon_conn} +
    {handler.get_id()}
    +
    {screens}
    + {this.renderGlobalScreens()} +
    + {svg_chat} + {svg_action} + {svg_display} + {this.renderDisplayPop()} + {this.renderActionPop()} +
    ; + } + + function renderDisplayPop() { + return + +
  • Adjust Window
  • +
    +
  • {svg_checkmark}Original
  • +
  • {svg_checkmark}Shrink
  • +
  • {svg_checkmark}Stretch
  • +
    +
  • {svg_checkmark}Good image quality
  • +
  • {svg_checkmark}Balanced
  • +
  • {svg_checkmark}Optimize reaction time
  • +
  • {svg_checkmark}Custom
  • +
    +
  • {svg_checkmark}Show remote cursor
  • + {audio_enabled ?
  • {svg_checkmark}Mute
  • : ""} + {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}Disable clipboard
  • : ""} + {keyboard_enabled ?
  • {svg_checkmark}Lock after session end
  • : ""} + {false && pi.platform == "Windows" ?
  • {svg_checkmark}Privacy mode
  • : ""} + + ; + } + + function renderActionPop() { + return + +
  • Transfer File
  • +
  • IP Tunneling
  • + {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • Insert Ctrl + Alt + Del
  • : ""} + {keyboard_enabled ?
  • Insert Lock
  • : ""} + {false && pi.platform == "Windows" ?
  • Block user input
  • : ""} + {handler.support_refresh() ?
  • Refresh
  • : ""} + +
    ; + } + + function renderGlobalScreens() { + if (pi.displays.length < 2) return ""; + var x0 = 9999999; + var y0 = 9999999; + var x = -9999999; + var y = -9999999; + pi.displays.map(function(d, i) { + if (d.x < x0) x0 = d.x; + if (d.y < y0) y0 = d.y; + var dx = d.x + d.width; + if (dx > x) x = dx; + var dy = d.y + d.height; + if (dy > y) y = dy; + }); + var w = x - x0; + var h = y - y0; + var scale = 16. / h; + var screens = pi.displays.map(function(d, i) { + var min_wh = d.width > d.height ? d.height : d.width; + var style = "width:" + (d.width * scale) + "px;" + + "height:" + (d.height * scale) + "px;" + + "left:" + ((d.x - x0) * scale) + "px;" + + "top:" + ((d.y - y0) * scale) + "px;" + + "font-size:" + (min_wh * 0.9 * scale) + "px;"; + return
    {i+1}
    ; + }); + + var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;"; + return
    + {screens} +
    ; + } + + event click $(#fullscreen) (_, el) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + if (old_window_state == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = old_window_state; + } else { + old_window_state = view.windowState; + if (view.windowState == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = View.WINDOW_FULL_SCREEN; + } + } + + event click $(#chat) { + startChat(); + } + + event click $(#action) (_, me) { + var menu = $(menu#action-options); + me.popup(menu); + } + + event click $(#display) (_, me) { + var menu = $(menu#display-options); + me.popup(menu); + } + + event click $(#screen) (_, me) { + if (pi.current_display == me.index) return; + handler.switch_display(me.index); + } + + event click $(#transfer-file) { + handler.transfer_file(); + } + + event click $(#tunnel) { + handler.tunnel(); + } + + event click $(#ctrl-alt-del) { + handler.ctrl_alt_del(); + } + + event click $(#lock-screen) { + handler.lock_screen(); + } + + event click $(#refresh) { + handler.refresh_video(); + } + + event click $(#block-input) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = "Unblock user input"; + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = "Block user input"; + } + } + + event click $(menu#display-options>li) (_, me) { + if (me.id == "custom") { + handle_custom_image_quality(); + } else if (me.attributes.hasClass("toggle-option")) { + handler.toggle_option(me.id); + toggleMenuState(); + } else if (!me.attributes.hasClass("selected")) { + var type = me.attributes["type"]; + if (type == "image-quality") { + handler.save_image_quality(me.id); + } else if (type == "view-style") { + handler.save_view_style(me.id); + adaptDisplay(); + } + toggleMenuState(); + } + } +} + +function handle_custom_image_quality() { + var tmp = handler.get_custom_image_quality(); + var bitrate0 = tmp[0] || 50; + var quantizer0 = tmp.length > 1 ? tmp[1] : 100; + handler.msgbox("custom", "Custom Image Quality", "
    \ +
    x% bitrate
    \ +
    x% quantizer
    \ +
    ", function(res=null) { + if (!res) return; + if (!res.bitrate) return; + handler.save_custom_image_quality(res.bitrate, res.quantizer); + toggleMenuState(); + }); +} + +function toggleMenuState() { + var values = []; + var q = handler.get_image_quality(); + if (!q) q = "balanced"; + values.push(q); + var s = handler.get_view_style(); + if (!s) s = "original"; + values.push(s); + for (var el in $$(menu#display-options>li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } + for (var id in ["show-remote-cursor", "disable-audio", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) { + var el = self.select('#' + id); + if (el) { + el.attributes.toggleClass("selected", handler.get_toggle_option(id)); + } + } +} + +if (is_osx) { + $(header).content(
    ); + $(header).attributes["role"] = "window-caption"; +} else { + if (is_file_transfer || is_port_forward) { + $(caption).content(
    ); + } else { + $(div.window-toolbar).content(
    ); + } + setWindowButontsAndIcon(); +} + +if (!(is_file_transfer || is_port_forward)) { + $(header).style.set { + height: "32px", + }; + if (!is_osx) { + $(div.window-icon).style.set { + size: "32px", + }; + } +} + +handler.updatePi = function(v) { + pi = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.switchDisplay = function(i) { + pi.current_display = i; + header.update(); +} + +function updateWindowToolbarPosition() { + if (is_osx) return; + self.timer(1ms, function() { + var el = $(div.window-toolbar); + var w1 = el.box(#width, #border); + var w2 = $(header).box(#width, #border); + var x = (w2 - w1) / 2; + el.style.set { + left: x + "px", + display: "block", + }; + }); +} + +view.on("size", function() { + // ensure size is done, so add timer + self.timer(1ms, function() { + updateWindowToolbarPosition(); + adaptDisplay(); + }); +}); + +handler.newMessage = function(text) { + chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()}); + startChat(); +} + +function sendMsg(text) { + chat_msgs.push({text: text, name: "me", time: getNowStr()}); + handler.send_chat(text); + if (chatbox) chatbox.refresh(); +} + +var chatbox; +function startChat() { + if (chatbox) { + chatbox.windowState = View.WINDOW_SHOWN; + chatbox.refresh(); + return; + } + var icon = handler.get_icon(); + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + var w = 300; + var h = 400; + var x = (sx + sw - w) / 2; + var y = sy + 80; + var params = { + type: View.FRAME_WINDOW, + x: x, + y: y, + width: w, + height: h, + client: true, + parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon }, + caption: handler.get_id(), + }; + var html = handler.get_chatbox(); + if (html) params.html = html; + else params.url = self.url("chatbox.html"); + chatbox = view.window(params); +} + +handler.setConnectionType = function(secured, direct) { + header.update({ + secure_connection: secured, + direct_connection: direct, + }); +} diff --git a/src/ui/index.css b/src/ui/index.css new file mode 100644 index 00000000000..5ed7008e78f --- /dev/null +++ b/src/ui/index.css @@ -0,0 +1,261 @@ +html { + background-color: transparent; + var(gray-bg-osx): rgba(238, 238, 238, 0.75); +} + +body { + overflow: hidden; +} + +@media platform != "OSX" { + body { + border-top: color(border) solid 1px; + } +} + +.title { + font-size: 1.4em; +} + +.app { + flow: horizontal; + size: *; +} + +.lighter-text { + color: color(lighter-text); + font-size: 0.9em; +} + +.left-pane { + width: 200px; + height: *; + background: color(bg); + border-right: color(border) 1px solid; +} + +.left-pane > div:nth-child(1) { + border-spacing: 1em; + padding: 20px; +} + +.your-desktop { + border-spacing: 0.5em; + border-left: color(accent) solid 2px; + padding-left: 0.5em; +} + +.your-desktop input[type=text] { + padding: 0; + border: none; + height: 1.5em; +} + +.your-desktop > div { + color: color(light-text); +} + +.right-pane { + size: *; + background: color(gray-bg); +} + +.right-content { + overflow: scroll-indicator; + padding: 1.6em; + border-spacing: 1.6em; + size: *; + flow: vertical; +} + +@media platform == "OSX" { + .right-pane { + background: color(gray-bg-osx); + } +} + +@mixin CARD { + padding: 1.6em; + border-spacing: 1em; + background: color(bg); + border-radius: 1em; +} + +.card-connect { + @CARD; + width: 320px; +} + +.right-buttons { + text-align: right; +} + +.right-buttons>button { + margin-left: 1.6em; +} + +div.connect-status { + left: 240px; + border-top: color(border) solid 1px; + width: 100%; + background: color(gray-bg); + padding: 1em; +} + +div.connect-status > span.connect-status-icon { + border-radius: 4px; + width: 8px; + height: 8px; + display: inline-block; + margin-right: 1em; +} + +div.connect-status > span.link { + margin-left: 1em; + display: inline-block; +} + +span.connect-status-1 { + background: #e04f5f; +} + +span.connect-status1 { + background: #32bea6; +} + +span.connect-status0 { + background: #F5853B; +} + +div.recent-sessions-content { + border-spacing: 1em; + flow: horizontal-flow; +} + +div.recent-sessions-title { + color: color(light-text); + padding-top: 0.5em; + border-top: color(border) solid 1px; + margin-bottom: 1em; +} + +div.remote-session { + border-radius: 1em; + height: 140px; + width: 220px; + padding: 0; + position: relative; + border: none; +} + +div.remote-session:hover { + outline: color(button) solid 2px -2px; +} + +div.remote-session .platform { + width: *; + height: 120px; + padding: *; + position: relative; +} + +div.remote-session .platform .username{ + left: 0; + color: #eee; + position: absolute; + bottom: 38px; + font-size: 0.8em; + width: 220px; + overflow: hidden; + text-align: center; +} + +div.remote-session .platform svg { + width: 60px; + height: 60px; + background: none; +} + +div.remote-session .text { + background: white; + position: absolute; + height: 3em; + width: 100%; + border-radius: 0 0 1em 1em; + bottom: 0; + flow: horizontal; +} + +div.remote-session .text > div { + padding-top: 1em; + padding-left: 1em; + width: *; +} + +svg#menu { + size: 1em; + background: none; + padding: 0.5em; + margin: 0.5em; + color: color(light-text); +} + +svg#menu:active { + color: black; + border-radius: 1em; + background: color(gray-bg); +} + +svg#edit:active { + opacity: 0.5; +} + +svg#edit { + display: inline-block; + margin-top: 0.25em; + margin-bottom: 0; +} + +div.install-me, div.trust-me { + margin-top: 0.5em; + padding: 20px; + color: white; + background: linear-gradient(left,#e242bc,#f4727c); +} + +div.install-me > div:nth-child(1) { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 0.5em; +} + +div.install-me > div:nth-child(2) { + line-height: 1.4em; +} + +div.trust-me > div:nth-child(1) { + font-size: 1.2em; + text-align: center; + font-weight: bold; + margin-bottom: 0.5em; +} + +div.trust-me > div:nth-child(2) { + font-size: 0.9em; + margin-bottom: 1em; +} + +div.trust-me > div:nth-child(3) { + text-align: center; + font-size: 1.5em; + font-weight: bold; +} + +div#myid { + position: relative; +} + +div#myid svg#menu { + position: absolute; + right: -1em; +} diff --git a/src/ui/index.html b/src/ui/index.html new file mode 100644 index 00000000000..638e5d09d70 --- /dev/null +++ b/src/ui/index.html @@ -0,0 +1,30 @@ + + + + + + + +
  • Connect
  • +
  • Transfer File
  • +
  • TCP Tunneling
  • +
  • RDP
  • +
  • Remove
  • + +
    + +
  • Refresh random password
  • +
  • Set your own password
  • +
    + + + + + + \ No newline at end of file diff --git a/src/ui/index.tis b/src/ui/index.tis new file mode 100644 index 00000000000..dd203f91797 --- /dev/null +++ b/src/ui/index.tis @@ -0,0 +1,713 @@ +if (is_osx) view.windowBlurbehind = #light; +stdout.println("current platform:", OS); + +// html min-width, min-height not working on mac, below works for all +view.windowMinSize = (500, 300); + +var app; +var tmp = handler.get_connect_status(); +var connect_status = tmp[0]; +var service_stopped = false; +var software_update_url = ""; +var key_confirmed = tmp[1]; +var system_error = ""; + +var svg_menu = + + + +; + +class ConnectStatus: Reactor.Component { + function render() { + return +
    + + {this.getConnectStatusStr()} + {service_stopped ? Start Service : ""} +
    ; + } + + function getConnectStatusStr() { + if (service_stopped) { + return "Service is not running"; + } else if (connect_status == -1) { + return "Not ready. Please check your connection"; + } else if (connect_status == 0) { + return "Connecting to the RustDesk network..."; + } + return "Ready"; + } + + event click $(.connect-status .link) () { + var options = handler.get_options(); + options["stop-service"] = ""; + handler.set_options(options); + } +} + +class RecentSessions: Reactor.Component { + function render() { + var sessions = handler.get_recent_sessions(); + if (sessions.length == 0) return ; + sessions = sessions.map(this.getSession); + return
    +
    RECENT SESSIONS
    +
    + {sessions} +
    +
    ; + } + + function getSession(s) { + var id = s[0]; + var username = s[1]; + var hostname = s[2]; + var platform = s[3]; + return
    +
    + {platformSvg(platform, "white")} +
    {username}@{hostname}
    +
    +
    +
    {formatId(id)}
    + {svg_menu} +
    +
    ; + } + + event dblclick $(div.remote-session) (evt, me) { + createNewConnect(me.id, "connect"); + } + + event click $(#menu) (_, me) { + var id = me.parent.parent.id; + var platform = me.parent.parent.attributes["platform"]; + $(#rdp).style.set{ + display: (platform == "Windows" && is_win) ? "block" : "none", + }; + // https://sciter.com/forums/topic/replacecustomize-context-menu/ + var menu = $(menu#remote-context); + menu.attributes["remote-id"] = id; + me.popup(menu); + } +} + +event click $(menu#remote-context li) (evt, me) { + var action = me.id; + var id = me.parent.attributes["remote-id"]; + if (action == "connect") { + createNewConnect(id, "connect"); + } else if (action == "transfer") { + createNewConnect(id, "file-transfer"); + } else if (action == "remove") { + handler.remove_peer(id); + app.recent_sessions.update(); + } else if (action == "rdp") { + createNewConnect(id, "rdp"); + } else if (action == "tunnel") { + createNewConnect(id, "port-forward"); + } +} + +function createNewConnect(id, type) { + id = id.replace(/\s/g, ""); + app.remote_id.value = formatId(id); + if (!id) return; + if (id == handler.get_id()) { + handler.msgbox("custom-error", "Error", "Sorry, it is yourself"); + return; + } + handler.set_remote_id(id); + handler.new_remote(id, type); +} + +var myIdMenu; +var audioInputMenu; +var configOptions = {}; +class AudioInputs: Reactor.Component { + function this() { + audioInputMenu = this; + } + + function render() { + if (!this.show) return
  • ; + var inputs = handler.get_sound_inputs(); + if (is_win) inputs = ["System Sound"].concat(inputs); + if (!inputs.length) return
  • ; + inputs = ["Mute"].concat(inputs); + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • Audio Input + + {inputs.map(function(name) { + return
  • {svg_checkmark}{name}
  • ; + })} +
    +
  • ; + } + + function get_default() { + if (is_win) return "System Sound"; + return ""; + } + + function get_value() { + return configOptions["audio-input"] || this.get_default(); + } + + function toggleMenuState() { + var v = this.get_value(); + for (var el in $$(menu#audio-input>li)) { + var selected = el.id == v; + el.attributes.toggleClass("selected", selected); + } + } + + event click $(menu#audio-input>li) (_, me) { + var v = me.id; + if (v == this.get_value()) return; + if (v == this.get_default()) v = ""; + configOptions["audio-input"] = v; + handler.set_options(configOptions); + this.toggleMenuState(); + } +} + +class MyIdMenu: Reactor.Component { + function this() { + myIdMenu = this; + } + + function render() { + var me = this; + return
    + {this.renderPop()} + ID{svg_menu} +
    ; + } + + function renderPop() { + return + +
  • {svg_checkmark}Enable Keyboard/Mouse
  • +
  • {svg_checkmark}Enable Clipboard
  • +
  • {svg_checkmark}Enable File Transfer
  • +
  • {svg_checkmark}Enable TCP Tunneling
  • + +
    +
  • IP Whitelisting
  • +
  • ID/Relay Server
  • +
    +
  • {service_stopped ? "Start service" : "Stop service"}
  • +
    +
  • Forum
  • +
  • About {handler.get_app_name()}
  • + + ; + } + + event click $(svg#menu) (_, me) { + audioInputMenu.update({ show: true }); + configOptions = handler.get_options(); + this.toggleMenuState(); + var menu = $(menu#config-options); + me.popup(menu); + } + + function toggleMenuState() { + for (var el in $$(menu#config-options>li)) { + if (el.id && el.id.indexOf("enable-") == 0) { + var enabled = configOptions[el.id] != "N"; + el.attributes.toggleClass("selected", enabled); + } + } + } + + event click $(menu#config-options>li) (_, me) { + if (me.id && me.id.indexOf("enable-") == 0) { + configOptions[me.id] = configOptions[me.id] == "N" ? "" : "N"; + handler.set_options(configOptions); + this.toggleMenuState(); + } + if (me.id == "whitelist") { + var old_value = (configOptions["whitelist"] || "").split(",").join("\n"); + handler.msgbox("custom-whitelist", "IP Whitelisting", "
    \ + \ +
    \ + ", function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + if (value) { + var values = value.split(/[\s,;]+/g); + for (var ip in values) { + if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) { + return "Invalid ip: " + ip; + } + } + value = values.join("\n"); + } + if (value == old_value) return; + configOptions["whitelist"] = value.replace("\n", ","); + stdin.println("whitelist updated"); + handler.set_options(configOptions); + }, 300); + } else if (me.id == "custom-server") { + var old_relay = configOptions["relay-server"] || ""; + var old_id = configOptions["custom-rendezvous-server"] || ""; + handler.msgbox("custom-server", "ID/Relay Server", "
    \ +
    ID Server:
    \ +
    Relay Server:
    \ +
    \ + ", function(res=null) { + if (!res) return; + var id = (res.id || "").trim(); + var relay = (res.relay || "").trim(); + if (id == old_id && relay == old_relay) return; + if (id) { + var err = handler.test_if_valid_server(id); + if (err) return "ID Server: " + err; + } + if (relay) { + var err = handler.test_if_valid_server(relay); + if (err) return "Relay Server: " + err; + } + configOptions["custom-rendezvous-server"] = id; + configOptions["relay-server"] = relay; + handler.set_options(configOptions); + }); + } else if (me.id == "forum") { + handler.open_url("https:://forum.rustdesk.com"); + } else if (me.id == "stop-service") { + configOptions["stop-service"] = service_stopped ? "" : "Y"; + handler.set_options(configOptions); + } else if (me.id == "about") { + var name = handler.get_app_name(); + handler.msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \ +
    Version: " + handler.get_version() + " \ +
    Privacy Statement
    \ +
    Forum
    \ +
    Copyright © 2020 CarrieZ Studio \ +
    Author: Carrie \ +

    Made with heart in this chaotic world!

    \ +
    \ +
    ", function(el) { + if (el && el.attributes) { + handler.open_url(el.attributes['url']); + }; + }, 400); + } + } +} + +class App: Reactor.Component +{ + function this() { + app = this; + } + + function render() { + var is_can_screen_recording = handler.is_can_screen_recording(false); + return +
    +
    +
    +
    Your Desktop
    +
    Your desktop can be accessed with this ID and password.
    +
    + + {key_confirmed ? : "Generating ..."} +
    +
    +
    Password
    + +
    +
    + {handler.is_installed() ? "": } + {handler.is_installed() && software_update_url ? : ""} + {handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? : ""} + {is_can_screen_recording ? "": } + {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} + {system_error ? : ""} + {!system_error && handler.is_login_wayland() ? : ""} +
    +
    +
    +
    +
    Control Remote Desktop
    + +
    + + +
    +
    + +
    + +
    +
    ; + } + + event click $(button#connect) { + this.newRemote("connect"); + } + + event click $(button#file-transfer) { + this.newRemote("file-transfer"); + } + + function newRemote(type) { + createNewConnect(this.remote_id.value, type); + } +} + +class InstalllMe: Reactor.Component { + function render() { + return
    +
    Install RustDesk
    +
    Install RustDesk on this computer ...
    +
    ; + } + + event click $(#install-me) { + handler.goto_install(); + } +} + +const http = function() { + + function makeRequest(httpverb) { + return function( params ) { + params.type = httpverb; + view.request(params); + }; + } + + function download(from, to, args..) + { + var rqp = { type:#get, url: from, toFile: to }; + var fn = 0; + var on = 0; + for( var p in args ) + if( p instanceof Function ) + { + switch(++fn) { + case 1: rqp.success = p; break; + case 2: rqp.error = p; break; + case 3: rqp.progress = p; break; + } + } else if( p instanceof Object ) + { + switch(++on) { + case 1: rqp.params = p; break; + case 2: rqp.headers = p; break; + } + } + view.request(rqp); + } + + return { + get: makeRequest(#get), + post: makeRequest(#post), + put: makeRequest(#put), + del: makeRequest(#delete), + download: download + }; + +}(); + +class UpgradeMe: Reactor.Component { + function render() { + var update_or_download = is_osx ? "download" : "update"; + return
    +
    {handler.get_app_name()} Status
    +
    Your installation is lower version.
    +
    Click to upgrade
    +
    ; + } + + event click $(#install-me) { + handler.update_me(""); + } +} + +class UpdateMe: Reactor.Component { + function render() { + var update_or_download = is_osx ? "download" : "update"; + return
    +
    {handler.get_app_name()} Status
    +
    There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.
    +
    Click to {update_or_download}
    +
    +
    ; + } + + event click $(#install-me) { + if (is_osx) { + handler.open_url("http://rustdesk.com"); + return; + } + var url = software_update_url + '.' + handler.get_software_ext(); + var path = handler.get_software_store_path(); + var onsuccess = function(md5) { + $(#download-percent).content("Installing ..."); + handler.update_me(path); + }; + var onerror = function(err) { + handler.msgbox("custom-error", "Download Error", "Failed to download"); + }; + var onprogress = function(loaded, total) { + if (!total) total = 5 * 1024 * 1024; + var el = $(#download-percent); + el.style.set{display: "block"}; + el.content("Downloading %" + (loaded * 100 / total)); + }; + stdout.println("Downloading " + url + " to " + path); + http.download( + url, + self.url(path), + onsuccess, onerror, onprogress); + } +} + +class SystemError: Reactor.Component { + function render() { + return
    +
    {system_error}
    +
    ; + } +} + +class TrustMe: Reactor.Component { + function render() { + return
    +
    Configuration Permissions
    +
    In order to control your Desktop remotely, you need to grant RustDesk "Accessibility" permissions
    +
    Configure
    +
    ; + } + + event click $(#trust-me) { + handler.is_process_trusted(true); + watch_trust(); + } +} + +class CanScreenRecording: Reactor.Component { + function render() { + return
    +
    Configuration Permissions
    +
    In order to access your Desktop remotely, you need to grant RustDesk "Screen Recording" permissions
    +
    Configure
    +
    ; + } + + event click $(#screen-recording) { + handler.is_can_screen_recording(true); + watch_trust(); + } +} + +class FixWayland: Reactor.Component { + function render() { + return
    +
    Warning
    +
    Login screen using Wayland is not supported
    +
    Fix it
    +
    ; + } + + event click $(#fix-wayland) { + handler.fix_login_wayland(); + app.update(); + } +} + +function watch_trust() { + // not use TrustMe::update, because it is buggy + var trusted = handler.is_process_trusted(false); + var el = $(div.trust-me); + if (el) { + el.style.set { + display: trusted ? "none" : "block", + }; + } + // if (trusted) return; + self.timer(1s, watch_trust); +} + +class PasswordEyeArea : Reactor.Component { + render() { + return +
    + + {svg_eye} +
    ; + } + + event mouseenter { + var me = this; + me.leaved = false; + me.timer(300ms, function() { + if (me.leaved) return; + me.input.value = handler.get_password(); + }); + } + + event mouseleave { + this.leaved = true; + this.input.value = "******"; + } +} + +class Password: Reactor.Component { + function render() { + return
    + + {svg_edit} +
    ; + } + + event click $(svg#edit) (_, me) { + var menu = $(menu#edit-password-context); + me.popup(menu); + } + + event click $(li#refresh-password) { + handler.update_password(""); + this.update(); + } + + event click $(li#set-password) { + var me = this; + handler.msgbox("custom-password", "Set Password", "
    \ +
    Password:
    \ +
    Confirmation:
    \ +
    \ + ", function(res=null) { + if (!res) return; + var p0 = (res.password || "").trim(); + var p1 = (res.confirmation || "").trim(); + if (p0.length < 6) { + return "Too short, at least 6 characters."; + } + if (p0 != p1) { + return "The confirmation is not identical."; + } + handler.update_password(p0); + me.update(); + }); + } +} + +class ID: Reactor.Component { + function render() { + return ; + } + + // https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm + event change { + var fid = formatId(this.value); + var d = this.value.length - (this.old_value || "").length; + this.old_value = this.value; + var start = this.xcall(#selectionStart) || 0; + var end = this.xcall(#selectionEnd); + if (fid == this.value || d <= 0 || start != end) { + return; + } + // fix Caret position + this.value = fid; + var text_after_caret = this.old_value.substr(start); + var n = fid.length - formatId(text_after_caret).length; + this.xcall(#setSelection, n, n); + } +} + +var reg = /^\d+$/; +function formatId(id) { + id = id.replace(/\s/g, ""); + if (reg.test(id) && id.length > 3) { + var n = id.length; + var a = n % 3 || 3; + var new_id = id.substr(0, a); + for (var i = a; i < n; i += 3) { + new_id += " " + id.substr(i, 3); + } + return new_id; + } + return id; +} + +event keydown (evt) { + if (!evt.shortcutKey) { + if (evt.keyCode == Event.VK_ENTER || + (view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) { + var el = $(button#connect); + view.focus = el; + el.sendEvent("click"); + // simulate button click effect, windows does not have this issue + el.attributes.toggleClass("active", true); + self.timer(0.3s, function() { + el.attributes.toggleClass("active", false); + }); + } + } +} + +$(body).content(); + +function self.closing() { + // return false; // can prevent window close + var (x, y, w, h) = view.box(#rectw, #border, #screen); + handler.save_size(x, y, w, h); +} + +function self.ready() { + var r = handler.get_size(); + if (r[2] == 0) { + centerize(800, 600); + } else { + view.move(r[0], r[1], r[2], r[3]); + } + if (!handler.get_remote_id()) { + view.focus = $(#remote_id); + } +} + +function checkConnectStatus() { + self.timer(1s, function() { + var tmp = !!handler.get_option("stop-service"); + if (tmp != service_stopped) { + service_stopped = tmp; + app.connect_status.update(); + myIdMenu.update(); + } + tmp = handler.get_connect_status(); + if (tmp[0] != connect_status) { + connect_status = tmp[0]; + app.connect_status.update(); + } + if (tmp[1] != key_confirmed) { + key_confirmed = tmp[1]; + app.update(); + } + tmp = handler.get_error(); + if (system_error != tmp) { + system_error = tmp; + app.update(); + } + tmp = handler.get_software_update_url(); + if (tmp != software_update_url) { + software_update_url = tmp; + app.update(); + } + if (handler.recent_sessions_updated()) { + stdout.println("recent sessions updated"); + app.recent_sessions.update(); + } + checkConnectStatus(); + }); +} + +checkConnectStatus(); diff --git a/src/ui/install.html b/src/ui/install.html new file mode 100644 index 00000000000..c86861ba071 --- /dev/null +++ b/src/ui/install.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/ui/install.tis b/src/ui/install.tis new file mode 100644 index 00000000000..99befec1e97 --- /dev/null +++ b/src/ui/install.tis @@ -0,0 +1,45 @@ +function self.ready() { + centerize(800, 600); +} + +class Install: Reactor.Component { + function render() { + return
    +
    Installation
    +
    Installation Path:
    +
    Create start menu shortcuts
    +
    Create desktop icon
    +
    End-user license agreement
    +
    By starting the installation, you accept the license agreement.
    +
    +
    + + + +
    +
    ; + } + + event click $(#cancel) { + view.close(); + } + + event click $(#aggrement) { + view.open_url("http://rustdesk.com/privacy"); + } + + event click $(#submit) { + for (var el in $$(button)) el.state.disabled = true; + $(progress).style.set{ display: "inline-block" }; + var args = ""; + if ($(#startmenu).value) { + args += "startmenu "; + } + if ($(#desktopicon).value) { + args += "desktopicon "; + } + view.install_me(args); + } +} + +$(body).content(); \ No newline at end of file diff --git a/src/ui/macos.rs b/src/ui/macos.rs new file mode 100644 index 00000000000..b722f387529 --- /dev/null +++ b/src/ui/macos.rs @@ -0,0 +1,145 @@ +#[cfg(target_os = "macos")] +use cocoa::{ + appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, + base::{id, nil, YES}, + foundation::{NSAutoreleasePool, NSString}, +}; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Object, Sel, BOOL}, + sel, sel_impl, +}; +use std::{ + ffi::c_void, + sync::{Arc, Mutex}, +}; + +static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; + +lazy_static::lazy_static! { + pub static ref SHOULD_OPEN_UNTITLED_FILE_CALLBACK: Arc>>> = Default::default(); +} + +trait AppHandler { + fn command(&mut self, cmd: u32); +} + +struct DelegateState { + handler: Option>, +} + +impl DelegateState { + fn command(&mut self, command: u32) { + if command == 0 { + unsafe { + let () = msg_send!(NSApp(), terminate: nil); + } + } else if let Some(inner) = self.handler.as_mut() { + inner.command(command) + } + } +} + +// https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs +unsafe fn set_delegate(handler: Option>) { + let mut decl = + ClassDecl::new("AppDelegate", class!(NSObject)).expect("App Delegate definition failed"); + decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR); + + decl.add_method( + sel!(applicationDidFinishLaunching:), + application_did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + + decl.add_method( + sel!(applicationShouldOpenUntitledFile:), + application_should_handle_open_untitled_file as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(handleMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + let decl = decl.register(); + let delegate: id = msg_send![decl, alloc]; + let () = msg_send![delegate, init]; + let state = DelegateState { handler }; + let handler_ptr = Box::into_raw(Box::new(state)); + (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + let () = msg_send![NSApp(), setDelegate: delegate]; +} + +extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) { + unsafe { + let () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; + } +} + +extern "C" fn application_should_handle_open_untitled_file( + _this: &mut Object, + _: Sel, + _sender: id, +) -> BOOL { + if let Some(callback) = SHOULD_OPEN_UNTITLED_FILE_CALLBACK.lock().unwrap().as_ref() { + callback(); + } + YES +} + +/// This handles menu items in the case that all windows are closed. +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let tag: isize = msg_send![item, tag]; + if tag == 0 { + let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); + let inner = &mut *(inner as *mut DelegateState); + (*inner).command(tag as u32); + } else if tag == 1 { + crate::run_me(Vec::::new()).ok(); + } + } +} + +pub fn make_menubar() { + unsafe { + let _pool = NSAutoreleasePool::new(nil); + set_delegate(None); + let menubar = NSMenu::new(nil).autorelease(); + let app_menu_item = NSMenuItem::new(nil).autorelease(); + menubar.addItem_(app_menu_item); + let app_menu = NSMenu::new(nil).autorelease(); + let quit_title = + NSString::alloc(nil).init_str(&format!("Quit {}", hbb_common::config::APP_NAME)); + let quit_action = sel!(handleMenuItem:); + let quit_key = NSString::alloc(nil).init_str("q"); + let quit_item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_(quit_title, quit_action, quit_key) + .autorelease(); + let () = msg_send![quit_item, setTag: 0]; + /* + if !enabled { + let () = msg_send![quit_item, setEnabled: NO]; + } + + if selected { + let () = msg_send![quit_item, setState: 1_isize]; + } + let () = msg_send![item, setTag: id as isize]; + */ + app_menu.addItem_(quit_item); + if std::env::args().len() > 1 { + let new_title = NSString::alloc(nil).init_str("New Window"); + let new_action = sel!(handleMenuItem:); + let new_key = NSString::alloc(nil).init_str("n"); + let new_item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_(new_title, new_action, new_key) + .autorelease(); + let () = msg_send![new_item, setTag: 1]; + app_menu.addItem_(new_item); + } + app_menu_item.setSubmenu_(app_menu); + NSApp().setMainMenu_(menubar); + } +} diff --git a/src/ui/msgbox.html b/src/ui/msgbox.html new file mode 100644 index 00000000000..79fa067b3af --- /dev/null +++ b/src/ui/msgbox.html @@ -0,0 +1,69 @@ + + + + + + + diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis new file mode 100644 index 00000000000..144530dc5a9 --- /dev/null +++ b/src/ui/msgbox.tis @@ -0,0 +1,271 @@ +var type, title, text, getParams, remember, hasRetry, callback; + +function updateParams(params) { + type = params.type; + title = params.title; + text = params.text; + getParams = params.getParams; + remember = params.remember; + callback = params.callback; + hasRetry = type == "error" && + title == "Connection Error" && + text.toLowerCase().indexOf("offline") < 0 && + text.toLowerCase().indexOf("exist") < 0 && + text.toLowerCase().indexOf("handshake") < 0 && + text.toLowerCase().indexOf("failed") < 0 && + text.toLowerCase().indexOf("resolve") < 0 && + text.toLowerCase().indexOf("manually") < 0; + if (hasRetry) { + self.timer(1s, function() { + view.close({ reconnect: true }); + }); + } +} + +var params = view.parameters; +updateParams(params); + +var svg_eye_cross = + + +; + +class Password: Reactor.Component { + this var visible = false; + + function render() { + return
    + + {this.visible ? svg_eye_cross : svg_eye} +
    ; + } + + event click $(svg) { + var el = this.$(input); + var value = el.value; + var start = el.xcall(#selectionStart) || 0; + var end = el.xcall(#selectionEnd); + this.update({ visible: !this.visible }); + self.timer(30ms, function() { + var el = this.$(input); + view.focus = el; + el.value = value; + el.xcall(#setSelection, start, end); + }); + } +} + +var body; + +class Body: Reactor.Component { + function this() { + body = this; + } + + function getIcon(color) { + if (type == "input-password") { + return ; + } + if (type == "connecting") { + return ; + } + if (type == "success") { + return ; + } + if (type.indexOf("error") >= 0 || type == "re-input-password") { + return ; + } + return ; + } + + function getInputPasswordContent() { + var ts = remember ? { checked: true } : {}; + return
    +
    Please enter your password
    + +
    Remember password
    +
    ; + } + + function getContent() { + if (type == "input-password") { + return this.getInputPasswordContent(); + } + return text; + } + + function getColor() { + if (type == "input-password") { + return "#AD448E"; + } + if (type == "success") { + return "#32bea6"; + } + if (type.indexOf("error") >= 0 || type == "re-input-password") { + return "#e04f5f"; + } + return "#2C8CFF"; + } + + function hasSkip() { + return type.indexOf("skip") >= 0; + } + + function render() { + var color = this.getColor(); + var icon = this.getIcon(color); + var content = this.getContent(); + var hasCancel = type.indexOf("error") < 0 && type != "success" && type.indexOf("nocancel") < 0; + var hasOk = type != "connecting" && type.indexOf("nook") < 0; + var hasClose = type.indexOf("hasclose") >= 0; + var show_progress = type == "connecting"; + self.style.set { border: color + " solid 1px" }; + var me = this; + self.timer(1ms, function() { + if (typeof content == "string") + me.$(#content).html = content; + else + me.$(#content).content(content); + }); + return ( +
    +
    + {title} +
    +
    +
    + {icon &&
    {icon}
    } +
    +
    +
    + + {show_progress ? : ""} + {hasCancel || hasRetry ? : ""} + {this.hasSkip() ? : ""} + {hasOk || hasRetry ? : ""} + {hasClose ? : ""} +
    +
    +
    ); + } + + event click $(.custom-event) (_, me) { + if (callback) callback(me); + } +} + +$(body).content(); + +function submit() { + if ($(button#submit)) { + $(button#submit).sendEvent("click"); + } +} + +function cancel() { + if ($(button#cancel)) { + $(button#cancel).sendEvent("click"); + } +} + +event click $(button#cancel) { + view.close(); + if (callback) callback(null); +} + +event click $(button#skip) { + var values = getValues(); + values.skip = true; + view.close(values); + if (callback) callback(values); +} + +function getValues() { + var values = { type: type }; + for (var el in $$(.form input)) { + values[el.attributes["name"]] = el.value; + } + for (var el in $$(.form textarea)) { + values[el.attributes["name"]] = el.value; + } + for (var el in $$(.form button)) { + values[el.attributes["name"]] = el.value; + } + if (type == "input-password") { + values.password = (values.password || "").trim(); + if (!values.password) { + return; + } + } + return values; +} + +event click $(button#submit) { + if (type == "error") { + if (hasRetry) { + view.close({ reconnect: true }); + } else { + view.close(); + if (callback) callback(null); + } + return; + } + if (type == "re-input-password") { + type = "input-password"; + body.update(); + set_outline_focus(); + return; + } + var values = getValues(); + if (callback) { + var err = callback(values); + if (err) { + $(#error).text = err; + return; + } + } + view.close(values); +} + +event keydown (evt) { + if (!evt.shortcutKey) { + if (evt.keyCode == Event.VK_ENTER || + (view.mediaVar("platform") == "OSX" && evt.keyCode == 0x4C)) { + submit(); + } + if (evt.keyCode == Event.VK_ESCAPE) { + cancel(); + } + } +} + +function set_outline_focus() { + self.timer(30ms, function() { + var el = $(input.outline-focus); + if (el) view.focus = el; + else { + el = $(#submit); + if (el) view.focus = el; + } + }); +} + +set_outline_focus(); + +function checkParams() { + self.timer(30ms, function() { + var tmp = getParams(); + if (!tmp || !tmp.type) { + view.close("!alive"); + return; + } else if (tmp != params) { + params = tmp; + updateParams(params); + body.update(); + set_outline_focus(); + } + checkParams(); + }); +} + +checkParams(); diff --git a/src/ui/port_forward.tis b/src/ui/port_forward.tis new file mode 100644 index 00000000000..43f25c89c6b --- /dev/null +++ b/src/ui/port_forward.tis @@ -0,0 +1,77 @@ +class PortForward: Reactor.Component { + function render() { + var args = handler.get_args(); + var is_rdp = handler.is_rdp(); + if (is_rdp) { + this.pfs = [["", "", "RDP"]]; + args = ["rdp"]; + } else if (args.length) { + this.pfs = [args]; + } else { + this.pfs = handler.get_port_forwards(); + } + var pfs = this.pfs.map(function(pf, i) { + return + {is_rdp ? : pf[0]} + {args.length ? svg_arrow : ""} + {pf[1] || "localhost"} + {pf[2]} + {args.length ? "" : {svg_cancel}} + ; + }); + return
    + {pfs.length ?
    + Listenning ...
    + Don't close this window while your are using tunnel +
    : ""} + + + + + + + {args.length ? "" : } + + + + {args.length ? "" : + + + + + + + + } + {pfs} + +
    Local Port + Remote HostRemote PortAction
    {svg_arrow}
    ; + } + + event click $(#add) () { + var port = ($(#port).value || "").toInteger() || 0; + var remote_host = $(#remote-host).value || ""; + var remote_port = ($(#remote-port).value || "").toInteger() || 0; + if (port <= 0 || remote_port <= 0) return; + handler.add_port_forward(port, remote_host, remote_port); + this.update(); + } + + event click $(#new-rdp) { + handler.new_rdp(); + } + + event click $(.remove svg) (_, me) { + var pf = this.pfs[me.parent.parent.index - 1]; + handler.remove_port_forward(pf[0]); + this.update(); + } +} + +function initializePortForward() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} diff --git a/src/ui/remote.css b/src/ui/remote.css new file mode 100644 index 00000000000..8ceab6791cf --- /dev/null +++ b/src/ui/remote.css @@ -0,0 +1,37 @@ +body { + margin: 0; + color: black; + overflow: scroll-indicator; +} + +div#video-wrapper { + size: *; + background: #212121; +} + +video#handler { + behavior: native-remote video; + size: *; + margin: *; + foreground-size: contain; + position: relative; +} + +img#cursor { + position: absolute; + display: none; + //opacity: 0.66, + //transform: scale(0.8); +} + +.goup { + transform: rotate(90deg); +} + +table#remote-folder-view { + context-menu: selector(menu#remote-folder-view); +} + +table#local-folder-view { + context-menu: selector(menu#local-folder-view); +} \ No newline at end of file diff --git a/src/ui/remote.html b/src/ui/remote.html new file mode 100644 index 00000000000..ebc03ccbec6 --- /dev/null +++ b/src/ui/remote.html @@ -0,0 +1,33 @@ + + + + + +
    +
    + + + +
    + +
    + +
    +
    +
    + + diff --git a/src/ui/remote.rs b/src/ui/remote.rs new file mode 100644 index 00000000000..b97a37650ab --- /dev/null +++ b/src/ui/remote.rs @@ -0,0 +1,1660 @@ +use crate::client::*; +use crate::common::{ + self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, +}; +use enigo::{self, Enigo, KeyboardControllable}; +use hbb_common::{ + allow_err, + config::{self, Config, PeerConfig}, + fs, log, + message_proto::*, + protobuf::Message as _, + tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, + }, + Stream, +}; +use sciter::{ + dom::{ + event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, + Element, HELEMENT, + }, + make_args, + video::{video_destination, AssetPtr, COLOR_SPACE}, + Value, +}; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, Mutex, RwLock}, +}; + +type Video = AssetPtr; + +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); + static ref VIDEO: Arc>> = Default::default(); +} + +fn get_key_state(key: enigo::Key) -> bool { + #[cfg(target_os = "macos")] + if key == enigo::Key::NumLock { + return true; + } + ENIGO.lock().unwrap().get_key_state(key) +} + +#[derive(Default)] +pub struct HandlerInner { + element: Option, + sender: Option>, + thread: Option>, + close_state: HashMap, + last_down_key: Option<(String, i32, bool)>, +} + +#[derive(Clone, Default)] +pub struct Handler { + inner: Arc>, + cmd: String, + id: String, + args: Vec, + lc: Arc>, +} + +impl Deref for Handler { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl sciter::EventHandler for Handler { + fn get_subscription(&mut self) -> Option { + Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) + } + + fn attached(&mut self, root: HELEMENT) { + self.write().unwrap().element = Some(Element::from(root)); + } + + fn detached(&mut self, _root: HELEMENT) { + self.write().unwrap().element = None; + self.write().unwrap().sender.take().map(|sender| { + sender.send(Data::Close).ok(); + }); + } + + // https://github.com/sciter-sdk/rust-sciter/blob/master/examples/video.rs + fn on_event( + &mut self, + _root: HELEMENT, + source: HELEMENT, + _target: HELEMENT, + code: BEHAVIOR_EVENTS, + phase: PHASE_MASK, + reason: EventReason, + ) -> bool { + if phase != PHASE_MASK::BUBBLING { + return false; + } + match code { + BEHAVIOR_EVENTS::VIDEO_BIND_RQ => { + let source = Element::from(source); + log::debug!("[video] {:?} {} ({:?})", code, source, reason); + if let EventReason::VideoBind(ptr) = reason { + if ptr.is_null() { + return true; + } + let site = AssetPtr::adopt(ptr as *mut video_destination); + log::debug!("[video] start video"); + *VIDEO.lock().unwrap() = Some(site); + self.reconnect(); + } + } + BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { + log::debug!("[video] {:?}", code); + } + BEHAVIOR_EVENTS::VIDEO_STARTED => { + log::debug!("[video] {:?}", code); + let source = Element::from(source); + use sciter::dom::ELEMENT_AREAS; + let flags = ELEMENT_AREAS::CONTENT_BOX as u32 | ELEMENT_AREAS::SELF_RELATIVE as u32; + let rc = source.get_location(flags).unwrap(); + log::debug!( + "[video] start video thread on <{}> which is about {:?} pixels", + source, + rc.size() + ); + } + BEHAVIOR_EVENTS::VIDEO_STOPPED => { + log::debug!("[video] {:?}", code); + } + _ => return false, + }; + return true; + } + + sciter::dispatch_script_call! { + fn get_id(); + fn get_default_pi(); + fn get_option(String); + fn save_close_state(String, String); + fn is_file_transfer(); + fn is_port_forward(); + fn is_rdp(); + fn login(String, bool); + fn new_rdp(); + fn send_mouse(i32, i32, i32, bool, bool, bool, bool); + fn key_down_or_up(bool, String, i32, bool, bool, bool, bool, bool); + fn ctrl_alt_del(); + fn transfer_file(); + fn tunnel(); + fn lock_screen(); + fn reconnect(); + fn get_msgbox(); + fn get_chatbox(); + fn get_icon(); + fn get_home_dir(); + fn read_dir(String, bool); + fn remove_dir(i32, String, bool); + fn create_dir(i32, String, bool); + fn remove_file(i32, String, i32, bool); + fn read_remote_dir(String, bool); + fn send_chat(String); + fn switch_display(i32); + fn remove_dir_all(i32, String, bool); + fn confirm_delete_files(i32, i32); + fn set_no_confirm(i32); + fn cancel_job(i32); + fn send_files(i32, String, String, bool, bool); + fn get_platform(bool); + fn get_path_sep(bool); + fn get_icon_path(i32, String); + fn get_char(String, i32); + fn get_size(); + fn get_port_forwards(); + fn remove_port_forward(i32); + fn get_args(); + fn add_port_forward(i32, String, i32); + fn save_size(i32, i32, i32, i32); + fn get_view_style(); + fn get_image_quality(); + fn get_custom_image_quality(); + fn save_view_style(String); + fn save_image_quality(String); + fn save_custom_image_quality(i32, i32); + fn refresh_video(); + fn support_refresh(); + fn get_toggle_option(String); + fn toggle_option(String); + fn get_remember(); + } +} + +impl Handler { + pub fn new(cmd: String, id: String, args: Vec) -> Self { + let me = Self { + cmd, + id: id.clone(), + args, + ..Default::default() + }; + me.lc + .write() + .unwrap() + .initialize(id, me.is_file_transfer(), me.is_port_forward()); + me + } + + fn get_view_style(&mut self) -> String { + return self.lc.read().unwrap().view_style.clone(); + } + + fn get_image_quality(&mut self) -> String { + return self.lc.read().unwrap().image_quality.clone(); + } + + fn get_custom_image_quality(&mut self) -> Value { + let mut v = Value::array(0); + for x in self.lc.read().unwrap().custom_image_quality.iter() { + v.push(x); + } + v + } + + #[inline] + fn save_config(&self, config: PeerConfig) { + self.lc.write().unwrap().save_config(config); + } + + fn save_view_style(&mut self, value: String) { + self.lc.write().unwrap().save_view_style(value); + } + + #[inline] + fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + fn toggle_option(&mut self, name: String) { + let msg = self.lc.write().unwrap().toggle_option(name); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + fn get_toggle_option(&mut self, name: String) -> bool { + self.lc.read().unwrap().get_toggle_option(&name) + } + + fn refresh_video(&mut self) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + + fn support_refresh(&self) -> bool { + self.lc.read().unwrap().support_refresh + } + + fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) { + let msg = self + .lc + .write() + .unwrap() + .save_custom_image_quality(bitrate, quantizer); + self.send(Data::Message(msg)); + } + + fn save_image_quality(&mut self, value: String) { + let msg = self.lc.write().unwrap().save_image_quality(value); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + fn get_remember(&mut self) -> bool { + self.lc.read().unwrap().remember + } + + fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { + let size = (x, y, w, h); + let mut config = self.load_config(); + if self.is_file_transfer() { + let close_state = self.read().unwrap().close_state.clone(); + let mut has_change = false; + for (k, v) in close_state { + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.options.get(&k) { + has_change = true; + if v2.is_none() { + config.options.remove(&k); + } else { + config.options.insert(k, v); + } + } + } + if size == config.size_ft && !has_change { + return; + } + config.size_ft = size; + } else if self.is_port_forward() { + if size == config.size_pf { + return; + } + config.size_pf = size; + } else { + if size == config.size { + return; + } + config.size = size; + } + self.save_config(config); + log::info!("size saved"); + } + + fn get_port_forwards(&mut self) -> Value { + let port_forwards = self.lc.read().unwrap().port_forwards.clone(); + let mut v = Value::array(0); + for (port, remote_host, remote_port) in port_forwards { + let mut v2 = Value::array(0); + v2.push(port); + v2.push(remote_host); + v2.push(remote_port); + v.push(v2); + } + v + } + + fn get_args(&mut self) -> Value { + let mut v = Value::array(0); + for x in self.args.iter() { + v.push(x); + } + v + } + + fn remove_port_forward(&mut self, port: i32) { + let mut config = self.load_config(); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != port) + .collect(); + self.save_config(config); + self.send(Data::RemovePortForward(port)); + } + + fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + let mut config = self.load_config(); + if config + .port_forwards + .iter() + .filter(|x| x.0 == port) + .next() + .is_some() + { + return; + } + let pf = (port, remote_host, remote_port); + config.port_forwards.push(pf.clone()); + self.save_config(config); + self.send(Data::AddPortForward(pf)); + } + + fn get_size(&mut self) -> Value { + let s = if self.is_file_transfer() { + self.lc.read().unwrap().size_ft + } else if self.is_port_forward() { + self.lc.read().unwrap().size_pf + } else { + self.lc.read().unwrap().size + }; + let mut v = Value::array(0); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + v + } + + fn get_id(&mut self) -> String { + self.id.clone() + } + + fn get_default_pi(&mut self) -> Value { + let mut pi = Value::map(); + let info = self.lc.read().unwrap().info.clone(); + pi.set_item("username", info.username.clone()); + pi.set_item("hostname", info.hostname.clone()); + pi.set_item("platform", info.platform.clone()); + pi + } + + fn get_option(&self, k: String) -> String { + self.lc.read().unwrap().get_option(&k) + } + + fn save_close_state(&self, k: String, v: String) { + self.write().unwrap().close_state.insert(k, v); + } + + fn get_msgbox(&mut self) -> String { + super::get_msgbox() + } + + fn get_chatbox(&mut self) -> String { + #[cfg(feature = "inline")] + return super::inline::get_chatbox(); + #[cfg(not(feature = "inline"))] + return "".to_owned(); + } + + fn get_icon(&mut self) -> String { + config::ICON.to_owned() + } + + fn get_home_dir(&mut self) -> String { + fs::get_home_as_string() + } + + fn read_dir(&mut self, path: String, include_hidden: bool) -> Value { + match fs::read_dir(&fs::get_path(&path), include_hidden) { + Err(_) => Value::null(), + Ok(fd) => { + let mut m = make_fd(0, &fd.entries.to_vec(), false); + m.set_item("path", path); + m + } + } + } + + fn cancel_job(&mut self, id: i32) { + self.send(Data::CancelJob(id)); + } + + fn read_remote_dir(&mut self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_dir(ReadDir { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + + fn send_chat(&mut self, text: String) { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + fn switch_display(&mut self, display: i32) { + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + fn remove_file(&mut self, id: i32, path: String, file_num: i32, is_remote: bool) { + self.send(Data::RemoveFile((id, path, file_num, is_remote))); + } + + fn remove_dir_all(&mut self, id: i32, path: String, is_remote: bool) { + self.send(Data::RemoveDirAll((id, path, is_remote))); + } + + fn confirm_delete_files(&mut self, id: i32, file_num: i32) { + self.send(Data::ConfirmDeleteFiles((id, file_num))); + } + + fn set_no_confirm(&mut self, id: i32) { + self.send(Data::SetNoConfirm(id)); + } + fn remove_dir(&mut self, id: i32, path: String, is_remote: bool) { + if is_remote { + self.send(Data::RemoveDir((id, path))); + } else { + fs::remove_all_empty_dir(&fs::get_path(&path)).ok(); + } + } + + fn create_dir(&mut self, id: i32, path: String, is_remote: bool) { + self.send(Data::CreateDir((id, path, is_remote))); + } + + fn send_files( + &mut self, + id: i32, + path: String, + to: String, + include_hidden: bool, + is_remote: bool, + ) { + self.send(Data::SendFiles((id, path, to, include_hidden, is_remote))); + } + + fn is_file_transfer(&self) -> bool { + self.cmd == "--file-transfer" + } + + fn is_port_forward(&self) -> bool { + self.cmd == "--port-forward" || self.is_rdp() + } + + fn is_rdp(&self) -> bool { + self.cmd == "--rdp" + } + + fn reconnect(&mut self) { + let cloned = self.clone(); + let mut lock = self.write().unwrap(); + lock.last_down_key.take(); + lock.thread.take().map(|t| t.join()); + lock.thread = Some(std::thread::spawn(move || { + io_loop(cloned); + })); + } + + #[inline] + fn peer_platform(&self) -> String { + self.lc.read().unwrap().info.platform.clone() + } + + fn get_platform(&mut self, is_remote: bool) -> String { + if is_remote { + self.peer_platform() + } else { + whoami::platform().to_string() + } + } + + fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + let p = self.get_platform(is_remote); + if &p == "Windows" { + return "\\"; + } else { + return "/"; + } + } + + fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { + let mut path = Config::icon_path(); + if file_type == FileType::DirLink as i32 { + let new_path = path.join("dir_link"); + if !std::fs::metadata(&new_path).is_ok() { + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::File as i32 { + if !ext.is_empty() { + path = path.join(format!("file.{}", ext)); + } else { + path = path.join("file"); + } + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + } else if file_type == FileType::FileLink as i32 { + let new_path = path.join("file_link"); + if !std::fs::metadata(&new_path).is_ok() { + path = path.join("file"); + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::DirDrive as i32 { + if cfg!(windows) { + path = fs::get_path("C:"); + } else if cfg!(target_os = "macos") { + if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { + for entry in entries { + if let Ok(entry) = entry { + path = entry.path(); + break; + } + } + } + } + } + fs::get_string(&path) + } + + #[inline] + fn send(&mut self, data: Data) { + if let Some(ref sender) = self.read().unwrap().sender { + sender.send(data).ok(); + } + } + + fn login(&mut self, password: String, remember: bool) { + self.send(Data::Login((password, remember))); + } + + fn new_rdp(&mut self) { + self.send(Data::NewRDP); + } + + fn send_mouse( + &mut self, + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let mut msg_out = Message::new(); + let mut mouse_event = MouseEvent { + mask, + x, + y, + ..Default::default() + }; + if alt { + mouse_event.modifiers.push(ControlKey::Alt.into()); + } + if shift { + mouse_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl { + mouse_event.modifiers.push(ControlKey::Control.into()); + } + if command { + mouse_event.modifiers.push(ControlKey::Meta.into()); + } + msg_out.set_mouse_event(mouse_event); + self.send(Data::Message(msg_out)); + // on macos, ctrl + left = right, up wont emit, so we need to + // emit up myself if peer is not macos + // to-do: how about ctrl + left from win to macos + if cfg!(target_os = "macos") { + let buttons = mask >> 3; + let evt_type = mask & 0x7; + if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { + self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); + return; + } + } + } + + fn set_cursor_data(&mut self, cd: CursorData) { + let colors = hbb_common::compress::decompress(&cd.colors); + let mut png = Vec::new(); + if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { + self.call( + "setCursorData", + &make_args!( + cd.id.to_string(), + cd.hotx, + cd.hoty, + cd.width, + cd.height, + &png[..] + ), + ); + } + } + + fn get_key_event(&self, down_or_up: i32, name: &str, code: i32) -> Option { + let mut key_event = KeyEvent::new(); + if down_or_up == 2 { + /* windows send both keyup/keydown and keychar, so here we avoid keychar + for <= 0xFF, best practise should only avoid those not on keyboard, but + for now, we have no way to test, so avoid <= 0xFF totaly + */ + if code <= 0xFF { + return None; + } + key_event.set_unicode(code.clone() as _); + } else if let Some(key) = KEY_MAP.get(name) { + match key { + Key::Chr(chr) => { + key_event.set_chr(chr.clone()); + } + Key::ControlKey(key) => { + key_event.set_control_key(key.clone()); + } + _ => {} + } + } else { + if cfg!(target_os = "macos") { + match code { + 0x4C => key_event.set_control_key(ControlKey::NumpadEnter), // numpad enter + 0x69 => key_event.set_control_key(ControlKey::Snapshot), + 0x72 => key_event.set_control_key(ControlKey::Help), + 0x47 => { + key_event.set_control_key(if self.peer_platform() == "Mac OS" { + ControlKey::Clear + } else { + ControlKey::NumLock + }); + } + 0x51 => key_event.set_control_key(ControlKey::Equals), + 0x2F => key_event.set_chr('.' as _), + 0x32 => key_event.set_chr('`' as _), + _ => { + log::error!("Unknown key code {}", code); + return None; + } + } + } else if cfg!(windows) { + match code { + 0x2C => key_event.set_control_key(ControlKey::Snapshot), + 0x91 => key_event.set_control_key(ControlKey::Scroll), + 0x90 => key_event.set_control_key(ControlKey::NumLock), + 0x5C => key_event.set_control_key(ControlKey::RWin), + 0x5D => key_event.set_control_key(ControlKey::Apps), + 0xBE => key_event.set_chr('.' as _), + 0xC0 => key_event.set_chr('`' as _), + _ => { + log::error!("Unknown key code {}", code); + return None; + } + } + } else { + log::error!("Unknown key code {}", code); + return None; + } + } + Some(key_event) + } + + fn get_char(&mut self, name: String, code: i32) -> String { + if let Some(key_event) = self.get_key_event(1, &name, code) { + match key_event.union { + Some(key_event::Union::chr(chr)) => { + if let Some(chr) = std::char::from_u32(chr as _) { + return chr.to_string(); + } + } + _ => {} + } + } + "".to_owned() + } + + fn ctrl_alt_del(&mut self) { + if self.peer_platform() == "Windows" { + let del = "CTRL_ALT_DEL".to_owned(); + self.key_down_or_up(1, del, 0, false, false, false, false, false); + } else { + let del = "VK_DELETE".to_owned(); + self.key_down_or_up(1, del.clone(), 0, true, true, false, false, false); + self.key_down_or_up(0, del, 0, true, true, false, false, false); + } + } + + fn lock_screen(&mut self) { + let lock = "LOCK_SCREEN".to_owned(); + self.key_down_or_up(1, lock, 0, false, false, false, false, false); + } + + fn transfer_file(&mut self) { + let id = self.get_id(); + let args = vec!["--file-transfer", &id]; + if let Err(err) = crate::run_me(args) { + log::error!("Failed to spawn file transfer: {}", err); + } + } + + fn tunnel(&mut self) { + let id = self.get_id(); + let args = vec!["--port-forward", &id]; + if let Err(err) = crate::run_me(args) { + log::error!("Failed to spawn IP tunneling: {}", err); + } + } + + fn key_down_or_up( + &mut self, + down_or_up: i32, + name: String, + code: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + extended: bool, + ) { + // extended: e.g. ctrl key on right side, https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-keybd_event + // not found api of osx and xdo + log::debug!( + "{:?} {} {} {} {} {} {} {} {}", + std::time::SystemTime::now(), + down_or_up, + name, + code, + alt, + ctrl, + shift, + command, + extended, + ); + + if let Some(mut key_event) = self.get_key_event(down_or_up, &name, code) { + // Linux has different repeated key down handling from mac and windows + /* // below cause hang some time, not find why, so disable. so shift + repeat char not work for mac->linux, win->linux works fine, linux->linux not test yet + if self.peer_platform() == "Linux" { + if down_or_up == 1 { + let mut is_repeat = false; + if let Some(x) = self.read().unwrap().last_down_key.as_ref() { + is_repeat = x.0 == name && x.1 == code && x.2 == extended; + } + if is_repeat { + self.key_down_or_up( + 0, + name.clone(), + code, + alt, + ctrl, + shift, + command, + extended, + ); + log::debug!("Repeated key down"); + } + self.write().unwrap().last_down_key = Some((name, code, extended)); + } else { + self.write().unwrap().last_down_key.take(); + } + } + */ + if alt && !crate::is_control_key(&key_event, &ControlKey::Alt) { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift && !crate::is_control_key(&key_event, &ControlKey::Shift) { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl && !crate::is_control_key(&key_event, &ControlKey::Control) { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command && !crate::is_control_key(&key_event, &ControlKey::Meta) { + key_event.modifiers.push(ControlKey::Meta.into()); + } + if crate::is_control_key(&key_event, &ControlKey::CapsLock) { + return; + } else if get_key_state(enigo::Key::CapsLock) && common::valid_for_capslock(&key_event) + { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if self.peer_platform() != "Mac OS" { + if crate::is_control_key(&key_event, &ControlKey::NumLock) { + return; + } else if get_key_state(enigo::Key::NumLock) + && common::valid_for_numlock(&key_event) + { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + } + if down_or_up == 1 { + key_event.down = true; + } else if down_or_up == 3 { + key_event.press = true; + } + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + log::debug!("{:?}", msg_out); + self.send(Data::Message(msg_out)); + } + } + + #[inline] + fn set_cursor_id(&mut self, id: String) { + self.call("setCursorId", &make_args!(id)); + } + + #[inline] + fn set_cursor_position(&mut self, cd: CursorPosition) { + self.call("setCursorPosition", &make_args!(cd.x, cd.y)); + } + + #[inline] + fn call(&self, func: &str, args: &[Value]) { + let r = self.read().unwrap(); + if let Some(ref e) = r.element { + allow_err!(e.call_method(func, args)); + } + } + + #[inline] + fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { + self.call("setDisplay", &make_args!(x, y, w, h)); + } +} + +const MILLI1: Duration = Duration::from_millis(1); + +async fn start_one_port_forward( + handler: Handler, + port: i32, + remote_host: String, + remote_port: i32, + receiver: mpsc::UnboundedReceiver, +) { + handler.lc.write().unwrap().port_forward = (remote_host, remote_port); + if let Err(err) = + crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await + { + handler.on_error(&format!("Failed to listen on {}: {}", port, err)); + } + log::info!("port forward (:{}) exit", port); +} + +#[tokio::main(basic_scheduler)] +async fn io_loop(handler: Handler) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + handler.write().unwrap().sender = Some(sender.clone()); + if handler.is_port_forward() { + if handler.is_rdp() { + start_one_port_forward(handler, 0, "".to_owned(), 3389, receiver).await; + } else if handler.args.len() == 0 { + let pfs = handler.lc.read().unwrap().port_forwards.clone(); + let mut queues = HashMap::>::new(); + for d in pfs { + sender.send(Data::AddPortForward(d)).ok(); + } + loop { + match receiver.recv().await { + Some(Data::AddPortForward((port, remote_host, remote_port))) => { + if port <= 0 || remote_port <= 0 { + continue; + } + let (sender, receiver) = mpsc::unbounded_channel::(); + queues.insert(port, sender); + let handler = handler.clone(); + tokio::spawn(async move { + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + ) + .await; + }); + } + Some(Data::RemovePortForward(port)) => { + if let Some(s) = queues.remove(&port) { + s.send(Data::Close).ok(); + } + } + Some(Data::Close) => { + break; + } + Some(d) => { + for (_, s) in queues.iter() { + s.send(d.clone()).ok(); + } + } + _ => {} + } + } + } else { + let port = handler.args[0].parse::().unwrap_or(0); + if handler.args.len() != 3 + || handler.args[2].parse::().unwrap_or(0) <= 0 + || port <= 0 + { + handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); + } + let remote_host = handler.args[1].clone(); + let remote_port = handler.args[2].parse::().unwrap_or(0); + start_one_port_forward(handler, port, remote_host, remote_port, receiver).await; + } + return; + } + let mut remote = Remote { + handler, + video_handler: VideoHandler::new(), + audio_handler: Default::default(), + receiver, + sender, + old_clipboard: Default::default(), + read_jobs: Vec::new(), + write_jobs: Vec::new(), + remove_jobs: Default::default(), + timer: time::interval(SEC30), + last_update_jobs_status: (Instant::now(), Default::default()), + clipboard: Arc::new(RwLock::new(true)), + keyboard: Arc::new(RwLock::new(true)), + first_frame: false, + }; + remote.io_loop().await; +} + +struct RemoveJob { + files: Vec, + path: String, + sep: &'static str, + is_remote: bool, + no_confirm: bool, + last_update_job_status: Instant, +} + +impl RemoveJob { + fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { + Self { + files, + path, + sep, + is_remote, + no_confirm: false, + last_update_job_status: Instant::now(), + } + } +} + +struct Remote { + handler: Handler, + audio_handler: AudioHandler, + video_handler: VideoHandler, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + old_clipboard: Arc>, + read_jobs: Vec, + write_jobs: Vec, + remove_jobs: HashMap, + timer: Interval, + last_update_jobs_status: (Instant, HashMap), + clipboard: Arc>, + keyboard: Arc>, + first_frame: bool, +} + +impl Remote { + async fn io_loop(&mut self) { + let stop_clipboard = self.start_clipboard(); + let mut last_recv_time = Instant::now(); + match Client::start(&self.handler.id).await { + Ok((mut peer, direct)) => { + self.handler + .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + log::error!("Connection closed: {}", err); + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + if !self.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + log::info!("Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer"); + break; + } + } + d = self.receiver.recv() => { + if let Some(d) = d { + if !self.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + self.handler.msgbox("error", "Connection Error", "Timeout"); + break; + } + if !self.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + self.update_jobs_status(); + } else { + self.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + } + } + log::debug!("Exit io_loop of id={}", self.handler.id); + } + Err(err) => { + self.handler + .msgbox("error", "Connection Error", &err.to_string()); + } + } + if let Some(stop) = stop_clipboard { + stop.send(()).ok(); + } + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(job) = self.remove_jobs.get_mut(&id) { + if job.no_confirm { + let file_num = (file_num + 1) as usize; + if file_num < job.files.len() { + let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); + self.sender + .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) + .ok(); + let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; + if elapsed >= 1000 { + job.last_update_job_status = Instant::now(); + } else { + return; + } + } else { + self.remove_jobs.remove(&id); + } + } + } + if let Some(err) = err { + self.handler + .call("jobError", &make_args!(id, err, file_num)); + } else { + self.handler.call("jobDone", &make_args!(id, file_num)); + } + } + + fn start_clipboard(&mut self) -> Option> { + if self.handler.is_file_transfer() { + return None; + } + let (tx, rx) = std::sync::mpsc::channel(); + let old_clipboard = self.old_clipboard.clone(); + let tx_protobuf = self.sender.clone(); + let clipboard = self.clipboard.clone(); + let keyboard = self.keyboard.clone(); + let lc = self.handler.lc.clone(); + match ClipboardContext::new() { + Ok(mut ctx) => { + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&old_clipboard)); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit clipboard service of client"); + break; + } + _ => {} + } + if !*clipboard.read().unwrap() + || !*keyboard.read().unwrap() + || lc.read().unwrap().disable_clipboard + { + continue; + } + if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { + tx_protobuf.send(Data::Message(msg)).ok(); + } + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + Some(tx) + } + + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { + match data { + Data::Close => { + return false; + } + Data::Login((password, remember)) => { + self.handler + .handle_login_from_ui(password, remember, peer) + .await; + } + Data::Message(msg) => { + allow_err!(peer.send(&msg).await); + } + Data::SendFiles((id, path, to, include_hidden, is_remote)) => { + if is_remote { + log::debug!("New job {}, write to {} from remote {}", id, to, path); + self.write_jobs + .push(fs::TransferJob::new_write(id, to, Vec::new())); + allow_err!(peer.send(&fs::new_send(id, path, include_hidden)).await); + } else { + match fs::TransferJob::new_read(id, path.clone(), include_hidden) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(job) => { + log::debug!( + "New job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + let m = make_fd(job.id(), job.files(), true); + self.handler.call("updateFolderFiles", &make_args!(m)); + let files = job.files().clone(); + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + allow_err!(peer.send(&fs::new_receive(id, to, files)).await); + } + } + } + } + Data::SetNoConfirm(id) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + job.no_confirm = true; + } + } + Data::ConfirmDeleteFiles((id, file_num)) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + let i = file_num as usize; + if i < job.files.len() { + self.handler.call( + "confirmDeleteFiles", + &make_args!(id, file_num, job.files[i].name.clone()), + ); + } + } + } + Data::RemoveDirAll((id, path, is_remote)) => { + let sep = self.handler.get_path_sep(is_remote); + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_all_files(ReadAllFiles { + id, + path: path.clone(), + include_hidden: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + self.remove_jobs + .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); + } else { + match fs::get_recursive_files(&path, true) { + Ok(entries) => { + let m = make_fd(id, &entries, true); + self.handler.call("updateFolderFiles", &make_args!(m)); + self.remove_jobs + .insert(id, RemoveJob::new(entries, path, sep, is_remote)); + } + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + } + } + } + Data::CancelJob(id) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id: id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.remove_download_file(); + fs::remove_job(id, &mut self.write_jobs); + } + fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + Data::RemoveDir((id, path)) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_dir(FileRemoveDir { + id, + path, + recursive: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } + Data::RemoveFile((id, path, file_num, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_file(FileRemoveFile { + id, + path, + file_num, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::remove_file(&path) { + Err(err) => { + self.handle_job_status(id, file_num, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, file_num, None); + } + } + } + } + Data::CreateDir((id, path, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_create(FileDirCreate { + id, + path, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::create_dir(&path) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, -1, None); + } + } + } + } + _ => {} + } + true + } + + #[inline] + fn update_job_status( + job: &fs::TransferJob, + elapsed: i32, + last_update_jobs_status: &mut (Instant, HashMap), + handler: &mut Handler, + ) { + if elapsed <= 0 { + return; + } + let transfered = job.transfered(); + let last_transfered = { + if let Some(v) = last_update_jobs_status.1.get(&job.id()) { + v.to_owned() + } else { + 0 + } + }; + last_update_jobs_status.1.insert(job.id(), transfered); + let speed = (transfered - last_transfered) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + handler.call( + "jobProgress", + &make_args!(job.id(), file_num, speed, job.finished_size() as f64), + ); + } + + fn update_jobs_status(&mut self) { + let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; + if elapsed >= 1000 { + for job in self.read_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { + if let Ok(msg_in) = Message::parse_from_bytes(&data) { + match msg_in.union { + Some(message::Union::video_frame(vf)) => { + if !self.first_frame { + self.first_frame = true; + self.handler.call("closeSuccess", &make_args!()); + self.handler.call("adaptSize", &make_args!()); + } + if let Some(video_frame::Union::vp9s(vp9s)) = &vf.union { + if let Ok(true) = self.video_handler.handle_vp9s(vp9s) { + VIDEO + .lock() + .unwrap() + .as_mut() + .map(|v| v.render_frame(&self.video_handler.rgb).ok()); + } + } + } + Some(message::Union::hash(hash)) => { + self.handler.handle_hash(hash, peer).await; + } + Some(message::Union::login_response(lr)) => match lr.union { + Some(login_response::Union::error(err)) => { + if !self.handler.handle_login_error(&err) { + return false; + } + } + Some(login_response::Union::peer_info(pi)) => { + self.handler.handle_peer_info(pi); + } + _ => {} + }, + Some(message::Union::cursor_data(cd)) => { + self.handler.set_cursor_data(cd); + } + Some(message::Union::cursor_id(id)) => { + self.handler.set_cursor_id(id.to_string()); + } + Some(message::Union::cursor_position(cp)) => { + self.handler.set_cursor_position(cp); + } + Some(message::Union::clipboard(cb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + update_clipboard(cb, Some(&self.old_clipboard)); + } + } + Some(message::Union::file_response(fr)) => match fr.union { + Some(file_response::Union::dir(fd)) => { + let entries = fd.entries.to_vec(); + let mut m = make_fd(fd.id, &entries, fd.id > 0); + if fd.id <= 0 { + m.set_item("path", fd.path); + } + self.handler.call("updateFolderFiles", &make_args!(m)); + if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { + job.set_files(entries); + } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + job.files = entries; + } + } + Some(file_response::Union::block(block)) => { + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } + } + Some(file_response::Union::done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + _ => {} + }, + Some(message::Union::misc(misc)) => match misc.union { + Some(misc::Union::audio_format(f)) => { + self.audio_handler.handle_format(f); + } + Some(misc::Union::chat_message(c)) => { + self.handler.call("newMessage", &make_args!(c.text)); + } + Some(misc::Union::permission_info(p)) => { + log::info!("Change permission {:?} -> {}", p.permission, p.enabled); + match p.permission.enum_value_or_default() { + Permission::Keyboard => { + self.handler + .call("setPermission", &make_args!("keyboard", p.enabled)); + } + Permission::Clipboard => { + *self.clipboard.write().unwrap() = p.enabled; + self.handler + .call("setPermission", &make_args!("clipboard", p.enabled)); + } + Permission::Audio => { + self.handler + .call("setPermission", &make_args!("audio", p.enabled)); + } + } + } + Some(misc::Union::switch_display(s)) => { + self.handler.call("switchDisplay", &make_args!(s.display)); + self.video_handler.reset(); + if s.width > 0 && s.height > 0 { + VIDEO.lock().unwrap().as_mut().map(|v| { + v.stop_streaming().ok(); + let ok = v.start_streaming( + (s.width, s.height), + COLOR_SPACE::Rgb32, + None, + ); + log::info!("[video] reinitialized: {:?}", ok); + }); + self.handler.set_display(s.x, s.y, s.width, s.height); + } + } + Some(misc::Union::close_reason(c)) => { + self.handler.msgbox("error", "Connection Error", &c); + return false; + } + _ => {} + }, + Some(message::Union::test_delay(t)) => { + self.handler.handle_test_delay(t, peer).await; + } + Some(message::Union::audio_frame(frame)) => { + self.audio_handler + .handle_frame(frame, !self.handler.lc.read().unwrap().disable_audio); + } + _ => {} + } + } + true + } +} + +fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { + let mut m = Value::map(); + m.set_item("id", id); + let mut a = Value::array(0); + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = Value::map(); + e.set_item("name", entry.name.to_owned()); + e.set_item("type", entry.entry_type.value()); + e.set_item("time", entry.modified_time as f64); + e.set_item("size", entry.size as f64); + a.push(e); + } + if only_count { + m.set_item("num_entries", entries.len() as i32); + } else { + m.set_item("entries", a); + } + m.set_item("total_size", n as f64); + m +} + +#[async_trait] +impl Interface for Handler { + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + self.call("msgbox", &make_args!(msgtype, title, text)); + } + + fn handle_login_error(&mut self, err: &str) -> bool { + self.lc.write().unwrap().handle_login_error(err, self) + } + + fn handle_peer_info(&mut self, pi: PeerInfo) { + let mut pi_sciter = Value::map(); + let username = self.lc.read().unwrap().get_username(&pi); + pi_sciter.set_item("username", username.clone()); + pi_sciter.set_item("hostname", pi.hostname.clone()); + pi_sciter.set_item("platform", pi.platform.clone()); + pi_sciter.set_item("sas_enabled", pi.sas_enabled); + if self.is_file_transfer() { + if pi.username.is_empty() { + self.on_error("No active console user logged on, please connect and logon first."); + return; + } + } else if !self.is_port_forward() { + if pi.displays.is_empty() { + self.lc.write().unwrap().handle_peer_info(username, pi); + self.msgbox("error", "Remote Error", "No Display"); + return; + } + let mut displays = Value::array(0); + for ref d in pi.displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + displays.push(display); + } + pi_sciter.set_item("displays", displays); + let mut current = pi.current_display as usize; + if current >= pi.displays.len() { + current = 0; + } + pi_sciter.set_item("current_display", current as i32); + let current = &pi.displays[current]; + self.set_display(current.x, current.y, current.width, current.height); + // https://sciter.com/forums/topic/color_spaceiyuv-crash + // Nothing spectacular in decoder – done on CPU side. + // So if you can do BGRA translation on your side – the better. + // BGRA is used as internal image format so it will not require additional transformations. + VIDEO.lock().unwrap().as_mut().map(|v| { + let ok = v.start_streaming( + (current.width as _, current.height as _), + COLOR_SPACE::Rgb32, + None, + ); + log::info!("[video] initialized: {:?}", ok); + }); + } + self.lc.write().unwrap().handle_peer_info(username, pi); + self.call("updatePi", &make_args!(pi_sciter)); + if self.is_file_transfer() { + self.call("closeSuccess", &make_args!()); + } else if !self.is_port_forward() { + self.msgbox("success", "Successful", "Connected, waiting for image..."); + } + #[cfg(windows)] + { + let mut path = std::env::temp_dir(); + path.push(&self.id); + let path = path.with_extension(config::APP_NAME.to_lowercase()); + std::fs::File::create(&path).ok(); + if let Some(path) = path.to_str() { + crate::platform::windows::add_recent_document(&path); + } + } + } + + async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + handle_test_delay(t, peer).await; + } +} + +impl Handler { + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err); + } +} diff --git a/src/ui/remote.tis b/src/ui/remote.tis new file mode 100644 index 00000000000..a447c98cd80 --- /dev/null +++ b/src/ui/remote.tis @@ -0,0 +1,434 @@ +var cursor_img = $(img#cursor); +var last_key_time = 0; +is_file_transfer = handler.is_file_transfer(); +var is_port_forward = handler.is_port_forward(); +var display_width = 0; +var display_height = 0; +var display_origin_x = 0; +var display_origin_y = 0; +var display_scale = 1; +var keyboard_enabled = true; // server side +var clipboard_enabled = true; // server side +var audio_enabled = true; // server side + +handler.setDisplay = function(x, y, w, h) { + display_width = w; + display_height = h; + display_origin_x = x; + display_origin_y = y; + adaptDisplay(); +} + +function adaptDisplay() { + var w = display_width; + var h = display_height; + if (!w || !h) return; + var style = handler.get_view_style(); + display_scale = 1.; + var (sx, sy, sw, sh) = view.screenBox(view.windowState == View.WINDOW_FULL_SCREEN ? #frame : #workarea, #rectw); + if (sw >= w && sh > h) { + var hh = $(header).box(#height, #border); + var el = $(div#adjust-window); + if (sh > h + hh && el) { + el.style.set{ display: "block" }; + el = $(li#adjust-window); + el.style.set{ display: "block" }; + el.onClick = function() { + view.windowState == View.WINDOW_SHOWN; + var (x, y) = view.box(#position, #border, #screen); + // extra for border + var extra = 2; + view.move(x, y, w + extra, h + hh + extra); + } + } + } + if (style != "original") { + var bw = $(body).box(#width, #border); + var bh = $(body).box(#height, #border); + if (view.windowState == View.WINDOW_FULL_SCREEN) { + bw = sw; + bh = sh; + } + if (bw > 0 && bh > 0) { + var scale_x = bw.toFloat() / w; + var scale_y = bh.toFloat() / h; + var scale = scale_x < scale_y ? scale_x : scale_y; + if ((scale > 1 && style == "stretch") || + (scale < 1 && style == "shrink")) { + display_scale = scale; + w = w * scale; + h = h * scale; + } + } + } + handler.style.set { + width: w + "px", + height: h + "px", + }; +} + +// https://sciter.com/event-handling/ +// https://sciter.com/docs/content/sciter/Event.htm + +var entered = false; + +var keymap = {}; +for (var (k, v) in Event) { + k = k + "" + if (k[0] == "V" && k[1] == "K") { + keymap[v] = k; + } +} + +// VK_ENTER = VK_RETURN +// somehow, handler.onKey and view.onKey not working +function self.onKey(evt) { + last_key_time = getTime(); + if (is_file_transfer || is_port_forward) return false; + if (!entered) return false; + if (!keyboard_enabled) return false; + switch (evt.type) { + case Event.KEY_DOWN: + handler.key_down_or_up(1, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey); + if (is_osx && evt.commandKey) { + handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey); + } + break; + case Event.KEY_UP: + handler.key_down_or_up(0, keymap[evt.keyCode] || "", evt.keyCode, evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey); + break; + case Event.KEY_CHAR: + // the keypress event is fired when the element receives character value. Event.keyCode is a UNICODE code point of the character + handler.key_down_or_up(2, "", evt.keyCode, evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey, evt.extendedKey); + break; + default: + return false; + } + return true; +} + +var wait_window_toolbar = false; +var last_mouse_mask; +var acc_wheel_delta_x = 0; +var acc_wheel_delta_y = 0; +var last_wheel_time = 0; +var inertia_velocity_x = 0; +var inertia_velocity_y = 0; +var acc_wheel_delta_x0 = 0; +var acc_wheel_delta_y0 = 0; +var total_wheel_time = 0; +var wheeling = false; +var dragging = false; + +// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum +function resetWheel() { + acc_wheel_delta_x = 0; + acc_wheel_delta_y = 0; + last_wheel_time = 0; + inertia_velocity_x = 0; + inertia_velocity_y = 0; + acc_wheel_delta_x0 = 0; + acc_wheel_delta_y0 = 0; + total_wheel_time = 0; + wheeling = false; +} + +var INERTIA_ACCELERATION = 30; + +// not good, precision not enough to simulate accelation effect, +// seems have to use pixel based rather line based delta +function accWheel(v, is_x) { + if (wheeling) return; + var abs_v = Math.abs(v); + var max_t = abs_v / INERTIA_ACCELERATION; + for (var t = 0.1; t < max_t; t += 0.1) { + var d = Math.round((abs_v - t * INERTIA_ACCELERATION / 2) * t).toInteger(); + if (d >= 1) { + abs_v -= t * INERTIA_ACCELERATION; + if (v < 0) { + d = -d; + v = -abs_v; + } else { + v = abs_v; + } + handler.send_mouse(3, is_x ? d : 0, !is_x ? d : 0, false, false, false, false); + accWheel(v, is_x); + break; + } + } +} + +function handler.onMouse(evt) +{ + if (is_file_transfer || is_port_forward) return false; + if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { + if (evt.y < 10) { + if (!wait_window_toolbar) { + wait_window_toolbar = true; + self.timer(300ms, function() { + if (!wait_window_toolbar) return; + if (view.windowState == View.WINDOW_FULL_SCREEN) { + $(header).style.set { + display: "block", + padding: (2 * workarea_offset) + "px 0 0 0", + }; + } + wait_window_toolbar = false; + }); + } + } else { + wait_window_toolbar = false; + } + } + var mask = 0; + var wheel_delta_x; + var wheel_delta_y; + switch(evt.type) { + case Event.MOUSE_DOWN: + mask = 1; + dragging = true; + break; + case Event.MOUSE_UP: + mask = 2; + dragging = false; + break; + case Event.MOUSE_MOVE: + if (cursor_img.style#display != "none" && keyboard_enabled) cursor_img.style#display = "none"; + break; + case Event.MOUSE_WHEEL: + // mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"]; + // seems buggy, it always -1 or 1, even I change system scrolling speed. + // to-do: should we use client side prefrence or server side? + mask = 3; + { + var (dx, dy) = evt.wheelDeltas; + if (Math.abs(dx) > Math.abs(dy)) { + dy = 0; + } else { + dx = 0; + } + acc_wheel_delta_x += dx; + acc_wheel_delta_y += dy; + wheel_delta_x = acc_wheel_delta_x.toInteger(); + wheel_delta_y = acc_wheel_delta_y.toInteger(); + acc_wheel_delta_x -= wheel_delta_x; + acc_wheel_delta_y -= wheel_delta_y; + var now = getTime(); + var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0; + if (dt > 0) { + var vx = dx / dt; + var vy = dy / dt; + if (vx != 0 || vy != 0) { + inertia_velocity_x = vx; + inertia_velocity_y = vy; + } + } + acc_wheel_delta_x0 += dx; + acc_wheel_delta_y0 += dy; + total_wheel_time += dt; + if (dx == 0 && dy == 0) { + wheeling = false; + if (dt < 0.1 && total_wheel_time > 0) { + var v2 = (acc_wheel_delta_y0 / total_wheel_time) * inertia_velocity_y; + if (v2 > 0) { + v2 = Math.sqrt(v2); + inertia_velocity_y = inertia_velocity_y < 0 ? -v2 : v2; + accWheel(inertia_velocity_y, false); + } + v2 = (acc_wheel_delta_x0 / total_wheel_time) * inertia_velocity_x; + if (v2 > 0) { + v2 = Math.sqrt(v2); + inertia_velocity_x = inertia_velocity_x < 0 ? -v2 : v2; + accWheel(inertia_velocity_x, true); + } + } + resetWheel(); + } else { + wheeling = true; + } + last_wheel_time = now; + if (wheel_delta_x == 0 && wheel_delta_y == 0) return keyboard_enabled; + } + break; + case Event.MOUSE_DCLICK: // seq: down, up, dclick, up + mask = 1; + break; + case Event.MOUSE_ENTER: + entered = true; + stdout.println("enter"); + if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { + wait_window_toolbar = false; + $(header).style.set { + display: "none", + }; + } + return keyboard_enabled; + case Event.MOUSE_LEAVE: + entered = false; + stdout.println("leave"); + return keyboard_enabled; + default: + return false; + } + var x = evt.x; + var y = evt.y; + if (mask != 0) { + // to gain control of the mouse, user must move mouse + if (cur_x != x || cur_y != y) { + return keyboard_enabled; + } + // save bandwidth + x = 0; + y = 0; + } else { + cur_x = x; + cur_y = y; + } + if (mask != 3) { + resetWheel(); + } + if (!keyboard_enabled) return false; + x = (x / display_scale).toInteger(); + y = (y / display_scale).toInteger(); + // insert down between two up, osx has this behavior for triple click + if (last_mouse_mask == 2 && mask == 2) { + handler.send_mouse((evt.buttons << 3) | 1, x + display_origin_x, y + display_origin_y, evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey); + } + last_mouse_mask = mask; + // to-do: altKey, ctrlKey etc + handler.send_mouse((evt.buttons << 3) | mask, + mask == 3 ? wheel_delta_x : x + display_origin_x, + mask == 3 ? wheel_delta_y : y + display_origin_y, + evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey); + return true; +}; + +var cur_hotx = 0; +var cur_hoty = 0; +var cur_img = null; +var cur_x = 0; +var cur_y = 0; +var cursors = {}; +var image_binded; + +handler.setCursorData = function(id, hotx, hoty, width, height, colors) { + cur_hotx = hotx; + cur_hoty = hoty; + cursor_img.style.set { + width: width + "px", + height: height + "px", + }; + var img = Image.fromBytes(colors); + if (img) { + image_binded = true; + cursors[id] = [img, hotx, hoty, width, height]; + this.bindImage("in-memory:cursor", img); + self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); }); + cur_img = img; + } +} + +handler.setCursorId = function(id) { + var img = cursors[id]; + if (img) { + image_binded = true; + cur_hotx = img[1]; + cur_hoty = img[2]; + cursor_img.style.set { + width: img[3] + "px", + height: img[4] + "px", + }; + img = img[0]; + this.bindImage("in-memory:cursor", img); + self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); }); + cur_img = img; + } +} + +handler.setCursorPosition = function(x, y) { + if (!image_binded) return; + cur_x = x - display_origin_x; + cur_y = y - display_origin_y; + var x = cur_x - cur_hotx; + var y = cur_y - cur_hoty; + x *= display_scale; + y *= display_scale; + cursor_img.style.set { + left: x + "px", + top: y + "px", + display: "block", + }; + handler.style.cursor(null); +} + +function self.ready() { + var w = 960; + var h = 640; + if (is_file_transfer || is_port_forward) { + var r = handler.get_size(); + if (r[0] > 0) { + view.move(r[0], r[1], r[2], r[3]); + } else { + centerize(w, h); + } + } else { + centerize(w, h); + } + if (!is_port_forward) connecting(); + if (is_file_transfer) initializeFileTransfer(); + if (is_port_forward) initializePortForward(); +} + +var workarea_offset = 0; +var size_adapted; +handler.adaptSize = function() { + if (size_adapted) return; + size_adapted = true; + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + var (fx, fy, fw, fh) = view.screenBox(#frame, #rectw); + workarea_offset = sy; + var r = handler.get_size(); + if (r[2] > 0) { + if (r[2] >= fw && r[3] >= fh) { + view.windowState = View.WINDOW_FULL_SCREEN; + } else if (r[2] >= sw && r[3] >= sh) { + view.windowState = View.WINDOW_MAXIMIZED; + } else { + view.move(r[0], r[1], r[2], r[3]); + } + } else { + var w = handler.box(#width, #border) + if (sw == w) { + view.windowState = View.WINDOW_MAXIMIZED; + return; + } + var h = $(header).box(#height, #border); + // extra for border + var extra = 2; + centerize(w + extra, handler.box(#height, #border) + h + extra); + } +} + +function self.closing() { + var (x, y, w, h) = view.box(#rectw, #border, #screen); + if (is_file_transfer) save_file_transfer_close_state(); + if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); +} + +handler.setPermission = function(name, enabled) { + if (name == "keyboard") keyboard_enabled = enabled; + if (name == "audio") audio_enabled = enabled; + if (name == "clipboard") clipboard_enabled = enabled; + header.update(); +} + +handler.closeSuccess = function() { + // handler.msgbox("success", "Successful", "Ready to go."); + handler.msgbox("", "", ""); +} diff --git a/src/windows.cc b/src/windows.cc new file mode 100644 index 00000000000..4614f40ce0c --- /dev/null +++ b/src/windows.cc @@ -0,0 +1,366 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include // NOLINT(build/include_order) +#include + +void flog(char const *fmt, ...) +{ + FILE *h = fopen("C:\\Windows\\temp\\test_rustdesk.log", "at"); + if (!h) + return; + va_list arg; + va_start(arg, fmt); + vfprintf(h, fmt, arg); + va_end(arg); + fclose(h); +} + +// ultravnc has rdp support +// https://github.com/veyon/ultravnc/blob/master/winvnc/winvnc/service.cpp +// https://github.com/TigerVNC/tigervnc/blob/master/win/winvnc/VNCServerService.cxx +// https://blog.csdn.net/MA540213/article/details/84638264 + +DWORD GetLogonPid(DWORD dwSessionId, BOOL as_user) +{ + DWORD dwLogonPid = 0; + HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnap != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32W procEntry; + procEntry.dwSize = sizeof procEntry; + + if (Process32FirstW(hSnap, &procEntry)) + do + { + DWORD dwLogonSessionId = 0; + if (_wcsicmp(procEntry.szExeFile, as_user ? L"explorer.exe" : L"winlogon.exe") == 0 && + ProcessIdToSessionId(procEntry.th32ProcessID, &dwLogonSessionId) && + dwLogonSessionId == dwSessionId) + { + dwLogonPid = procEntry.th32ProcessID; + break; + } + } while (Process32NextW(hSnap, &procEntry)); + CloseHandle(hSnap); + } + return dwLogonPid; +} + +// if should try WTSQueryUserToken? +// https://stackoverflow.com/questions/7285666/example-code-a-service-calls-createprocessasuser-i-want-the-process-to-run-in +BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL as_user) +{ + BOOL bResult = FALSE; + DWORD Id = GetLogonPid(dwSessionId, as_user); + if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id)) + { + bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken); + CloseHandle(hProcess); + } + return bResult; +} + +// START the app as system +extern "C" +{ + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user) + { + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + if (GetSessionUserTokenWin(&hToken, dwSessionId, as_user)) + { + STARTUPINFOW si; + ZeroMemory(&si, sizeof si); + si.cb = sizeof si; + si.dwFlags = STARTF_USESHOWWINDOW; + wchar_t buf[MAX_PATH]; + wcscpy_s(buf, sizeof(buf), cmd); + PROCESS_INFORMATION pi; + LPVOID lpEnvironment = NULL; + DWORD dwCreationFlags = DETACHED_PROCESS; + if (as_user) + { + + CreateEnvironmentBlock(&lpEnvironment, // Environment block + hToken, // New token + TRUE); // Inheritence + } + if (lpEnvironment) + { + dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT; + } + if (CreateProcessAsUserW(hToken, NULL, buf, NULL, NULL, FALSE, dwCreationFlags, lpEnvironment, NULL, &si, &pi)) + { + CloseHandle(pi.hThread); + hProcess = pi.hProcess; + } + CloseHandle(hToken); + if (lpEnvironment) + DestroyEnvironmentBlock(lpEnvironment); + } + return hProcess; + } + + // Switch the current thread to the specified desktop + static bool + switchToDesktop(HDESK desktop) + { + HDESK old_desktop = GetThreadDesktop(GetCurrentThreadId()); + if (!SetThreadDesktop(desktop)) + { + return false; + } + if (!CloseDesktop(old_desktop)) + { + // + } + return true; + } + + // https://github.com/TigerVNC/tigervnc/blob/8c6c584377feba0e3b99eecb3ef33b28cee318cb/win/rfb_win32/Service.cxx + + // Determine whether the thread's current desktop is the input one + BOOL + inputDesktopSelected() + { + HDESK current = GetThreadDesktop(GetCurrentThreadId()); + HDESK input = OpenInputDesktop(0, FALSE, + DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW | + DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | + DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | GENERIC_WRITE); + if (!input) + { + return FALSE; + } + + DWORD size; + char currentname[256]; + char inputname[256]; + + if (!GetUserObjectInformation(current, UOI_NAME, currentname, sizeof(currentname), &size)) + { + CloseDesktop(input); + return FALSE; + } + if (!GetUserObjectInformation(input, UOI_NAME, inputname, sizeof(inputname), &size)) + { + CloseDesktop(input); + return FALSE; + } + CloseDesktop(input); + // flog("%s %s\n", currentname, inputname); + return strcmp(currentname, inputname) == 0 ? TRUE : FALSE; + } + + // Switch the current thread into the input desktop + bool + selectInputDesktop() + { + // - Open the input desktop + HDESK desktop = OpenInputDesktop(0, FALSE, + DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW | + DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | + DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | GENERIC_WRITE); + if (!desktop) + { + return false; + } + + // - Switch into it + if (!switchToDesktop(desktop)) + { + CloseDesktop(desktop); + return false; + } + + // *** + DWORD size = 256; + char currentname[256]; + if (GetUserObjectInformation(desktop, UOI_NAME, currentname, 256, &size)) + { + // + } + + return true; + } + + int handleMask(uint8_t *rwbuffer, const uint8_t *mask, int width, int height, int bmWidthBytes) + { + auto andMask = mask; + auto xorMask = mask + height * bmWidthBytes; + int doOutline = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int byte = y * bmWidthBytes + x / 8; + int bit = 7 - x % 8; + + if (!(andMask[byte] & (1 << bit))) + { + // Valid pixel, so make it opaque + rwbuffer[3] = 0xff; + + // Black or white? + if (xorMask[byte] & (1 << bit)) + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0xff; + else + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0; + } + else if (xorMask[byte] & (1 << bit)) + { + // Replace any XORed pixels with black, because RFB doesn't support + // XORing of cursors. XORing is used for the I-beam cursor, which is most + // often used over a white background, but also sometimes over a black + // background. We set the XOR'd pixels to black, then draw a white outline + // around the whole cursor. + + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0; + rwbuffer[3] = 0xff; + + doOutline = 1; + } + else + { + // Transparent pixel + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = rwbuffer[3] = 0; + } + + rwbuffer += 4; + } + } + return doOutline; + } + + void drawOutline(uint8_t *out0, const uint8_t *in0, int width, int height) + { + auto in = in0; + auto out = out0 + width * 4 + 4; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Visible pixel? + if (in[3] > 0) + { + // Outline above... + memset(out - (width + 2) * 4 - 4, 0xff, 4 * 3); + // ...besides... + memset(out - 4, 0xff, 4 * 3); + // ...and above + memset(out + (width + 2) * 4 - 4, 0xff, 4 * 3); + } + in += 4; + out += 4; + } + // outline is slightly larger + out += 2 * 4; + } + + // Pass 2, overwrite with actual cursor + in = in0; + out = out0 + width * 4 + 4; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (in[3] > 0) + memcpy(out, in, 4); + in += 4; + out += 4; + } + out += 2 * 4; + } + } + + int ffi(unsigned v) + { + static const int MultiplyDeBruijnBitPosition[32] = + { + 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + return MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27]; + } + + int get_di_bits(uint8_t *out, HDC dc, HBITMAP hbmColor, int width, int height) + { + BITMAPV5HEADER bi; + memset(&bi, 0, sizeof(BITMAPV5HEADER)); + + bi.bV5Size = sizeof(BITMAPV5HEADER); + bi.bV5Width = width; + bi.bV5Height = -height; // Negative for top-down + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x000000FF; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x00FF0000; + bi.bV5AlphaMask = 0xFF000000; + + if (!GetDIBits(dc, hbmColor, 0, height, + out, (LPBITMAPINFO)&bi, DIB_RGB_COLORS)) + return 1; + + // We may not get the RGBA order we want, so shuffle things around + int ridx, gidx, bidx, aidx; + + ridx = ffi(bi.bV5RedMask) / 8; + gidx = ffi(bi.bV5GreenMask) / 8; + bidx = ffi(bi.bV5BlueMask) / 8; + // Usually not set properly + aidx = 6 - ridx - gidx - bidx; + + if ((bi.bV5RedMask != ((unsigned)0xff << ridx * 8)) || + (bi.bV5GreenMask != ((unsigned)0xff << gidx * 8)) || + (bi.bV5BlueMask != ((unsigned)0xff << bidx * 8))) + return 1; + + auto rwbuffer = out; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint8_t r, g, b, a; + + r = rwbuffer[ridx]; + g = rwbuffer[gidx]; + b = rwbuffer[bidx]; + a = rwbuffer[aidx]; + + rwbuffer[0] = r; + rwbuffer[1] = g; + rwbuffer[2] = b; + rwbuffer[3] = a; + + rwbuffer += 4; + } + } + return 0; + } + + void blank_screen(BOOL set) + { + if (set) + { + SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2); + } + else + { + SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)-1); + } + } + + void AddRecentDocument(PCWSTR path) + { + SHAddToRecentDocs(SHARD_PATHW, path); + } +} // end of extern "C" \ No newline at end of file