diff --git a/Cargo.lock b/Cargo.lock index fb17c7e41..fd2c981b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" + [[package]] name = "addr2line" version = "0.17.0" @@ -54,12 +60,37 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "andrew" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" +dependencies = [ + "bitflags", + "rusttype", + "walkdir", + "xdg", + "xml-rs", +] + [[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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ed09b18365ed295d722d0b5ed59c01b79a826ff2d2a8f73d5ecca8e6fb2f66" +dependencies = [ + "android_log-sys", + "env_logger 0.8.4", + "lazy_static", + "log", +] + [[package]] name = "android_logger" version = "0.11.0" @@ -67,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74b7ddf197de32e415d197aa21c1c0cb36e01e4794fd801302280ac7847ee02" dependencies = [ "android_log-sys", - "env_logger", + "env_logger 0.9.0", "log", "once_cell", ] @@ -107,6 +138,59 @@ dependencies = [ "x11rb", ] +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2 0.4.4", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-process" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +dependencies = [ + "async-io", + "blocking", + "cfg-if 1.0.0", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi 0.3.9", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + [[package]] name = "async-trait" version = "0.1.53" @@ -118,44 +202,18 @@ dependencies = [ "syn", ] -[[package]] -name = "atk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812b4911e210bd51b24596244523c856ca749e6223c50a7fbbba3f89ee37c426" -dependencies = [ - "atk-sys 0.10.0", - "bitflags", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", -] - [[package]] name = "atk" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ - "atk-sys 0.15.1", + "atk-sys", "bitflags", "glib 0.15.11", "libc", ] -[[package]] -name = "atk-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" -dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "system-deps 1.3.2", -] - [[package]] name = "atk-sys" version = "0.15.1" @@ -168,6 +226,12 @@ dependencies = [ "system-deps 6.0.2", ] +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + [[package]] name = "atty" version = "0.2.14" @@ -225,7 +289,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.34.0", - "env_logger", + "env_logger 0.9.0", "lazy_static", "lazycell", "log", @@ -259,6 +323,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + [[package]] name = "bumpalo" version = "3.9.1" @@ -284,19 +362,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] -name = "cairo-rs" -version = "0.9.1" +name = "cache-padded" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" -dependencies = [ - "bitflags", - "cairo-sys-rs 0.10.0", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "thiserror", -] +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cairo-rs" @@ -305,23 +374,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62be3562254e90c1c6050a72aa638f6315593e98c5cdaba9017cedbabf0a5dee" dependencies = [ "bitflags", - "cairo-sys-rs 0.15.1", + "cairo-sys-rs", "glib 0.15.11", "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 0.10.1", - "libc", - "system-deps 1.3.2", -] - [[package]] name = "cairo-sys-rs" version = "0.15.1" @@ -333,6 +391,16 @@ dependencies = [ "system-deps 6.0.2", ] +[[package]] +name = "calloop" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" +dependencies = [ + "log", + "nix 0.18.0", +] + [[package]] name = "cc" version = "1.0.73" @@ -398,7 +466,7 @@ checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.7.3", ] [[package]] @@ -452,20 +520,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "clipboard-master" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459887701008d8ee21f8de7f45f0f0707417c7eea3311973f6e67222bd686b7a" -dependencies = [ - "objc", - "objc-foundation", - "objc_id", - "winapi 0.3.9", - "windows-win", - "x11-clipboard", -] - [[package]] name = "clipboard-win" version = "4.4.1" @@ -557,6 +611,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "confy" version = "0.4.0" @@ -649,6 +712,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "objc", +] + [[package]] name = "coreaudio-rs" version = "0.10.0" @@ -682,8 +758,8 @@ dependencies = [ "lazy_static", "libc", "mach", - "ndk", - "ndk-glue", + "ndk 0.6.0", + "ndk-glue 0.6.2", "nix 0.23.1", "oboe", "parking_lot 0.11.2", @@ -816,14 +892,38 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[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]] @@ -840,13 +940,24 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", "quote", "syn", ] @@ -1011,6 +1122,15 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1021,6 +1141,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1038,6 +1169,24 @@ 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 = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading 0.7.3", +] + [[package]] name = "docopt" version = "1.1.1" @@ -1050,6 +1199,12 @@ dependencies = [ "strsim 0.10.0", ] +[[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.8" @@ -1071,6 +1226,15 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "enigo" version = "0.0.14" @@ -1086,6 +1250,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -1123,6 +1297,12 @@ dependencies = [ "str-buf", ] +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + [[package]] name = "failure" version = "0.1.8" @@ -1215,6 +1395,16 @@ 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" @@ -1285,6 +1475,21 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.21" @@ -1335,26 +1540,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "gdk" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db00839b2a68a7a10af3fa28dfb3febaba3a20c3a9ac2425a33b7df1f84a6b7d" -dependencies = [ - "bitflags", - "cairo-rs 0.9.1", - "cairo-sys-rs 0.10.0", - "gdk-pixbuf 0.9.0", - "gdk-sys 0.10.0", - "gio 0.9.1", - "gio-sys 0.10.1", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "pango 0.9.1", -] - [[package]] name = "gdk" version = "0.15.4" @@ -1362,28 +1547,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" dependencies = [ "bitflags", - "cairo-rs 0.15.11", - "gdk-pixbuf 0.15.11", - "gdk-sys 0.15.1", - "gio 0.15.11", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", "glib 0.15.11", "libc", - "pango 0.15.10", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6dae3cb99dd49b758b88f0132f8d401108e63ae8edd45f432d42cdff99998a" -dependencies = [ - "gdk-pixbuf-sys 0.10.0", - "gio 0.9.1", - "gio-sys 0.10.1", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", + "pango", ] [[package]] @@ -1393,68 +1563,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" dependencies = [ "bitflags", - "gdk-pixbuf-sys 0.15.10", - "gio 0.15.11", + "gdk-pixbuf-sys", + "gio", "glib 0.15.11", "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 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "system-deps 1.3.2", -] - [[package]] name = "gdk-pixbuf-sys" version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" dependencies = [ - "gio-sys 0.15.10", + "gio-sys", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", "system-deps 6.0.2", ] -[[package]] -name = "gdk-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" -dependencies = [ - "cairo-sys-rs 0.10.0", - "gdk-pixbuf-sys 0.10.0", - "gio-sys 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "pango-sys 0.10.0", - "pkg-config", - "system-deps 1.3.2", -] - [[package]] name = "gdk-sys" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" dependencies = [ - "cairo-sys-rs 0.15.1", - "gdk-pixbuf-sys 0.15.10", - "gio-sys 0.15.10", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "pango-sys 0.15.10", + "pango-sys", "pkg-config", "system-deps 6.0.2", ] @@ -1496,27 +1636,6 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" -[[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 0.10.1", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "once_cell", - "thiserror", -] - [[package]] name = "gio" version = "0.15.11" @@ -1527,26 +1646,13 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "gio-sys 0.15.10", + "gio-sys", "glib 0.15.11", "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 0.10.1", - "gobject-sys 0.10.0", - "libc", - "system-deps 1.3.2", - "winapi 0.3.9", -] - [[package]] name = "gio-sys" version = "0.15.10" @@ -1811,90 +1917,44 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "gtk" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f022f2054072b3af07666341984562c8e626a79daa8be27b955d12d06a5ad6a" -dependencies = [ - "atk 0.9.0", - "bitflags", - "cairo-rs 0.9.1", - "cairo-sys-rs 0.10.0", - "cc", - "gdk 0.13.2", - "gdk-pixbuf 0.9.0", - "gdk-pixbuf-sys 0.10.0", - "gdk-sys 0.10.0", - "gio 0.9.1", - "gio-sys 0.10.1", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "gtk-sys 0.10.0", - "libc", - "once_cell", - "pango 0.9.1", - "pango-sys 0.10.0", - "pkg-config", -] - [[package]] name = "gtk" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" dependencies = [ - "atk 0.15.1", + "atk", "bitflags", - "cairo-rs 0.15.11", + "cairo-rs", "field-offset", "futures-channel", - "gdk 0.15.4", - "gdk-pixbuf 0.15.11", - "gio 0.15.11", + "gdk", + "gdk-pixbuf", + "gio", "glib 0.15.11", - "gtk-sys 0.15.3", + "gtk-sys", "gtk3-macros", "libc", "once_cell", - "pango 0.15.10", + "pango", "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 0.10.0", - "cairo-sys-rs 0.10.0", - "gdk-pixbuf-sys 0.10.0", - "gdk-sys 0.10.0", - "gio-sys 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "pango-sys 0.10.0", - "system-deps 1.3.2", -] - [[package]] name = "gtk-sys" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" dependencies = [ - "atk-sys 0.15.1", - "cairo-sys-rs 0.15.1", - "gdk-pixbuf-sys 0.15.10", - "gdk-sys 0.15.1", - "gio-sys 0.15.10", + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "pango-sys 0.15.10", + "pango-sys", "system-deps 6.0.2", ] @@ -1912,6 +1972,25 @@ dependencies = [ "syn", ] +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.1", + "tracing", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -1927,7 +2006,7 @@ dependencies = [ "confy", "directories-next", "dirs-next", - "env_logger", + "env_logger 0.9.0", "filetime", "futures", "futures-util", @@ -1946,7 +2025,7 @@ dependencies = [ "sodiumoxide", "tokio", "tokio-socks", - "tokio-util", + "tokio-util 0.6.9", "toml", "winapi 0.3.9", "zstd", @@ -1982,18 +2061,100 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" +[[package]] +name = "http" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.1", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.1", + "pin-project-lite", + "socket2 0.4.4", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.23.14" @@ -2057,6 +2218,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "itertools" version = "0.9.0" @@ -2144,18 +2311,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "libappindicator" -version = "0.6.1" -source = "git+https://github.com/liyue201/libappindicator-rs#3763cfd629dd90050af1feafa643cbfca0bf487e" -dependencies = [ - "glib 0.10.3", - "gtk 0.9.2", - "gtk-sys 0.10.0", - "libappindicator-sys 0.6.1", - "log", -] - [[package]] name = "libappindicator" version = "0.7.0" @@ -2163,28 +2318,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b29fab3280d59f3d06725f75da9ef9a1b001b2c748b1abfebd1c966c61d7de" dependencies = [ "glib 0.15.11", - "gtk 0.15.5", - "gtk-sys 0.15.3", - "libappindicator-sys 0.7.0", + "gtk", + "gtk-sys", + "libappindicator-sys", "log", ] -[[package]] -name = "libappindicator-sys" -version = "0.6.1" -source = "git+https://github.com/liyue201/libappindicator-rs#3763cfd629dd90050af1feafa643cbfca0bf487e" -dependencies = [ - "gtk-sys 0.10.0", - "pkg-config", -] - [[package]] name = "libappindicator-sys" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bdcb8c5cfc11febe2ff3f18386d6cb7d29f464cbaf6b286985c3f1a501d74f" dependencies = [ - "gtk-sys 0.15.3", + "gtk-sys", "pkg-config", ] @@ -2203,6 +2349,16 @@ dependencies = [ "pkg-config", ] +[[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 = "libloading" version = "0.7.3" @@ -2347,12 +2503,27 @@ dependencies = [ "libc", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memmap2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -2362,6 +2533,12 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2442,6 +2619,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "mio-misc" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47412f3a52115b936ff2a229b803498c7b4d332adeb87c2f1498c9da54c398c" +dependencies = [ + "crossbeam", + "crossbeam-queue", + "log", + "mio 0.7.14", +] + [[package]] name = "mio-named-pipes" version = "0.1.7" @@ -2490,6 +2679,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys 0.2.2", + "num_enum", + "thiserror", +] + [[package]] name = "ndk" version = "0.6.0" @@ -2498,7 +2717,7 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags", "jni-sys", - "ndk-sys", + "ndk-sys 0.3.0", "num_enum", "thiserror", ] @@ -2509,6 +2728,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.3.0", + "ndk-macro 0.2.0", + "ndk-sys 0.2.2", +] + [[package]] name = "ndk-glue" version = "0.6.2" @@ -2518,10 +2751,23 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.6.0", "ndk-context", - "ndk-macro", - "ndk-sys", + "ndk-macro 0.3.0", + "ndk-sys 0.3.0", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling 0.10.2", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2530,13 +2776,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn", ] +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + [[package]] name = "ndk-sys" version = "0.3.0" @@ -2557,6 +2809,30 @@ dependencies = [ "winapi 0.3.9", ] +[[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.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "nix" version = "0.22.3" @@ -2768,8 +3044,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2463c8f2e19b4e0d0710a21f8e4011501ff28db1c95d7a5482a553b2100502d2" dependencies = [ "jni", - "ndk", - "ndk-glue", + "ndk 0.6.0", + "ndk-glue 0.6.2", "num-derive", "num-traits 0.2.14", "oboe-sys", @@ -2790,39 +3066,72 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +[[package]] +name = "openssl" +version = "0.10.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" +dependencies = [ + "autocfg 1.1.0", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +[[package]] +name = "owned_ttf_parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" +dependencies = [ + "ttf-parser", +] + [[package]] name = "padlock" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" -[[package]] -name = "pango" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9937068580bebd8ced19975938573803273ccbcbd598c58d4906efd4ac87c438" -dependencies = [ - "bitflags", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "once_cell", - "pango-sys 0.10.0", -] - [[package]] name = "pango" version = "0.15.10" @@ -2833,19 +3142,7 @@ dependencies = [ "glib 0.15.11", "libc", "once_cell", - "pango-sys 0.15.10", -] - -[[package]] -name = "pango-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" -dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "system-deps 1.3.2", + "pango-sys", ] [[package]] @@ -2875,6 +3172,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2935,6 +3238,12 @@ 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 = "pest" version = "2.1.3" @@ -3032,6 +3341,19 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "winapi 0.3.9", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -3174,15 +3496,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "quick-xml" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" -dependencies = [ - "memchr", -] - [[package]] name = "quinn" version = "0.8.2" @@ -3382,6 +3695,25 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "raw-window-handle" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28f55143d0548dad60bb4fbdc835a3d7ac6acc3324506450c5fdd6e42903a76" +dependencies = [ + "libc", + "raw-window-handle 0.4.3", +] + +[[package]] +name = "raw-window-handle" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" +dependencies = [ + "cty", +] + [[package]] name = "rayon" version = "1.5.2" @@ -3495,6 +3827,42 @@ dependencies = [ "flate2", ] +[[package]] +name = "reqwest" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde 1.0.136", + "serde_json 1.0.79", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.10.1", +] + [[package]] name = "ring" version = "0.16.20" @@ -3588,15 +3956,15 @@ dependencies = [ name = "rustdesk" version = "1.1.9" dependencies = [ - "android_logger", + "android_logger 0.11.0", "arboard", + "async-process", "async-trait", "base64", "cc", "cfg-if 1.0.0", "clap 3.1.12", "clipboard", - "clipboard-master", "cocoa 0.24.0", "core-foundation 0.9.3", "core-graphics 0.22.3", @@ -3609,6 +3977,7 @@ dependencies = [ "hbb_common", "hound", "include_dir", + "jni", "lazy_static", "libc", "libpulse-binding", @@ -3621,6 +3990,7 @@ dependencies = [ "parity-tokio-ipc", "rdev", "repng", + "reqwest", "rpassword 6.0.1", "rubato", "runas", @@ -3634,13 +4004,14 @@ dependencies = [ "sha2", "sys-locale", "sysinfo", - "systray", "tray-item", + "trayicon", "uuid", "virtual_display", "whoami", "winapi 0.3.9", "windows-service", + "winit", "winreg 0.10.1", "winres", ] @@ -3700,6 +4071,16 @@ dependencies = [ "base64", ] +[[package]] +name = "rusttype" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + [[package]] name = "rustversion" version = "1.0.6" @@ -3751,6 +4132,12 @@ dependencies = [ "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" @@ -3761,6 +4148,7 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" name = "scrap" version = "0.5.0" dependencies = [ + "android_logger 0.10.1", "bindgen", "block", "cfg-if 1.0.0", @@ -3769,14 +4157,16 @@ dependencies = [ "gstreamer", "gstreamer-app", "gstreamer-video", + "jni", + "lazy_static", "libc", + "log", "num_cpus", "quest", "repng", "serde 1.0.136", "target_build_utils", "tracing", - "vcpkg", "webm", "winapi 0.3.9", ] @@ -3881,6 +4271,18 @@ dependencies = [ "serde 1.0.136", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.1", + "ryu", + "serde 1.0.136", +] + [[package]] name = "sha2" version = "0.10.2" @@ -3898,6 +4300,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -3931,6 +4343,25 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "smithay-client-toolkit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" +dependencies = [ + "andrew", + "bitflags", + "calloop", + "dlib 0.4.2", + "lazy_static", + "log", + "memmap2", + "nix 0.18.0", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + [[package]] name = "socket2" version = "0.3.19" @@ -3994,6 +4425,12 @@ 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 = "strsim" version = "0.10.0" @@ -4097,19 +4534,6 @@ dependencies = [ "version-compare 0.1.0", ] -[[package]] -name = "systray" -version = "0.4.1" -source = "git+https://github.com/liyue201/systray-rs#84cca4b4171661bc6c4d1ba5aaa2320ff8e085aa" -dependencies = [ - "glib 0.10.3", - "gtk 0.9.2", - "libappindicator 0.6.1", - "libc", - "log", - "winapi 0.3.9", -] - [[package]] name = "target_build_utils" version = "0.3.1" @@ -4263,6 +4687,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -4276,7 +4710,7 @@ dependencies = [ "pin-project", "thiserror", "tokio", - "tokio-util", + "tokio-util 0.6.9", ] [[package]] @@ -4295,6 +4729,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.9" @@ -4304,6 +4752,12 @@ dependencies = [ "serde 1.0.136", ] +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + [[package]] name = "tracing" version = "0.1.34" @@ -4354,8 +4808,8 @@ checksum = "76863575f7842ed64fda361f417a787efa82811b4617267709066969cd4ccf3b" dependencies = [ "cocoa 0.24.0", "core-graphics 0.22.3", - "gtk 0.15.5", - "libappindicator 0.7.0", + "gtk", + "libappindicator", "libc", "objc", "objc-foundation", @@ -4364,6 +4818,28 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "trayicon" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c367fd7cdcdf19234aa104f7e03abe1be526018e4282af9f275bf436b9c9ad23" +dependencies = [ + "winapi 0.3.9", + "winit", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "ttf-parser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" + [[package]] name = "typenum" version = "1.15.0" @@ -4376,6 +4852,21 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.9.0" @@ -4400,6 +4891,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "uuid" version = "1.0.0" @@ -4451,6 +4954,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -4462,6 +4971,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -4499,6 +5018,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.80" @@ -4528,6 +5059,79 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +[[package]] +name = "wayland-client" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.20.0", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" +dependencies = [ + "nix 0.20.0", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" +dependencies = [ + "nix 0.20.0", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" +dependencies = [ + "dlib 0.5.0", + "lazy_static", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.57" @@ -4572,6 +5176,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4" +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "which" version = "3.1.1" @@ -4699,15 +5312,6 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] -[[package]] -name = "windows-win" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4243ec23afe4e9b4e668b3c0a0e973f1b8265f6a46223cfcbc16fd267480c0" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "windows_aarch64_msvc" version = "0.28.0" @@ -4768,6 +5372,38 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +[[package]] +name = "winit" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8" +dependencies = [ + "bitflags", + "cocoa 0.24.0", + "core-foundation 0.9.3", + "core-graphics 0.22.3", + "core-video-sys", + "dispatch", + "instant", + "lazy_static", + "libc", + "log", + "mio 0.7.14", + "mio-misc", + "ndk 0.3.0", + "ndk-glue 0.3.0", + "ndk-sys 0.2.2", + "objc", + "parking_lot 0.11.2", + "percent-encoding", + "raw-window-handle 0.3.4", + "scopeguard", + "smithay-client-toolkit", + "wayland-client", + "winapi 0.3.9", + "x11-dl", +] + [[package]] name = "winreg" version = "0.6.2" @@ -4816,12 +5452,14 @@ dependencies = [ ] [[package]] -name = "x11-clipboard" -version = "0.5.3" +name = "x11-dl" +version = "2.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473068b7b80ac86a18328824f1054e5e007898c47b5bbc281bd7abe32bc3653c" +checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" dependencies = [ - "xcb", + "lazy_static", + "libc", + "pkg-config", ] [[package]] @@ -4837,16 +5475,29 @@ dependencies = [ ] [[package]] -name = "xcb" -version = "0.10.1" +name = "xcursor" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771e2b996df720cd1c6dd9ff90f62d91698fd3610cc078388d0564bdd6622a9c" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" dependencies = [ - "libc", - "log", - "quick-xml", + "nom", ] +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "zstd" version = "0.9.2+zstd.1.5.1" diff --git a/Cargo.toml b/Cargo.toml index 1b715bd5c..6de6fb430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,19 @@ authors = ["rustdesk "] edition = "2021" build= "build.rs" description = "A remote control software." +default-run = "rustdesk" + +[lib] +name = "librustdesk" +crate-type = ["cdylib", "staticlib", "rlib"] + +[[bin]] +name = "lic" +path = "src/lic_main.rs" [features] inline = [] +hbbs = [] cli = [] use_samplerate = ["samplerate"] use_rubato = ["rubato"] @@ -40,13 +50,16 @@ dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpol rubato = { version = "0.12", optional = true } samplerate = { version = "0.2", optional = true } async-trait = "0.1" -uuid = { version = "1.0.0", features = ["v4"] } +uuid = { version = "1.0", features = ["v4"] } clap = "3.0" rpassword = "6.0" base64 = "0.13" sysinfo = "0.23" num_cpus = "1.13" +[target.'cfg(not(target_os = "linux"))'.dependencies] +reqwest = { version = "0.11", features = ["json"] } + [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" @@ -54,14 +67,19 @@ cpal = "0.13.5" machine-uid = "0.2" mac_address = "1.1" sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" } +sys-locale = "0.2" +enigo = { path = "libs/enigo" } +clipboard = { path = "libs/clipboard" } +rdev = { git = "https://github.com/open-trade/rdev" } ctrlc = "3.2" arboard = "2.0" -clipboard-master = "3.1" -#rdev = { path = "../rdev" } -rdev = { git = "https://github.com/open-trade/rdev" } +#minreq = { version = "2.4", features = ["punycode", "https-native"] } [target.'cfg(target_os = "windows")'.dependencies] -systray = { git = "https://github.com/liyue201/systray-rs" } +#systray = { git = "https://github.com/open-trade/systray-rs" } +trayicon = { version = "0.1", features = ["winit"] } +# > 0.25 not work with trayicon +winit = "0.25" winapi = { version = "0.3", features = ["winuser"] } winreg = "0.10" windows-service = "0.4" @@ -79,15 +97,17 @@ tray-item = "0.7" # looks better than trayicon psimple = { package = "libpulse-simple-binding", version = "2.25" } pulse = { package = "libpulse-binding", version = "2.26" } rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } +async-process = "1.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" +jni = "0.19.0" [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display"] [package.metadata.winres] -LegalCopyright = "Copyright © 2020" +LegalCopyright = "Copyright © 2022 Purslane, Inc." # this FileDescription overrides package.description FileDescription = "RustDesk" @@ -106,7 +126,7 @@ hound = "3.4" 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"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio", "python3-pip", "curl"] osx_minimum_system_version = "10.14" resources = ["mac-tray.png"] @@ -114,7 +134,7 @@ resources = ["mac-tray.png"] #!!! rembember call "strip target/release/rustdesk" # which reduce binary size a lot [profile.release] -#lto = true -#codegen-units = 1 -#panic = 'abort' +lto = true +codegen-units = 1 +panic = 'abort' #opt-level = 'z' # only have smaller size after strip diff --git a/libs/enigo/.gitattributes b/libs/enigo/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/libs/enigo/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index c8193474a..bc31223cc 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hbb_common" version = "0.1.0" -authors = ["rustdesk"] +authors = ["open-trade "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -35,7 +35,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } mac_address = "1.1" [features] -quic = ["quinn"] +quic = [] [build-dependencies] protobuf-codegen-pure = "3.0.0-alpha.2" diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index afceede23..9fba78a92 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -72,6 +72,7 @@ message PeerInfo { int32 current_display = 5; bool sas_enabled = 6; string version = 7; + int32 conn_id = 8; } message LoginResponse { diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto index 4fce625f9..2c5f1b3ba 100644 --- a/libs/hbb_common/protos/rendezvous.proto +++ b/libs/hbb_common/protos/rendezvous.proto @@ -18,7 +18,9 @@ message RegisterPeerResponse { bool request_pk = 2; } message PunchHoleRequest { string id = 1; NatType nat_type = 2; + string licence_key = 3; ConnType conn_type = 4; + string token = 5; } message PunchHole { @@ -55,6 +57,7 @@ message RegisterPk { string id = 1; bytes uuid = 2; bytes pk = 3; + string old_id = 4; } message RegisterPkResponse { @@ -99,7 +102,9 @@ message RequestRelay { bytes socket_addr = 3; string relay_server = 4; bool secure = 5; + string licence_key = 6; ConnType conn_type = 7; + string token = 8; } message RelayResponse { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 3ad87267a..a7c1bc634 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -12,7 +12,6 @@ use std::{ time::SystemTime, }; -pub const APP_NAME: &str = "RustDesk"; pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; pub const CONNECT_TIMEOUT: u64 = 18_000; pub const REG_INTERVAL: i64 = 12_000; @@ -26,7 +25,9 @@ pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACA pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA7VBMVEUAAAAAcf8Acf8Acf8Adf8Acf8Acf8AcP8Acv8AcP8Acf8Acf8Acf8Acv8Acf8Acf8Ab/8AcP8Acf8Acf8Acf/////7/f8Dc/8TfP/1+f/n8v9Hmf/u9v+Uw//Q5f9hp/8Yfv8Qev8Ld/+52P+z1f+s0f81j/8wjP8Hdf/3+/8mh/8fg//x9//h7//H4P9xsP9rrf9oq/8rif/r9P/D3v+92/+Duv9bpP/d7f/U5/9NnP8/lP8jhP/L4v/B3P+OwP9+t/95tf9Rn/8bgf/Z6v+Zx/90sv9lqf85kf+hy/9UoP+Wxf+kzP+dyP+Lvv/H4q8IAAAAFHRSTlMA+u6bB6x5XR4V0+S4i4k5N+a81W8MiAQAAAVcSURBVHjazdvpWtpAGIbhgEutdW3fL2GHsMsiq4KI+66t5384XahF/GbizJAy3j/1Ah5CJhNCxpm1vbryLRrBfxKJrq+sbjtSa5u7WIDdzTVH5PNSBAsSWfrsMJ+iWKDoJ2fW8hIWbGl55vW/YuE2XhUsb8CCr9OCJVix9G//gyWf/o6/KCyJfrbwAfAPYS0CayK/j4mbsGjrV8AXWLTrONuwasdZhVWrzgqsWnG+wap1Jwqrok4EVkUcmKhdVvBaOVnzYEY/oJpMD4mo6ONF/ZSIUsX2FZjQA7xRqUET+y/v2W/Sy59u62DCDMgdJmhqgIk7eqWQBBNWwPhmj147w8QTzTjKVsGEEBBLuzSrhIkivTF8DD/Aa6forQNMHBD/VyXkgHGfuBN5ALln1TADOnESyGCiT8L/1kILqD6Q0BEm9kkofhdSwNUJiV1jQvZ/SnthBNSaJJGZbgGJUnX+gEqCZPpsJ2T2Y/MGVBrE8eOAvCA/X8A4QXLnmEhTgIPqPAG5IQU4fhmkFOT7HAFenwIU8Jd/TUEODQIUtu1eOj/dUD9cknOTpgEDkup3YrOfVStDUomcWcBVisTiNxVw3TPpgCl4RgFFybZ/9iHmn8uS2yYBA8m7qUEu9oOEejH9gHxC+PazCHbcFM8K+gGHJNAs4z2xgnAkVHQDcnG1IzvnCSfvom7AM3EZ9voah4+KXoAvGFJHMSgqEfegF3BBTKoOVfkMMXFfJ8AT7MuXUDeOE9PWCUiKBpKOlmAP1gngH2LChw7vhJgr9YD8Hnt0BxrE27CtHnDJR4AHTX1+KFAP4Ef0LHTxN9HwlAMSbAjmoavKZ8ayakDXYAhwN3wzqgZk2UPvwRjshmeqATeCT09f3mWnEqoBGf4NxAB/moRqADuOtmDiid6KqQVcsQeOYOKW3uqqBRwL5nITj/yrlFpAVrDpTJT5llQLaLMHwshY7UDgvD+VujDC96WWWsBtSAE5FnChFnAeUkDMdAvw88EqTNT5SYXpTlgPaRQM1AIGorkolNnoUS1gJHigCX48SaoF3Asuspg4Mz0U8+FTgIkCG01V09kwBQP8xG5ofD5AXeirkPEJSUlwSVIfP5ykVQNaggvz+k7prTvVgDKF8BnUXP4kqgEe/257E8Ig7EE1gA8g2stBTz7FLxqrB3SIeYaeQ2IG6gE5l2+Cmt5MGOfP4KsGiH8DOYWOoujnDY2ALHF3810goZFOQDVBTFx9Uj7eI6bp6QTgnLjeGGq6KeJuoRUQixN3pDYWyz1Rva8XIL5UPFQZCsmG3gV7R+dieS+Jd3iHLglce7oBuCOhp3zwHLxPQpfQDvBOSKjZqUIml3ZJ6AD6AajFSZJwewWR8ZPsEY26SQDaJOMeZP23w6bTJ6kBjAJQILm9hzqm7otu4G+nhgGxIQUlPLKzL7GhbxqAboMCuN2XXd+lAL0ajAMwclV+FD6jAPEy5ghAlhfwX2FODX445gHKxyN++fs64PUHmDMAbbYN2DlKk2QaScwdgMs4SZxMv4OJJSoIIQBl2Qtk3gk4qiOUANRPJQHB+0A6j5AC4J27QQEZ4eZPAsYBXFk0N/YD7iUrxRBqALxOTzoMC3x8lCFlfkMjuz8iLfk6fzQCQgjg8q3ZEd8RzUVuKelBh96Nzcc3qelL1V+2zfRv1xc56Ino3tpdPT7cd//MspfTrD/7R6p4W4O2qLMObfnyIHvvYcrPtkZjDybW7d/eb32Bg/UlHnYXuXz5CMt8rC90sr7Uy/5iN+vL/ewveLS/5NNKwcbyR1r2a3/h8wdY+v3L2tZC5oUvW2uO1M7qyvp/Xv6/48z4CTxjJEfyjEaMAAAAAElFTkSuQmCC "; #[cfg(target_os = "macos")] -pub const ORG: &str = "com.carriez"; +lazy_static::lazy_static! { + pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned())); +} type Size = (i32, i32, i32, i32); @@ -35,10 +36,13 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); + pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); } #[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); + pub static ref APP_HOME_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', @@ -256,13 +260,13 @@ impl Config { } fn file_(suffix: &str) -> PathBuf { - let name = format!("{}{}", APP_NAME, suffix); + let name = format!("{}{}", *APP_NAME.read().unwrap(), suffix); Self::path(name).with_extension("toml") } pub fn get_home() -> PathBuf { #[cfg(any(target_os = "android", target_os = "ios"))] - return Self::path(""); + return Self::path(APP_HOME_DIR.read().unwrap().as_str()); if let Some(path) = dirs_next::home_dir() { patch(path) } else if let Ok(path) = std::env::current_dir() { @@ -282,9 +286,9 @@ impl Config { #[cfg(not(target_os = "macos"))] let org = ""; #[cfg(target_os = "macos")] - let org = ORG; + let org = ORG.read().unwrap().clone(); // /var/root for root - if let Some(project) = ProjectDirs::from("", org, APP_NAME) { + if let Some(project) = ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) { let mut path = patch(project.config_dir().to_path_buf()); path.push(p); return path; @@ -297,14 +301,14 @@ impl Config { #[cfg(target_os = "macos")] { if let Some(path) = dirs_next::home_dir().as_mut() { - path.push(format!("Library/Logs/{}", APP_NAME)); + path.push(format!("Library/Logs/{}", *APP_NAME.read().unwrap())); return path.clone(); } } #[cfg(target_os = "linux")] { let mut path = Self::get_home(); - path.push(format!(".local/share/logs/{}", APP_NAME)); + path.push(format!(".local/share/logs/{}", *APP_NAME.read().unwrap())); std::fs::create_dir_all(&path).ok(); return path; } @@ -322,12 +326,20 @@ impl Config { // \\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) + format!( + "\\\\.\\pipe\\{}\\query{}", + *APP_NAME.read().unwrap(), + postfix + ) } #[cfg(not(windows))] { use std::os::unix::fs::PermissionsExt; - let mut path: PathBuf = format!("/tmp/{}", APP_NAME).into(); + #[cfg(target_os = "android")] + let mut path: PathBuf = + format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into(); + #[cfg(not(target_os = "android"))] + let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); fs::create_dir(&path).ok(); fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); path.push(format!("ipc{}", postfix)); @@ -351,7 +363,10 @@ impl Config { pub fn get_rendezvous_server() -> String { let mut rendezvous_server = Self::get_option("custom-rendezvous-server"); if rendezvous_server.is_empty() { - rendezvous_server = CONFIG2.write().unwrap().rendezvous_server.clone(); + rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); + } + if rendezvous_server.is_empty() { + rendezvous_server = CONFIG2.read().unwrap().rendezvous_server.clone(); } if rendezvous_server.is_empty() { rendezvous_server = Self::get_rendezvous_servers() @@ -370,6 +385,10 @@ impl Config { if !s.is_empty() { return vec![s]; } + let s = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); + 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") @@ -446,7 +465,13 @@ impl Config { fn get_auto_id() -> Option { #[cfg(any(target_os = "android", target_os = "ios"))] - return None; + { + return Some( + rand::thread_rng() + .gen_range(1_000_000_000..2_000_000_000) + .to_string(), + ); + } let mut id = 0u32; #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Ok(Some(ma)) = mac_address::get_mac_address() { @@ -531,6 +556,15 @@ impl Config { id } + pub fn get_id_or(b: String) -> String { + let a = CONFIG.read().unwrap().id.clone(); + if a.is_empty() { + b + } else { + a + } + } + pub fn get_options() -> HashMap { CONFIG2.read().unwrap().options.clone() } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 6fad26037..475f4dfc6 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -430,10 +430,11 @@ pub fn new_error(id: i32, err: T, file_num: i32) -> Me } #[inline] -pub fn new_dir(id: i32, files: Vec) -> Message { +pub fn new_dir(id: i32, path: String, files: Vec) -> Message { let mut resp = FileResponse::new(); resp.set_dir(FileDirectory { id, + path, entries: files.into(), ..Default::default() }); diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index b8472ba52..0a9dace0c 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -27,7 +27,7 @@ pub use anyhow::{self, bail}; pub use futures_util; pub mod config; pub mod fs; -pub use lazy_static; +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub use mac_address; pub use rand; pub use regex; @@ -35,6 +35,7 @@ pub use sodiumoxide; pub use tokio_socks; pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::TargetAddr; +pub use lazy_static; #[cfg(feature = "quic")] pub type Stream = quic::Connection; @@ -179,6 +180,12 @@ where Ok(io::BufReader::new(file).lines()) } +pub fn is_valid_custom_id(id: &str) -> bool { + regex::Regex::new(r"^[a-zA-Z]\w{5,15}$") + .unwrap() + .is_match(id) +} + pub fn get_version_number(v: &str) -> i64 { let mut n = 0; for x in v.split(".") { diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index f465a0e23..033861800 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -14,7 +14,7 @@ pub enum FramedSocket { ProxySocks(Socks5UdpFramed), } -fn new_socket(addr: SocketAddr, reuse: bool) -> Result { +fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result { let socket = match addr { SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None), SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None), @@ -27,6 +27,14 @@ fn new_socket(addr: SocketAddr, reuse: bool) -> Result { socket.set_reuse_port(true)?; socket.set_reuse_address(true)?; } + if buf_size > 0 { + socket.set_recv_buffer_size(buf_size).ok(); + } + log::info!( + "Receive buf size of udp {}: {:?}", + addr, + socket.recv_buffer_size() + ); socket.bind(&addr.into())?; Ok(socket) } @@ -40,7 +48,7 @@ impl FramedSocket { #[allow(clippy::never_loop)] pub async fn new_reuse(addr: T) -> ResultType { for addr in addr.to_socket_addrs()? { - let socket = new_socket(addr, true)?.into_udp_socket(); + let socket = new_socket(addr, true, 0)?.into_udp_socket(); return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(socket)?, BytesCodec::new(), @@ -49,6 +57,19 @@ impl FramedSocket { bail!("could not resolve to any address"); } + pub async fn new_with_buf_size( + addr: T, + buf_size: usize, + ) -> ResultType { + for addr in addr.to_socket_addrs()? { + return Ok(Self::Direct(UdpFramed::new( + UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, + BytesCodec::new(), + ))); + } + bail!("could not resolve to any address"); + } + pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( proxy: P, local: T, diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index eb3faed24..00c4509ab 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -23,7 +23,13 @@ version = "0.3" default-features = true features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser"] -[dev-dependencies] +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.10" +jni = "0.19" +lazy_static = "1.4" +log = "0.4" + +[target.'cfg(not(target_os = "android"))'.dev-dependencies] repng = "0.2" docopt = "1.1" webm = "1.0" @@ -33,7 +39,6 @@ quest = "0.3" [build-dependencies] target_build_utils = "0.3" bindgen = "0.59" -vcpkg = "0.2" [target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9", optional = true } diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index 1f7aafe7e..93ea41ca7 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -4,35 +4,40 @@ use std::{ }; fn find_package(name: &str) -> Vec { - let library = vcpkg::find_package(name).expect("Failed to find package"); - println!("cargo:info={}", library.vcpkg_triplet); //TODO - let lib_name = name.trim_start_matches("lib").to_string(); - println!("{}", format!("cargo:rustc-link-lib=static={}", lib_name)); - - match ( - library.link_paths.as_slice(), - library.include_paths.as_slice(), - ) { - ([link_search, ..], [include, ..]) => { - println!( - "{}", - format!("cargo:rustc-link-search={}", link_search.display()) - ); - println!("{}", format!("cargo:include={}", include.display())); - } - _ => { - panic!( - "{}", - if library.link_paths.is_empty() { - "link path not found" - } else { - "include path not found" - } - ) - } + 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(); } - - library.include_paths + let mut target = if target_os == "macos" { + "x64-osx".to_owned() + } else if target_os == "windows" { + "x64-windows-static".to_owned() + } else { + format!("{}-{}", target_arch, target_os) + }; + if target_arch == "x86" { + target = target.replace("x64", "x86"); + } + println!("cargo:info={}", target); + path.push("installed"); + path.push(target); + let lib = name.trim_start_matches("lib").to_string(); + println!("{}", format!("cargo:rustc-link-lib=static={}", 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( @@ -94,8 +99,10 @@ fn main() { // 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" { + if target_os == "ios" { // nothing + } else if target_os == "android" { + println!("cargo:rustc-cfg=android"); } else if cfg!(windows) { // The first choice is Windows because DXGI is amazing. println!("cargo:rustc-cfg=dxgi"); diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs new file mode 100644 index 000000000..7723d3ace --- /dev/null +++ b/libs/scrap/src/android/ffi.rs @@ -0,0 +1,235 @@ +use jni::objects::JByteBuffer; +use jni::objects::JString; +use jni::objects::JValue; +use jni::sys::jboolean; +use jni::JNIEnv; +use jni::{ + objects::{GlobalRef, JClass, JObject}, + JavaVM, +}; + +use jni::errors::{Error as JniError, Result as JniResult}; +use lazy_static::lazy_static; +use std::ops::Not; +use std::sync::atomic::{AtomicPtr, Ordering::SeqCst}; +use std::sync::{Mutex, RwLock}; +use std::time::{Duration, Instant}; +lazy_static! { + static ref JVM: RwLock> = RwLock::new(None); + static ref MAIN_SERVICE_CTX: RwLock> = RwLock::new(None); // MainService -> video service / audio service / info + static ref INPUT_CTX: RwLock> = RwLock::new(None); + static ref VIDEO_RAW: Mutex = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT)); + static ref AUDIO_RAW: Mutex = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT)); +} + +const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100); +const MAX_AUDIO_FRAME_TIMEOUT: Duration = Duration::from_millis(1000); + +struct FrameRaw { + name: &'static str, + ptr: AtomicPtr, + len: usize, + last_update: Instant, + timeout: Duration, + enable: bool, +} + +impl FrameRaw { + fn new(name: &'static str, timeout: Duration) -> Self { + FrameRaw { + name, + ptr: AtomicPtr::default(), + len: 0, + last_update: Instant::now(), + timeout, + enable: false, + } + } + + fn set_enable(&mut self, value: bool) { + self.enable = value; + } + + fn update(&mut self, data: &mut [u8]) { + if self.enable.not() { + return; + } + self.len = data.len(); + self.ptr.store(data.as_mut_ptr(), SeqCst); + self.last_update = Instant::now(); + } + + // take inner data as slice + // release when success + fn take<'a>(&mut self) -> Option<&'a [u8]> { + if self.enable.not() { + return None; + } + let ptr = self.ptr.load(SeqCst); + if ptr.is_null() || self.len == 0 { + None + } else { + if self.last_update.elapsed() > self.timeout { + log::trace!("Failed to take {} raw,timeout!", self.name); + return None; + } + let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) }; + self.release(); + Some(slice) + } + } + + fn release(&mut self) { + self.len = 0; + self.ptr.store(std::ptr::null_mut(), SeqCst); + } +} + +pub fn get_video_raw<'a>() -> Option<&'a [u8]> { + VIDEO_RAW.lock().ok()?.take() +} + +pub fn get_audio_raw<'a>() -> Option<&'a [u8]> { + AUDIO_RAW.lock().ok()?.take() +} + +#[no_mangle] +pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpdate( + env: JNIEnv, + _class: JClass, + buffer: JObject, +) { + let jb = JByteBuffer::from(buffer); + let slice = env.get_direct_buffer_address(jb).unwrap(); + VIDEO_RAW.lock().unwrap().update(slice); +} + +#[no_mangle] +pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpdate( + env: JNIEnv, + _class: JClass, + buffer: JObject, +) { + let jb = JByteBuffer::from(buffer); + let slice = env.get_direct_buffer_address(jb).unwrap(); + AUDIO_RAW.lock().unwrap().update(slice); +} + +#[no_mangle] +pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_setFrameRawEnable( + env: JNIEnv, + _class: JClass, + name: JString, + value: jboolean, +) { + if let Ok(name) = env.get_string(name) { + let name: String = name.into(); + let value = value.eq(&1); + if name.eq("video") { + VIDEO_RAW.lock().unwrap().set_enable(value); + } else if name.eq("audio") { + AUDIO_RAW.lock().unwrap().set_enable(value); + } + }; +} + +#[no_mangle] +pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init( + env: JNIEnv, + _class: JClass, + ctx: JObject, +) { + log::debug!("MainService init from java"); + let jvm = env.get_java_vm().unwrap(); + + *JVM.write().unwrap() = Some(jvm); + + let context = env.new_global_ref(ctx).unwrap(); + *MAIN_SERVICE_CTX.write().unwrap() = Some(context); +} + +#[no_mangle] +pub extern "system" fn Java_com_carriez_flutter_1hbb_InputService_init( + env: JNIEnv, + _class: JClass, + ctx: JObject, +) { + log::debug!("InputService init from java"); + let jvm = env.get_java_vm().unwrap(); + + *JVM.write().unwrap() = Some(jvm); + + let context = env.new_global_ref(ctx).unwrap(); + *INPUT_CTX.write().unwrap() = Some(context); +} + +pub fn call_input_service_mouse_input(mask: i32, x: i32, y: i32) -> JniResult<()> { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + INPUT_CTX.read().unwrap().as_ref(), + ) { + let env = jvm.attach_current_thread_as_daemon()?; + env.call_method( + ctx, + "rustMouseInput", + "(III)V", + &[JValue::Int(mask), JValue::Int(x), JValue::Int(y)], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_main_service_get_by_name(name: &str) -> JniResult { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + MAIN_SERVICE_CTX.read().unwrap().as_ref(), + ) { + let env = jvm.attach_current_thread_as_daemon()?; + let name = env.new_string(name)?; + let res = env + .call_method( + ctx, + "rustGetByName", + "(Ljava/lang/String;)Ljava/lang/String;", + &[JValue::Object(name.into())], + )? + .l()?; + let res = env.get_string(res.into())?; + let res = res.to_string_lossy().to_string(); + return Ok(res); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_main_service_set_by_name( + name: &str, + arg1: Option<&str>, + arg2: Option<&str>, +) -> JniResult<()> { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + MAIN_SERVICE_CTX.read().unwrap().as_ref(), + ) { + let env = jvm.attach_current_thread_as_daemon()?; + let name = env.new_string(name)?; + let arg1 = env.new_string(arg1.unwrap_or(""))?; + let arg2 = env.new_string(arg2.unwrap_or(""))?; + + env.call_method( + ctx, + "rustSetByName", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + &[ + JValue::Object(name.into()), + JValue::Object(arg1.into()), + JValue::Object(arg2.into()), + ], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} diff --git a/libs/scrap/src/android/mod.rs b/libs/scrap/src/android/mod.rs new file mode 100644 index 000000000..92d84676e --- /dev/null +++ b/libs/scrap/src/android/mod.rs @@ -0,0 +1,6 @@ +pub mod ffi; +use std::sync::RwLock; + +pub use ffi::*; + +use lazy_static::lazy_static; diff --git a/libs/scrap/src/common/android.rs b/libs/scrap/src/common/android.rs new file mode 100644 index 000000000..555e6cd7b --- /dev/null +++ b/libs/scrap/src/common/android.rs @@ -0,0 +1,129 @@ +use crate::android::ffi::*; +use crate::rgba_to_i420; +use lazy_static::lazy_static; +use std::io; +use std::sync::Mutex; + +lazy_static! { + static ref SCREEN_SIZE: Mutex<(u16, u16)> = Mutex::new((0, 0)); +} + +pub struct Capturer { + display: Display, + bgra: Vec, + saved_raw_data: Vec, // for faster compare and copy +} + +impl Capturer { + pub fn new(display: Display, _yuv: bool) -> io::Result { + Ok(Capturer { + display, + bgra: Vec::new(), + saved_raw_data: Vec::new(), + }) + } + + pub fn width(&self) -> usize { + self.display.width() as usize + } + + pub fn height(&self) -> usize { + self.display.height() as usize + } + + pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> { + if let Some(buf) = get_video_raw() { + crate::would_block_if_equal(&mut self.saved_raw_data, buf)?; + rgba_to_i420(self.width(), self.height(), buf, &mut self.bgra); + Ok(Frame::RAW(&self.bgra)) + } else { + return Err(io::ErrorKind::WouldBlock.into()); + } + } +} + +pub enum Frame<'a> { + RAW(&'a [u8]), + VP9(&'a [u8]), + Empty, +} + +pub struct Display { + default: bool, + rect: Rect, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +struct Rect { + pub x: i16, + pub y: i16, + pub w: u16, + pub h: u16, +} + +impl Display { + pub fn primary() -> io::Result { + let mut size = SCREEN_SIZE.lock().unwrap(); + if size.0 == 0 || size.1 == 0 { + let (w, h) = get_size().unwrap_or((0, 0)); + size.0 = w; + size.1 = h; + } + Ok(Display { + default: true, + rect: Rect { + x: 0, + y: 0, + w: size.0, + h: size.1, + }, + }) + } + + pub fn all() -> io::Result> { + Ok(vec![Display::primary()?]) + } + + pub fn width(&self) -> usize { + self.rect.w as usize + } + + pub fn height(&self) -> usize { + self.rect.h as usize + } + + pub fn origin(&self) -> (i32, i32) { + let r = self.rect; + (r.x as _, r.y as _) + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + self.default + } + + pub fn name(&self) -> String { + "Android".into() + } + + pub fn refresh_size() { + let mut size = SCREEN_SIZE.lock().unwrap(); + let (w, h) = get_size().unwrap_or((0, 0)); + size.0 = w; + size.1 = h; + } +} + +fn get_size() -> Option<(u16, u16)> { + let res = call_main_service_get_by_name("screen_size").ok()?; + if res.len() > 0 { + let mut sp = res.split(":"); + let w = sp.next()?.parse::().ok()?; + let h = sp.next()?.parse::().ok()?; + return Some((w, h)); + } + None +} diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 78e328f5b..dd2b4295a 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -19,7 +19,10 @@ cfg_if! { } else if #[cfg(dxgi)] { mod dxgi; pub use self::dxgi::*; - } else { + } else if #[cfg(android)] { + mod android; + pub use self::android::*; + }else { //TODO: Fallback implementation. } } @@ -42,4 +45,4 @@ pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<() old.resize(b.len(), 0); old.copy_from_slice(b); Ok(()) -} +} \ No newline at end of file diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs index 8db2a5788..4d481ed26 100644 --- a/libs/scrap/src/lib.rs +++ b/libs/scrap/src/lib.rs @@ -20,4 +20,7 @@ pub mod wayland; #[cfg(dxgi)] pub mod dxgi; +#[cfg(android)] +pub mod android; + mod common; diff --git a/src/cli.rs b/src/cli.rs index 1ce11878f..5aebea432 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -79,14 +79,20 @@ impl Interface for Session { } #[tokio::main(flavor = "current_thread")] -pub async fn start_one_port_forward(id: String, port: i32, remote_host: String, remote_port: i32) { +pub async fn start_one_port_forward( + id: String, + port: i32, + remote_host: String, + remote_port: i32, + key: String, +) { 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 + crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver, &key).await { log::error!("Failed to listen on {}: {}", port, err); } diff --git a/src/client.rs b/src/client.rs index 7f71b1bbf..cfe02c20c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -29,7 +29,8 @@ use std::{ sync::{mpsc, Arc, RwLock}, }; use uuid::Uuid; - +pub mod file_trait; +pub use file_trait::FileManager; pub const SEC30: Duration = Duration::from_secs(30); pub struct Client; @@ -101,8 +102,13 @@ impl Drop for OboePlayer { } impl Client { - pub async fn start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> { - match Self::_start(peer, conn_type).await { + pub async fn start( + peer: &str, + key: &str, + token: &str, + conn_type: ConnType, + ) -> ResultType<(Stream, bool)> { + match Self::_start(peer, key, token, conn_type).await { Err(err) => { let err_str = err.to_string(); if err_str.starts_with("Failed") { @@ -115,7 +121,12 @@ impl Client { } } - async fn _start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> { + async fn _start( + peer: &str, + key: &str, + token: &str, + conn_type: ConnType, + ) -> 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(); if crate::is_ip(peer) { @@ -150,7 +161,9 @@ impl Client { 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(), + token: token.to_owned(), nat_type: nat_type.into(), + licence_key: key.to_owned(), conn_type: conn_type.into(), ..Default::default() }); @@ -195,9 +208,9 @@ impl Client { ); signed_id_pk = rr.get_pk().into(); let mut conn = - Self::create_relay(peer, rr.uuid, rr.relay_server, conn_type) + Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) .await?; - Self::secure_connection(peer, signed_id_pk, &mut conn).await?; + Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?; return Ok((conn, false)); } _ => { @@ -235,6 +248,8 @@ impl Client { peer_nat_type, my_nat_type, is_local, + key, + token, conn_type, ) .await @@ -251,6 +266,8 @@ impl Client { peer_nat_type: NatType, my_nat_type: i32, is_local: bool, + key: &str, + token: &str, conn_type: ConnType, ) -> ResultType<(Stream, bool)> { let direct_failures = PeerConfig::load(peer_id).direct_failures; @@ -297,6 +314,8 @@ impl Client { relay_server.to_owned(), rendezvous_server, !signed_id_pk.is_empty(), + key, + token, conn_type, ) .await; @@ -318,12 +337,21 @@ impl Client { } let mut conn = conn?; log::info!("{:?} used to establish connection", start.elapsed()); - Self::secure_connection(peer_id, signed_id_pk, &mut conn).await?; + Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; Ok((conn, direct)) } - async fn secure_connection(peer_id: &str, signed_id_pk: Vec, conn: &mut Stream) -> ResultType<()> { - let rs_pk = get_rs_pk("OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="); + async fn secure_connection( + peer_id: &str, + signed_id_pk: Vec, + key: &str, + conn: &mut Stream, + ) -> ResultType<()> { + let rs_pk = get_rs_pk(if key.is_empty() { + "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" + } else { + key + }); let mut sign_pk = None; if !signed_id_pk.is_empty() && rs_pk.is_some() { if let Ok((id, pk)) = decode_id_pk(&signed_id_pk, &rs_pk.unwrap()) { @@ -395,6 +423,8 @@ impl Client { relay_server: String, rendezvous_server: &str, secure: bool, + key: &str, + token: &str, conn_type: ConnType, ) -> ResultType { let any_addr = Config::get_any_listen_addr(); @@ -419,6 +449,7 @@ impl Client { ); msg_out.set_request_relay(RequestRelay { id: peer.to_owned(), + token: token.to_owned(), uuid: uuid.clone(), relay_server: relay_server.clone(), secure, @@ -440,13 +471,14 @@ impl Client { if !succeed { bail!("Timeout"); } - Self::create_relay(peer, uuid, relay_server, conn_type).await + Self::create_relay(peer, uuid, relay_server, key, conn_type).await } async fn create_relay( peer: &str, uuid: String, relay_server: String, + key: &str, conn_type: ConnType, ) -> ResultType { let mut conn = socket_client::connect_tcp( @@ -458,6 +490,7 @@ impl Client { .with_context(|| "Failed to connect to relay server")?; let mut msg_out = RendezvousMessage::new(); msg_out.set_request_relay(RequestRelay { + licence_key: key.to_owned(), id: peer.to_owned(), uuid, conn_type: conn_type.into(), @@ -498,11 +531,10 @@ impl AudioHandler { if !spec.is_valid() { bail!("Invalid audio format"); } - use hbb_common::config::APP_NAME; self.simple = Some(Simple::new( None, // Use the default server - APP_NAME, // Our application’s name + &crate::get_app_name(), // Our application’s name Direction::Playback, // We want a playback stream None, // Use the default device "playback", // Description of our stream @@ -693,7 +725,7 @@ impl VideoHandler { #[derive(Default)] pub struct LoginConfigHandler { id: String, - is_file_transfer: bool, + pub is_file_transfer: bool, is_port_forward: bool, hash: Hash, password: Vec, // remember password for reconnect @@ -701,6 +733,7 @@ pub struct LoginConfigHandler { config: PeerConfig, pub port_forward: (String, i32), pub version: i64, + pub conn_id: i32, } impl Deref for LoginConfigHandler { @@ -726,6 +759,17 @@ impl LoginConfigHandler { self.config = config; } + pub fn should_auto_login(&self) -> String { + let l = self.lock_after_session_end; + let a = !self.get_option("auto-login").is_empty(); + let p = self.get_option("os-password"); + if !p.is_empty() && l && a { + p + } else { + "".to_owned() + } + } + fn load_config(&self) -> PeerConfig { load_config(&self.id) } @@ -1002,11 +1046,12 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } + self.conn_id = pi.conn_id; // no matter if change, for update file time self.save_config(config); } - fn get_remote_dir(&self) -> String { + pub fn get_remote_dir(&self) -> String { serde_json::from_str::>(&self.get_option("remote_dir")) .unwrap_or_default() .remove(&self.info.username) @@ -1027,7 +1072,7 @@ impl LoginConfigHandler { 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(); + let my_id = Config::get_id_or(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 { @@ -1127,6 +1172,83 @@ pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { } } +// mask = buttons << 3 | type +// type, 1: down, 2: up, 3: wheel +// buttons, 1: left, 2: right, 4: middle +#[inline] +pub fn send_mouse( + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + interface: &impl Interface, +) { + 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); + interface.send(Data::Message(msg_out)); +} + +fn activate_os(interface: &impl Interface) { + send_mouse(0, 0, 0, false, false, false, false, interface); + std::thread::sleep(Duration::from_millis(50)); + send_mouse(0, 3, 3, false, false, false, false, interface); + std::thread::sleep(Duration::from_millis(50)); + send_mouse(1 | 1 << 3, 0, 0, false, false, false, false, interface); + send_mouse(2 | 1 << 3, 0, 0, false, false, false, false, interface); + /* + let mut key_event = KeyEvent::new(); + // do not use Esc, which has problem with Linux + key_event.set_control_key(ControlKey::RightArrow); + key_event.press = true; + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event.clone()); + interface.send(Data::Message(msg_out.clone())); + */ +} + +pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { + std::thread::spawn(move || { + _input_os_password(p, activate, interface); + }); +} + +fn _input_os_password(p: String, activate: bool, interface: impl Interface) { + if activate { + activate_os(&interface); + std::thread::sleep(Duration::from_millis(1200)); + } + let mut key_event = KeyEvent::new(); + key_event.press = true; + let mut msg_out = Message::new(); + key_event.set_seq(p); + msg_out.set_key_event(key_event.clone()); + interface.send(Data::Message(msg_out.clone())); + key_event.set_control_key(ControlKey::Return); + msg_out.set_key_event(key_event); + interface.send(Data::Message(msg_out)); +} + pub async fn handle_hash( lc: Arc>, hash: Hash, @@ -1175,6 +1297,7 @@ pub async fn handle_login_from_ui( #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { + fn send(&self, data: Data); 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); diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs new file mode 100644 index 000000000..0e6e97e42 --- /dev/null +++ b/src/client/file_trait.rs @@ -0,0 +1,88 @@ +use super::{Data, Interface}; +use hbb_common::{ + fs, + message_proto::*, +}; + +pub trait FileManager: Interface { + fn get_home_dir(&self) -> String{ + fs::get_home_as_string() + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn read_dir(&self,path: String, include_hidden: bool) -> sciter::Value { + match fs::read_dir(&fs::get_path(&path), include_hidden) { + Err(_) => sciter::Value::null(), + Ok(fd) => { + use crate::ui::remote::make_fd; + let mut m = make_fd(0, &fd.entries.to_vec(), false); + m.set_item("path", path); + m + } + } + } + + #[cfg(any(target_os = "android", target_os = "ios"))] + fn read_dir(&self,path: &str, include_hidden: bool) -> String { + use crate::mobile::make_fd_to_json; + match fs::read_dir(&fs::get_path(path), include_hidden){ + Ok(fd) => make_fd_to_json(fd), + Err(_)=>"".into() + } + } + + fn cancel_job(&mut self, id: i32) { + self.send(Data::CancelJob(id)); + } + + fn read_remote_dir(&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 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))); + } +} diff --git a/src/common.rs b/src/common.rs index 6d2d9c373..2a865afbb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,9 +1,10 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; use hbb_common::{ allow_err, anyhow::bail, compress::{compress as compress_func, decompress}, - config::{Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, + config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, protobuf::Message as _, @@ -54,6 +55,7 @@ pub fn create_clipboard_msg(content: String) -> Message { msg } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn check_clipboard( ctx: &mut ClipboardContext, old: Option<&Arc>>, @@ -73,6 +75,7 @@ pub fn check_clipboard( None } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { decompress(&clipboard.content) @@ -80,15 +83,16 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) clipboard.content }; if let Ok(content) = String::from_utf8(content) { + if content.is_empty() { + // ctx.set_text may crash if content is empty + return; + } 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(); - if !content.is_empty() { - // empty content make ctx.set_text crash - allow_err!(ctx.set_text(content)); - } + allow_err!(ctx.set_text(content)); log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } Err(err) => { @@ -234,7 +238,10 @@ pub fn test_nat_type() { #[tokio::main(flavor = "current_thread")] async fn test_nat_type_() -> ResultType { log::info!("Testing nat ..."); + #[cfg(not(any(target_os = "android", target_os = "ios")))] let is_direct = crate::ipc::get_socks_async(1_000).await.is_none(); // sync socks BTW + #[cfg(any(target_os = "android", target_os = "ios"))] + let is_direct = Config::get_socks().is_none(); // sync socks BTW if !is_direct { Config::set_nat_type(NatType::SYMMETRIC as _); return Ok(true); @@ -451,12 +458,21 @@ async fn _check_software_update() -> hbb_common::ResultType<()> { Ok(()) } +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub fn get_icon() -> String { + hbb_common::config::ICON.to_owned() +} + +pub fn get_app_name() -> String { + hbb_common::config::APP_NAME.read().unwrap().clone() +} + #[cfg(target_os = "macos")] pub fn get_full_name() -> String { format!( "{}.{}", - hbb_common::config::ORG, - hbb_common::config::APP_NAME, + hbb_common::config::ORG.read().unwrap(), + hbb_common::config::APP_NAME.read().unwrap(), ) } @@ -466,7 +482,115 @@ pub fn is_ip(id: &str) -> bool { .is_match(id) } -#[inline] -pub fn get_app_name() -> &'static str { - hbb_common::config::APP_NAME +pub fn is_setup(name: &str) -> bool { + name.to_lowercase().ends_with("putes.exe") || name.to_lowercase().ends_with("安装.exe") +} + +pub fn get_uuid() -> Vec { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(id) = machine_uid::get() { + return id.into(); + } + Config::get_key_pair().1 +} + +pub fn get_custom_rendezvous_server(custom: String) -> String { + if !custom.is_empty() { + return custom; + } + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + if !lic.host.is_empty() { + return lic.host.clone(); + } + } + if !config::PROD_RENDEZVOUS_SERVER.read().unwrap().is_empty() { + return config::PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); + } + "".to_owned() +} + +pub fn get_api_server(api: String, custom: String) -> String { + if !api.is_empty() { + return api.to_owned(); + } + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + if !lic.api.is_empty() { + return lic.api.clone(); + } + } + let s = get_custom_rendezvous_server(custom); + if !s.is_empty() { + if s.contains(':') { + let tmp: Vec<&str> = s.split(":").collect(); + if tmp.len() == 2 { + let port: u16 = tmp[1].parse().unwrap_or(0); + if port > 2 { + return format!("http://{}:{}", tmp[0], port - 2); + } + } + } else { + return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + } + } + "https://admin.rustdesk.com".to_owned() +} + +pub fn get_audit_server(api: String, custom: String) -> String { + let url = get_api_server(api, custom); + if url.is_empty() || url.contains("rustdesk.com") { + return "".to_owned(); + } + format!("{}/api/audit", url) +} + +pub async fn post_request(url: String, body: String, header: &str) -> ResultType { + #[cfg(not(target_os = "linux"))] + { + let mut req = reqwest::Client::new().post(url); + if !header.is_empty() { + let tmp: Vec<&str> = header.split(": ").collect(); + if tmp.len() == 2 { + req = req.header(tmp[0], tmp[1]); + } + } + req = req.header("Content-Type", "application/json"); + let to = std::time::Duration::from_secs(12); + Ok(req.body(body).timeout(to).send().await?.text().await?) + } + #[cfg(target_os = "linux")] + { + let mut data = vec![ + "curl", + "-sS", + "-X", + "POST", + &url, + "-H", + "Content-Type: application/json", + "-d", + &body, + "--connect-timeout", + "12", + ]; + if !header.is_empty() { + data.push("-H"); + data.push(header); + } + let output = async_process::Command::new("curl") + .args(&data) + .output() + .await?; + let res = String::from_utf8_lossy(&output.stdout).to_string(); + if !res.is_empty() { + return Ok(res); + } + bail!(String::from_utf8_lossy(&output.stderr).to_string()); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn post_request_sync(url: String, body: String, header: &str) -> ResultType { + post_request(url, body, header).await } diff --git a/src/ipc.rs b/src/ipc.rs index c120cc253..f09367a1a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,4 +1,5 @@ use crate::rendezvous_mediator::RendezvousMediator; +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipbaordFile; use hbb_common::{ allow_err, bail, bytes, @@ -15,7 +16,7 @@ use parity_tokio_ipc::{ Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, }; use serde_derive::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{collections::HashMap, sync::atomic::Ordering}; #[cfg(not(windows))] use std::{fs::File, io::prelude::*}; @@ -84,6 +85,8 @@ pub enum Data { enabled: bool, }, SystemInfo(Option), + ClickTime(i64), + MouseMoveTime(i64), Authorize, Close, SAS, @@ -201,8 +204,17 @@ async fn handle(data: Data, stream: &mut Connection) { ); allow_err!(stream.send(&Data::SystemInfo(Some(info))).await); } + Data::ClickTime(_) => { + let t = crate::server::CLICK_TIME.load(Ordering::SeqCst); + allow_err!(stream.send(&Data::ClickTime(t)).await); + } + Data::MouseMoveTime(_) => { + let t = crate::server::MOUSE_MOVE_TIME.load(Ordering::SeqCst); + allow_err!(stream.send(&Data::MouseMoveTime(t)).await); + } Data::Close => { log::info!("Receive close message"); + #[cfg(not(target_os = "android"))] crate::server::input_service::fix_key_down_timeout_at_exit(); std::process::exit(0); } @@ -442,13 +454,17 @@ async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType ResultType<()> { +pub async fn set_config_async(name: &str, value: String) -> ResultType<()> { let mut c = connect(1000, "").await?; c.send_config(name, value).await?; Ok(()) } +#[tokio::main(flavor = "current_thread")] +pub async fn set_config(name: &str, value: String) -> ResultType<()> { + set_config_async(name, value).await +} + pub fn set_password(v: String) -> ResultType<()> { Config::set_password(&v); set_config("password", v) @@ -498,13 +514,17 @@ async fn get_options_(ms_timeout: u64) -> ResultType> { } } -#[tokio::main(flavor = "current_thread")] -pub async fn get_options() -> HashMap { +pub async fn get_options_async() -> 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) { +#[tokio::main(flavor = "current_thread")] +pub async fn get_options() -> HashMap { + get_options_async().await +} + +pub async fn get_option_async(key: &str) -> String { + if let Some(v) = get_options_async().await.get(key) { v.clone() } else { "".to_owned() @@ -550,6 +570,13 @@ pub async fn get_nat_type(ms_timeout: u64) -> i32 { .unwrap_or(Config::get_nat_type()) } +pub async fn get_rendezvous_servers(ms_timeout: u64) -> Vec { + if let Ok(Some(v)) = get_config_async("rendezvous_servers", ms_timeout).await { + return v.split(',').map(|x| x.to_owned()).collect(); + } + return Config::get_rendezvous_servers(); +} + #[inline] async fn get_socks_(ms_timeout: u64) -> ResultType> { let mut c = connect(ms_timeout, "").await?; @@ -584,63 +611,3 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { .await?; Ok(()) } - -/* -static mut SHARED_MEMORY: *mut i64 = std::ptr::null_mut(); - -pub fn initialize_shared_memory(create: bool) { - let mut shmem_flink = "shared-memory".to_owned(); - if cfg!(windows) { - let df = "C:\\ProgramData"; - let df = if std::path::Path::new(df).exists() { - df.to_owned() - } else { - std::env::var("TEMP").unwrap_or("C:\\Windows\\TEMP".to_owned()) - }; - let df = format!("{}\\{}", df, *hbb_common::config::APP_NAME.read().unwrap()); - std::fs::create_dir(&df).ok(); - shmem_flink = format!("{}\\{}", df, shmem_flink); - } else { - shmem_flink = Config::ipc_path("").replace("ipc", "") + &shmem_flink; - } - use shared_memory::*; - let shmem = if create { - match ShmemConf::new() - .force_create_flink() - .size(16) - .flink(&shmem_flink) - .create() - { - Err(ShmemError::LinkExists) => ShmemConf::new().flink(&shmem_flink).open(), - Ok(m) => Ok(m), - Err(e) => Err(e), - } - } else { - ShmemConf::new().flink(&shmem_flink).open() - }; - if create { - set_all_perm(&shmem_flink); - } - match shmem { - Ok(shmem) => unsafe { - SHARED_MEMORY = shmem.as_ptr() as *mut i64; - std::mem::forget(shmem); - }, - Err(err) => { - log::error!( - "Unable to create or open shmem flink {} : {}", - shmem_flink, - err - ); - } - } -} - -fn set_all_perm(p: &str) { - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o0777)).ok(); - } -} -*/ diff --git a/src/lang.rs b/src/lang.rs index 6a945dde2..472a07f18 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,4 +1,3 @@ -use hbb_common::{config::LocalConfig, log}; use std::ops::Deref; mod cn; @@ -15,12 +14,17 @@ mod id; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn translate(name: String) -> String { let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase(); - log::trace!("The current locale is {}", locale); translate_locale(name, &locale) } pub fn translate_locale(name: String, locale: &str) -> String { - let mut lang = LocalConfig::get_option("lang"); + let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase(); + if lang.is_empty() { + // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android + if locale.starts_with("zh") && (locale.ends_with("CN") || locale.ends_with("SG") || locale.ends_with("Hans")) { + lang = "cn".to_owned(); + } + } if lang.is_empty() { lang = locale .split("-") @@ -38,10 +42,10 @@ pub fn translate_locale(name: String, locale: &str) -> String { "de" => de::T.deref(), "ru" => ru::T.deref(), "eo" => eo::T.deref(), + "id" => id::T.deref(), "ptbr" => ptbr::T.deref(), "br" => ptbr::T.deref(), "pt" => ptbr::T.deref(), - "id" => id::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/template.rs b/src/lang/template.rs index d6eb3d916..17adea637 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -265,7 +265,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", ""), ("android_start_service_tip", ""), ("Account", ""), - ("Quit", ""), - ("Help", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 0f788cb67..c5dd4d1a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,33 +1,39 @@ -#[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; -mod lang; - -#[cfg(windows)] -pub mod clipboard_file; +#[cfg(not(any(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 = "ios")))] +mod server; +#[cfg(not(any(target_os = "ios")))] +pub use self::server::*; +mod client; +#[cfg(not(any(target_os = "ios")))] +mod rendezvous_mediator; +#[cfg(not(any(target_os = "ios")))] +pub use self::rendezvous_mediator::*; +pub mod common; +#[cfg(not(any( 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; +#[cfg(all(windows, feature = "hbbs"))] +mod hbbs; +#[cfg(windows)] +mod license; +#[cfg(windows)] +mod tray; +mod lang; + +#[cfg(windows)] +pub mod clipboard_file; diff --git a/src/lic_main.rs b/src/lic_main.rs new file mode 100644 index 000000000..ed7808970 --- /dev/null +++ b/src/lic_main.rs @@ -0,0 +1,46 @@ +mod license; +use hbb_common::{sodiumoxide::crypto::sign, ResultType}; +use license::*; + +fn gen_license(lic: &License) -> ResultType { + let tmp = serde_json::to_vec::(lic)?; + const SK: &[u8; 64] = &[ + 139, 164, 88, 86, 6, 123, 221, 248, 96, 36, 106, 207, 99, 124, 27, 196, 5, 159, 58, 253, + 238, 94, 3, 184, 237, 236, 122, 59, 205, 95, 6, 189, 88, 168, 68, 104, 60, 5, 163, 198, + 165, 38, 12, 85, 114, 203, 96, 163, 70, 48, 0, 131, 57, 12, 46, 129, 83, 17, 84, 193, 119, + 197, 130, 103, + ]; + let sk = sign::SecretKey(*SK); + let tmp = base64::encode_config(sign::sign(&tmp, &sk), base64::URL_SAFE_NO_PAD); + let tmp: String = tmp.chars().rev().collect(); + Ok(tmp) +} + +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; + } + let api = if args.len() < 3 { + "".to_owned() + } else { + args[2].clone() + }; + if args.len() == 3 { + println!( + "{:?}", + gen_license(&License { + key: args[0].clone(), + host: args[1].clone(), + api, + }) + ); + } + if args.len() == 1 { + println!("{:?}", get_license_from_string(&args[0])); + } +} diff --git a/src/license.rs b/src/license.rs new file mode 100644 index 000000000..f5ec35501 --- /dev/null +++ b/src/license.rs @@ -0,0 +1,30 @@ +use hbb_common::{bail, sodiumoxide::crypto::sign, ResultType}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] +pub struct License { + #[serde(default)] + pub key: String, + #[serde(default)] + pub host: String, + #[serde(default)] + pub api: String, +} + +pub fn get_license_from_string(s: &str) -> ResultType { + let tmp: String = s.chars().rev().collect(); + const PK: &[u8; 32] = &[ + 88, 168, 68, 104, 60, 5, 163, 198, 165, 38, 12, 85, 114, 203, 96, 163, 70, 48, 0, 131, 57, + 12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103, + ]; + let pk = sign::PublicKey(*PK); + let data = base64::decode_config(tmp, base64::URL_SAFE_NO_PAD)?; + if let Ok(lic) = serde_json::from_slice::(&data) { + return Ok(lic); + } + if let Ok(data) = sign::verify(&data, &pk) { + Ok(serde_json::from_slice::(&data)?) + } else { + bail!("sign:verify failed"); + } +} diff --git a/src/main.rs b/src/main.rs index 1e237b804..b5b88a14f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ //#![windows_subsystem = "windows"] use hbb_common::log; -use rustdesk::*; +use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios"))] fn main() { @@ -11,24 +11,40 @@ fn main() { 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() { // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write let mut _async_logger_holder: Option = None; - let mut args: Vec = std::env::args().skip(1).collect(); + let mut args = Vec::new(); + let mut i = 0; + let mut is_setup = false; + for arg in std::env::args() { + if i == 0 && common::is_setup(&arg) { + is_setup = true; + } else if i > 0 { + args.push(arg); + } + i += 1; + } + if is_setup { + if args.is_empty() { + args.push("--install".to_owned()); + } else if args[0] == "--noinstall" { + args.clear(); + } + } if args.len() > 0 && args[0] == "--version" { println!("{}", crate::VERSION); return; } - #[cfg(not(feature = "inline"))] + #[cfg(feature = "inline")] { use hbb_common::env_logger::*; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); } - #[cfg(feature = "inline")] + #[cfg(not(feature = "inline"))] { let mut path = hbb_common::config::Config::log_path(); if args.len() > 0 && args[0].starts_with("--") { @@ -53,7 +69,7 @@ fn main() { } } if args.is_empty() { - std::thread::spawn(move || start_server(false, false)); + std::thread::spawn(move || start_server(false)); } else { #[cfg(windows)] { @@ -62,12 +78,31 @@ fn main() { log::error!("Failed to uninstall: {}", err); } return; + } else if args[0] == "--after-install" { + if let Err(err) = platform::run_after_install() { + log::error!("Failed to after-install: {}", err); + } + return; + } else if args[0] == "--before-uninstall" { + if let Err(err) = platform::run_before_uninstall() { + log::error!("Failed to before-uninstall: {}", err); + } + return; } else if args[0] == "--update" { hbb_common::allow_err!(platform::update_me()); return; } else if args[0] == "--reinstall" { hbb_common::allow_err!(platform::uninstall_me()); - hbb_common::allow_err!(platform::install_me("desktopicon startmenu",)); + hbb_common::allow_err!(platform::install_me( + "desktopicon startmenu", + "".to_owned() + )); + return; + } else if args[0] == "--silent-install" { + hbb_common::allow_err!(platform::install_me( + "desktopicon startmenu", + "".to_owned() + )); return; } } @@ -86,12 +121,12 @@ fn main() { log::info!("start --server"); #[cfg(not(target_os = "macos"))] { - start_server(true, true); + start_server(true); return; } #[cfg(target_os = "macos")] { - std::thread::spawn(move || start_server(true, true)); + std::thread::spawn(move || start_server(true)); } } else if args[0] == "--import-config" { if args.len() == 2 { @@ -138,6 +173,7 @@ fn main() { use clap::App; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -k, --key=[KEY] '' -s, --server... 'Start server'", ); let matches = App::new("rustdesk") @@ -172,6 +208,7 @@ fn main() { if options.len() > 3 { remote_host = options[3].clone(); } - cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port); + let key = matches.value_of("key").unwrap_or("").to_owned(); + cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key); } } diff --git a/src/mobile.rs b/src/mobile.rs new file mode 100644 index 000000000..4f7f96d87 --- /dev/null +++ b/src/mobile.rs @@ -0,0 +1,1306 @@ +use crate::client::*; +use hbb_common::{ + allow_err, + compress::decompress, + config::{Config, LocalConfig}, + fs, log, + message_proto::*, + protobuf::Message as _, + rendezvous_proto::ConnType, + tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, + }, + Stream, +}; +use std::{ + collections::{HashMap, VecDeque}, + sync::{Arc, Mutex, RwLock}, +}; + +lazy_static::lazy_static! { + static ref SESSION: Arc>> = Default::default(); +} + +#[derive(Clone, Default)] +pub struct Session { + id: String, + sender: Arc>>>, + lc: Arc>, + events2ui: Arc>>, + rgba: Arc>>>, +} + +impl Session { + pub fn start(id: &str, is_file_transfer: bool) { + LocalConfig::set_remote_id(id); + Self::close(); + let mut session = Session::default(); + session + .lc + .write() + .unwrap() + .initialize(id.to_owned(), false, false); + session.id = id.to_owned(); + *SESSION.write().unwrap() = Some(session.clone()); + std::thread::spawn(move || { + Connection::start(session, is_file_transfer); + }); + } + + pub fn ensure_init_event_queue() { + let mut s = SESSION.write().unwrap(); + if s.is_none() { + s.replace(Session::default()); + } + } + + pub fn get() -> Arc>> { + SESSION.clone() + } + + pub fn get_option(name: &str) -> String { + if let Some(session) = SESSION.read().unwrap().as_ref() { + if name == "remote_dir" { + return session.lc.read().unwrap().get_remote_dir(); + } + return session.lc.read().unwrap().get_option(name); + } + "".to_owned() + } + + pub fn set_option(name: String, value: String) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + let mut value = value; + if name == "remote_dir" { + value = session.lc.write().unwrap().get_all_remote_dir(value); + } + return session.lc.write().unwrap().set_option(name, value); + } + } + + pub fn input_os_password(pass: String, activate: bool) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + input_os_password(pass, activate, session.clone()); + } + } + + fn send(data: Data) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + session.send(data); + } + } + + pub fn rgba() -> Option> { + if let Some(session) = SESSION.read().unwrap().as_ref() { + session.rgba.write().unwrap().take() + } else { + None + } + } + + pub fn pop_event() -> Option { + if let Some(session) = SESSION.read().unwrap().as_ref() { + session.events2ui.write().unwrap().pop_front() + } else { + None + } + } + + pub fn toggle_option(name: &str) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + let msg = session.lc.write().unwrap().toggle_option(name.to_owned()); + if let Some(msg) = msg { + session.send_msg(msg); + } + } + } + + pub fn refresh() { + Self::send(Data::Message(LoginConfigHandler::refresh())); + } + + pub fn get_image_quality() -> String { + if let Some(session) = SESSION.read().unwrap().as_ref() { + session.lc.read().unwrap().image_quality.clone() + } else { + "".to_owned() + } + } + + pub fn set_image_quality(value: &str) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + let msg = session + .lc + .write() + .unwrap() + .save_image_quality(value.to_owned()); + if let Some(msg) = msg { + session.send_msg(msg); + } + } + } + + pub fn get_toggle_option(name: &str) -> Option { + if let Some(session) = SESSION.read().unwrap().as_ref() { + Some(session.lc.write().unwrap().get_toggle_option(name)) + } else { + None + } + } + + pub fn login(password: &str, remember: bool) { + Session::send(Data::Login((password.to_owned(), remember))); + } + + pub fn close() { + Session::send(Data::Close); + SESSION.write().unwrap().take(); + } + + pub fn reconnect() { + if let Some(session) = SESSION.read().unwrap().as_ref() { + if let Some(sender) = session.sender.read().unwrap().as_ref() { + sender.send(Data::Close).ok(); + } + let session = session.clone(); + std::thread::spawn(move || { + Connection::start(session, false); + }); + } + } + + pub fn get_remember() -> bool { + if let Some(session) = SESSION.read().unwrap().as_ref() { + session.lc.read().unwrap().remember + } else { + false + } + } + + #[inline] + pub fn send_msg(&self, msg: Message) { + if let Some(sender) = self.sender.read().unwrap().as_ref() { + sender.send(Data::Message(msg)).ok(); + } + } + + pub fn send_chat(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_msg_static(msg_out); + } + + pub fn send_files(id: i32, path: String, to: String, include_hidden: bool, is_remote: bool) { + if let Some(session) = SESSION.write().unwrap().as_mut() { + session.send_files(id, path, to, include_hidden, is_remote); + } + } + + #[inline] + pub fn send_msg_static(msg: Message) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + session.send_msg(msg); + } + } + + fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { + let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); + assert!(h.get("name").is_none()); + h.insert("name", name); + self.events2ui + .write() + .unwrap() + .push_back(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + } + + #[inline] + fn peer_platform(&self) -> String { + self.lc.read().unwrap().info.platform.clone() + } + + pub fn ctrl_alt_del() { + if let Some(session) = SESSION.read().unwrap().as_ref() { + if session.peer_platform() == "Windows" { + let k = Key::ControlKey(ControlKey::CtrlAltDel); + session.key_down_or_up(1, k, false, false, false, false); + } else { + let k = Key::ControlKey(ControlKey::Delete); + session.key_down_or_up(3, k, true, true, false, false); + } + } + } + + pub fn switch_display(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_msg_static(msg_out); + } + + pub fn lock_screen() { + if let Some(session) = SESSION.read().unwrap().as_ref() { + let k = Key::ControlKey(ControlKey::LockScreen); + session.key_down_or_up(1, k, false, false, false, false); + } + } + + pub fn input_key(name: &str, down: bool, press: bool, alt: bool, ctrl: bool, shift: bool, command: bool) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + let chars: Vec = name.chars().collect(); + if chars.len() == 1 { + let key = Key::_Raw(chars[0] as _); + session._input_key(key, down, press, alt, ctrl, shift, command); + } else { + if let Some(key) = KEY_MAP.get(name) { + session._input_key(key.clone(), down, press, alt, ctrl, shift, command); + } + } + } + } + + pub fn input_string(value: &str) { + let mut key_event = KeyEvent::new(); + key_event.set_seq(value.to_owned()); + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + Self::send_msg_static(msg_out); + } + + fn _input_key(&self, key: Key, down: bool, press: bool, alt: bool, ctrl: bool, shift: bool, command: bool) { + let v = if press { + 3 + } else if down { + 1 + } else { + 0 + }; + self.key_down_or_up(v, key, alt, ctrl, shift, command); + } + + pub fn send_mouse( + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + if let Some(session) = SESSION.read().unwrap().as_ref() { + send_mouse(mask, x, y, alt, ctrl, shift, command, session); + } + } + + fn key_down_or_up( + &self, + down_or_up: i32, + key: Key, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let mut down_or_up = down_or_up; + let mut key_event = KeyEvent::new(); + match key { + Key::Chr(chr) => { + key_event.set_chr(chr); + } + Key::ControlKey(key) => { + key_event.set_control_key(key.clone()); + } + Key::_Raw(raw) => { + if raw > 'z' as u32 || raw < 'a' as u32 { + key_event.set_unicode(raw); + if down_or_up == 0 { + // ignore up, avoiding trigger twice + return; + } + down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side + } else { + // to make ctrl+c works on windows + key_event.set_chr(raw); + } + } + _ => {} + } + if alt { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command { + key_event.modifiers.push(ControlKey::Meta.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_msg(msg_out); + } +} + +impl FileManager for Session {} + +#[async_trait] +impl Interface for Session { + fn send(&self, data: Data) { + if let Some(sender) = self.sender.read().unwrap().as_ref() { + sender.send(data).ok(); + } + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + let has_retry = if check_if_retry(msgtype, title, text) { + "true" + } else { + "" + }; + self.push_event( + "msgbox", + vec![ + ("type", msgtype), + ("title", title), + ("text", text), + ("hasRetry", has_retry), + ], + ); + } + + 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 lc = self.lc.write().unwrap(); + let username = lc.get_username(&pi); + let mut displays = Vec::new(); + let mut current = pi.current_display as usize; + + if !lc.is_file_transfer { + if pi.displays.is_empty() { + self.msgbox("error", "Remote Error", "No Display"); + } + for ref d in pi.displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + displays.push(h); + } + if current >= pi.displays.len() { + current = 0; + } + } + let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + self.push_event( + "peer_info", + vec![ + ("username", &username), + ("hostname", &pi.hostname), + ("platform", &pi.platform), + ("sas_enabled", &pi.sas_enabled.to_string()), + ("displays", &displays), + ("version", &pi.version), + ("current_display", ¤t.to_string()), + ("is_file_transfer", &lc.is_file_transfer.to_string()), + ], + ); + lc.handle_peer_info(username, pi); + let p = lc.should_auto_login(); + if !p.is_empty() { + input_os_password(p, true, self.clone()); + } + } + + 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; + } +} + +const MILLI1: Duration = Duration::from_millis(1); + +struct Connection { + video_handler: VideoHandler, + audio_handler: AudioHandler, + session: Session, + first_frame: bool, + read_jobs: Vec, + write_jobs: Vec, + timer: Interval, + last_update_jobs_status: (Instant, HashMap), +} + +impl Connection { + #[tokio::main(flavor = "current_thread")] + async fn start(session: Session, is_file_transfer: bool) { + let mut last_recv_time = Instant::now(); + let (sender, mut receiver) = mpsc::unbounded_channel::(); + *session.sender.write().unwrap() = Some(sender); + let conn_type = if is_file_transfer { + session.lc.write().unwrap().is_file_transfer = true; + ConnType::FILE_TRANSFER + } else { + ConnType::DEFAULT_CONN + }; + + let mut conn = Connection { + video_handler: VideoHandler::new(), + audio_handler: Default::default(), + session: session.clone(), + first_frame: false, + read_jobs: Vec::new(), + write_jobs: Vec::new(), + timer: time::interval(SEC30), + last_update_jobs_status: (Instant::now(), Default::default()), + }; + let key = Config::get_option("key"); + let token = Config::get_option("access_token"); + + match Client::start(&session.id, &key, &token, conn_type).await { + Ok((mut peer, direct)) => { + session.push_event( + "connection_ready", + vec![ + ("secure", &peer.is_secured().to_string()), + ("direct", &direct.to_string()), + ], + ); + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + log::error!("Connection closed: {}", err); + session.msgbox("error", "Connection Error", &err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + if !conn.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + log::info!("Reset by the peer"); + session.msgbox("error", "Connection Error", "Reset by the peer"); + break; + } + } + d = receiver.recv() => { + if let Some(d) = d { + if !conn.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _ = conn.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + session.msgbox("error", "Connection Error", "Timeout"); + break; + } + if !conn.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { + log::debug!("Connection Error"); + break; + } + conn.update_jobs_status(); + } else { + conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + } + } + log::debug!("Exit io_loop of id={}", session.id); + } + Err(err) => { + crate::common::test_rendezvous_server(); + session.msgbox("error", "Connection Error", &err.to_string()); + } + } + } + + 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; + } + if let Some(video_frame::Union::vp9s(vp9s)) = &vf.union { + if let Ok(true) = self.video_handler.handle_vp9s(vp9s) { + *self.session.rgba.write().unwrap() = + Some(self.video_handler.rgb.clone()); + } + } + } + Some(message::Union::hash(hash)) => { + self.session.handle_hash(hash, peer).await; + } + Some(message::Union::login_response(lr)) => match lr.union { + Some(login_response::Union::error(err)) => { + if !self.session.handle_login_error(&err) { + return false; + } + } + Some(login_response::Union::peer_info(pi)) => { + self.session.handle_peer_info(pi); + } + _ => {} + }, + Some(message::Union::clipboard(cb)) => { + if !self.session.lc.read().unwrap().disable_clipboard { + let content = if cb.compress { + decompress(&cb.content) + } else { + cb.content + }; + if let Ok(content) = String::from_utf8(content) { + self.session + .push_event("clipboard", vec![("content", &content)]); + } + } + } + Some(message::Union::cursor_data(cd)) => { + let colors = hbb_common::compress::decompress(&cd.colors); + self.session.push_event( + "cursor_data", + vec![ + ("id", &cd.id.to_string()), + ("hotx", &cd.hotx.to_string()), + ("hoty", &cd.hoty.to_string()), + ("width", &cd.width.to_string()), + ("height", &cd.height.to_string()), + ( + "colors", + &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), + ), + ], + ); + } + Some(message::Union::cursor_id(id)) => { + self.session + .push_event("cursor_id", vec![("id", &id.to_string())]); + } + Some(message::Union::cursor_position(cp)) => { + self.session.push_event( + "cursor_position", + vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], + ); + } + Some(message::Union::file_response(fr)) => match fr.union { + Some(file_response::Union::dir(fd)) => { + let entries = fd.entries.to_vec(); + let id = fd.id; + self.session.push_event( + "file_dir", + vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], + ); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.set_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, None).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.session + .push_event("chat_client_mode", vec![("text", &c.text)]); + } + Some(misc::Union::permission_info(p)) => { + log::info!("Change permission {:?} -> {}", p.permission, p.enabled); + use permission_info::Permission; + self.session.push_event( + "permission", + vec![( + match p.permission.enum_value_or_default() { + Permission::Keyboard => "keyboard", + Permission::Clipboard => "clipboard", + Permission::Audio => "audio", + _ => "", + }, + &p.enabled.to_string(), + )], + ); + } + Some(misc::Union::switch_display(s)) => { + self.video_handler.reset(); + self.session.push_event( + "switch_display", + vec![ + ("display", &s.display.to_string()), + ("x", &s.x.to_string()), + ("y", &s.y.to_string()), + ("width", &s.width.to_string()), + ("height", &s.height.to_string()), + ], + ); + } + Some(misc::Union::close_reason(c)) => { + self.session.msgbox("error", "Connection Error", &c); + return false; + } + _ => {} + }, + Some(message::Union::test_delay(t)) => { + self.session.handle_test_delay(t, peer).await; + } + Some(message::Union::audio_frame(frame)) => { + if !self.session.lc.read().unwrap().disable_audio { + self.audio_handler.handle_frame(frame); + } + } + _ => {} + } + } + true + } + + 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.session + .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 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::RemoveDirAll((id, path, 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); + } else { + match fs::get_recursive_files(&path, true) { + Ok(entries) => { + let mut fd = FileDirectory::new(); + fd.id = id; + fd.path = path; + fd.entries = entries; + self.session.push_event( + "file_dir", + vec![("value", &make_fd_to_json(fd)), ("is_local", "true")], + ); + } + 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); + } + 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), + session: &Session, + ) { + if elapsed <= 0 { + return; + } + let transferred = job.transferred(); + let last_transferred = { + 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(), transferred); + let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + session.push_event( + "job_progress", + vec![ + ("id", &job.id().to_string()), + ("file_num", &file_num.to_string()), + ("speed", &speed.to_string()), + ("finished_size", &job.finished_size().to_string()), + ], + ); + } + + 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, + &self.session, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &self.session, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(err) = err { + self.session + .push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); + } else { + self.session.push_event( + "job_done", + vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], + ); + } + } +} + +pub fn make_fd_to_json(fd: FileDirectory) -> String { + use serde_json::json; + let mut fd_json = serde_json::Map::new(); + fd_json.insert("id".into(), json!(fd.id)); + fd_json.insert("path".into(), json!(fd.path)); + + let mut entries = vec![]; + for entry in fd.entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries)); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +// Server Side +// TODO connection_manager need use struct and trait,impl default method +#[cfg(target_os = "android")] +pub mod connection_manager { + use std::{ + iter::FromIterator, + rc::{Rc, Weak}, + }; + + use super::*; + use crate::ipc; + use crate::ipc::Data; + use crate::server::Connection as Conn; + use hbb_common::{ + allow_err, log, + tokio::{ + self, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::spawn_blocking, + }, + }; + use scrap::android::call_main_service_set_by_name; + use serde_derive::Serialize; + + #[derive(Debug, Serialize, Clone)] + struct Client { + id: i32, + pub authorized: bool, + is_file_transfer: bool, + name: String, + peer_id: String, + keyboard: bool, + clipboard: bool, + audio: bool, + #[serde(skip)] + tx: UnboundedSender, + } + + lazy_static::lazy_static! { + static ref CLIENTS: RwLock> = Default::default(); + static ref WRITE_JOBS: Mutex> = Mutex::new(Vec::new()); + } + + pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { + std::thread::spawn(move || start_listen(rx, tx)); + } + + #[tokio::main(flavor = "current_thread")] + async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { + let mut current_id = 0; + loop { + match rx.recv().await { + Some(Data::Login { + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + file_transfer_enabled, + }) => { + current_id = id; + let mut client = Client { + id, + authorized, + is_file_transfer, + name: name.clone(), + peer_id: peer_id.clone(), + keyboard, + clipboard, + audio, + tx: tx.clone(), + }; + if authorized { + client.authorized = true; + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service,active notification no matter UI is shown or not. + if let Err(e) = call_main_service_set_by_name( + "on_client_authorized", + Some(&client_json), + None, + ) { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI,refresh widget + if let Some(session) = Session::get().read().unwrap().as_ref() { + session + .push_event("on_client_authorized", vec![("client", &client_json)]); + }; + } else { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service,active notification no matter UI is shown or not. + if let Err(e) = call_main_service_set_by_name( + "try_start_without_auth", + Some(&client_json), + None, + ) { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI,refresh widget + if let Some(session) = Session::get().read().unwrap().as_ref() { + session.push_event( + "try_start_without_auth", + vec![("client", &client_json)], + ); + }; + } + CLIENTS.write().unwrap().insert(id, client); + } + Some(Data::ChatMessage { text }) => { + handle_chat(current_id, text); + } + Some(Data::FS(fs)) => { + handle_fs(fs, &tx).await; + } + Some(Data::Close) => { + break; + } + None => { + break; + } + _ => {} + } + } + remove_connection(current_id); + } + + pub fn get_clients_state() -> String { + let clients = CLIENTS.read().unwrap(); + let res = Vec::from_iter(clients.values().cloned()); + serde_json::to_string(&res).unwrap_or("".into()) + } + + pub fn get_clients_length() -> usize { + let clients = CLIENTS.read().unwrap(); + clients.len() + } + + pub fn close_conn(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get(&id) { + allow_err!(client.tx.send(Data::Close)); + }; + } + + pub fn on_login_res(id: i32, res: bool) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + if res { + allow_err!(client.tx.send(Data::Authorize)); + client.authorized = true; + } else { + allow_err!(client.tx.send(Data::Close)); + } + }; + } + + fn remove_connection(id: i32) { + let mut clients = CLIENTS.write().unwrap(); + clients.remove(&id); + + if clients + .iter() + .filter(|(k, v)| !v.is_file_transfer) + .next() + .is_none() + { + if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { + log::debug!("stop_capture err:{}", e); + } + } + + if let Some(session) = Session::get().read().unwrap().as_ref() { + session.push_event("on_client_remove", vec![("id", &id.to_string())]); + }; + } + + // server mode handle chat from other peers + fn handle_chat(id: i32, text: String) { + if let Some(session) = Session::get().read().unwrap().as_ref() { + session.push_event( + "chat_server_mode", + vec![("id", &id.to_string()), ("text", &text)], + ); + }; + } + + // server mode send chat to peer + pub fn send_chat(id: i32, text: String) { + let mut clients = CLIENTS.read().unwrap(); + if let Some(client) = clients.get(&id) { + allow_err!(client.tx.send(Data::ChatMessage { text })); + } + } + + // handle FS server + async fn handle_fs(fs: ipc::FS, tx: &UnboundedSender) { + match fs { + ipc::FS::ReadDir { + dir, + include_hidden, + } => { + read_dir(&dir, include_hidden, tx).await; + } + ipc::FS::RemoveDir { + path, + id, + recursive, + } => { + remove_dir(path, id, recursive, tx).await; + } + ipc::FS::RemoveFile { path, id, file_num } => { + remove_file(path, id, file_num, tx).await; + } + ipc::FS::CreateDir { path, id } => { + create_dir(path, id, tx).await; + } + ipc::FS::NewWrite { + path, + id, + mut files, + } => { + WRITE_JOBS.lock().unwrap().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 } => { + let write_jobs = &mut *WRITE_JOBS.lock().unwrap(); + 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 } => { + let write_jobs = &mut *WRITE_JOBS.lock().unwrap(); + if let Some(job) = fs::get_job(id, write_jobs) { + job.modify_time(); + send_raw(fs::new_done(id, file_num), tx); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteBlock { + id, + file_num, + data, + compressed, + } => { + if let Some(job) = fs::get_job(id, &mut *WRITE_JOBS.lock().unwrap()) { + if let Err(err) = job + .write( + FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }, + None, + ) + .await + { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + _ => {} + } + } + + async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + 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); + send_raw(msg_out, tx); + } + } + + async fn handle_result( + res: std::result::Result, S>, + id: i32, + file_num: i32, + tx: &UnboundedSender, + ) { + match res { + Err(err) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Err(err)) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Ok(())) => { + send_raw(fs::new_done(id, file_num), tx); + } + } + } + + async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::remove_file(&path)).await, + id, + file_num, + tx, + ) + .await; + } + + async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::create_dir(&path)).await, + id, + 0, + tx, + ) + .await; + } + + async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { + let path = fs::get_path(&path); + 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, + tx, + ) + .await; + } + + fn send_raw(msg: Message, tx: &UnboundedSender) { + match msg.write_to_bytes() { + Ok(bytes) => { + allow_err!(tx.send(Data::RawMessage(bytes))); + } + err => allow_err!(err), + } + } +} diff --git a/src/mobile_ffi.rs b/src/mobile_ffi.rs new file mode 100644 index 000000000..f32053b12 --- /dev/null +++ b/src/mobile_ffi.rs @@ -0,0 +1,561 @@ +use crate::client::file_trait::FileManager; +#[cfg(target_os = "android")] +use crate::mobile::connection_manager::{self, get_clients_length, get_clients_state}; +use crate::mobile::{make_fd_to_json, Session}; +use hbb_common::{ + config::{self, Config, PeerConfig, ONLINE, LocalConfig}, + fs, log, +}; +use serde_json::{Number, Value}; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + os::raw::c_char, +}; + +fn initialize(app_dir: &str) { + *config::APP_DIR.write().unwrap() = app_dir.to_owned(); + #[cfg(target_os = "android")] + { + android_logger::init_once( + android_logger::Config::default() + .with_min_level(log::Level::Debug) // limit log level + .with_tag("ffi"), // logs will show under mytag tag + ); + } + #[cfg(target_os = "ios")] + { + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); + } + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + #[cfg(target_os = "android")] + crate::common::check_software_update(); +} + +#[no_mangle] +unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *const c_char { + let mut res = "".to_owned(); + let arg: &CStr = CStr::from_ptr(arg); + let name: &CStr = CStr::from_ptr(name); + if let Ok(name) = name.to_str() { + match name { + "peers" => { + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .drain(..) + .map(|(id, _, p)| (id, p.info)) + .collect(); + res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); + } + } + "remote_id" => { + if !config::APP_DIR.read().unwrap().is_empty() { + res = LocalConfig::get_remote_id(); + } + } + "remember" => { + res = Session::get_remember().to_string(); + } + "event" => { + if let Some(e) = Session::pop_event() { + res = e; + } + } + "toggle_option" => { + if let Ok(arg) = arg.to_str() { + if let Some(v) = Session::get_toggle_option(arg) { + res = v.to_string(); + } + } + } + "test_if_valid_server" => { + if let Ok(arg) = arg.to_str() { + res = hbb_common::socket_client::test_if_valid_server(arg); + } + } + "option" => { + if let Ok(arg) = arg.to_str() { + res = Config::get_option(arg); + } + } + "image_quality" => { + res = Session::get_image_quality(); + } + "software_update_url" => { + res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() + } + "translate" => { + if let Ok(arg) = arg.to_str() { + if let Ok(m) = serde_json::from_str::>(arg) { + if let Some(locale) = m.get("locale") { + if let Some(text) = m.get("text") { + res = crate::client::translate_locale(text.to_owned(), locale); + } + } + } + } + } + "peer_option" => { + if let Ok(arg) = arg.to_str() { + res = Session::get_option(arg); + } + } + "server_id" => { + res = Config::get_id(); + } + "server_password" => { + res = Config::get_password(); + } + "connect_statue" => { + res = ONLINE + .lock() + .unwrap() + .values() + .max() + .unwrap_or(&0) + .clone() + .to_string(); + } + // File Action + "get_home_dir" => { + res = fs::get_home_as_string(); + } + "read_local_dir_sync" => { + if let Ok(value) = arg.to_str() { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(path), Some(show_hidden)) = + (m.get("path"), m.get("show_hidden")) + { + if let Ok(fd) = + fs::read_dir(&fs::get_path(path), show_hidden.eq("true")) + { + res = make_fd_to_json(fd); + } + } + } + } + } + // Server Side + #[cfg(target_os = "android")] + "clients_state" => { + res = get_clients_state(); + } + #[cfg(target_os = "android")] + "check_clients_length" => { + if let Ok(value) = arg.to_str() { + if value.parse::().unwrap_or(usize::MAX) != get_clients_length() { + res = get_clients_state() + } + } + } + "uuid" => { + res = base64::encode(crate::get_uuid()); + } + _ => { + log::error!("Unknown name of get_by_name: {}", name); + } + } + } + CString::from_vec_unchecked(res.into_bytes()).into_raw() +} + +#[no_mangle] +unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { + let value: &CStr = CStr::from_ptr(value); + if let Ok(value) = value.to_str() { + let name: &CStr = CStr::from_ptr(name); + if let Ok(name) = name.to_str() { + match name { + "init" => { + initialize(value); + } + "info1" => { + *crate::common::MOBILE_INFO1.lock().unwrap() = value.to_owned(); + } + "info2" => { + *crate::common::MOBILE_INFO2.lock().unwrap() = value.to_owned(); + } + "connect" => { + Session::start(value, false); + } + "connect_file_transfer" => { + Session::start(value, true); + } + "login" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let Some(password) = m.get("password") { + if let Some(remember) = m.get("remember") { + Session::login(password, remember == "true"); + } + } + } + } + "close" => { + Session::close(); + } + "refresh" => { + Session::refresh(); + } + "reconnect" => { + Session::reconnect(); + } + "toggle_option" => { + Session::toggle_option(value); + } + "image_quality" => { + Session::set_image_quality(value); + } + "lock_screen" => { + Session::lock_screen(); + } + "ctrl_alt_del" => { + Session::ctrl_alt_del(); + } + "switch_display" => { + if let Ok(v) = value.parse::() { + Session::switch_display(v); + } + } + "remove" => { + PeerConfig::remove(value); + } + "input_key" => { + if let Ok(m) = serde_json::from_str::>(value) { + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + let down = m.get("down").is_some(); + let press = m.get("press").is_some(); + if let Some(name) = m.get("name") { + Session::input_key(name, down, press, alt, ctrl, shift, command); + } + } + } + "input_string" => { + Session::input_string(value); + } + "chat_client_mode" => { + Session::send_chat(value.to_owned()); + } + "send_mouse" => { + if let Ok(m) = serde_json::from_str::>(value) { + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + let x = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y = m + .get("y") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let mut mask = 0; + if let Some(_type) = m.get("type") { + mask = match _type.as_str() { + "down" => 1, + "up" => 2, + "wheel" => 3, + _ => 0, + }; + } + if let Some(buttons) = m.get("buttons") { + mask |= match buttons.as_str() { + "left" => 1, + "right" => 2, + "wheel" => 4, + _ => 0, + } << 3; + } + Session::send_mouse(mask, x, y, alt, ctrl, shift, command); + } + } + "option" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let Some(name) = m.get("name") { + if let Some(value) = m.get("value") { + Config::set_option(name.to_owned(), value.to_owned()); + if name == "custom-rendezvous-server" { + #[cfg(target_os = "android")] + crate::rendezvous_mediator::RendezvousMediator::restart(); + crate::common::test_rendezvous_server(); + } + } + } + } + } + "peer_option" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let Some(name) = m.get("name") { + if let Some(value) = m.get("value") { + Session::set_option(name.to_owned(), value.to_owned()); + } + } + } + } + "input_os_password" => { + Session::input_os_password(value.to_owned(), true); + } + // File Action + "read_remote_dir" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(path), Some(show_hidden), Some(session)) = ( + m.get("path"), + m.get("show_hidden"), + Session::get().read().unwrap().as_ref(), + ) { + session.read_remote_dir(path.to_owned(), show_hidden.eq("true")); + } + } + } + "send_files" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let ( + Some(id), + Some(path), + Some(to), + Some(show_hidden), + Some(is_remote), + ) = ( + m.get("id"), + m.get("path"), + m.get("to"), + m.get("show_hidden"), + m.get("is_remote"), + ) { + Session::send_files( + id.parse().unwrap_or(0), + path.to_owned(), + to.to_owned(), + show_hidden.eq("true"), + is_remote.eq("true"), + ); + } + } + } + "remove_file" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let ( + Some(id), + Some(path), + Some(file_num), + Some(is_remote), + Some(session), + ) = ( + m.get("id"), + m.get("path"), + m.get("file_num"), + m.get("is_remote"), + Session::get().write().unwrap().as_mut(), + ) { + session.remove_file( + id.parse().unwrap_or(0), + path.to_owned(), + file_num.parse().unwrap_or(0), + is_remote.eq("true"), + ); + } + } + } + "read_dir_recursive" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( + m.get("id"), + m.get("path"), + m.get("is_remote"), + Session::get().write().unwrap().as_mut(), + ) { + session.remove_dir_all( + id.parse().unwrap_or(0), + path.to_owned(), + is_remote.eq("true"), + ); + } + } + } + "remove_all_empty_dirs" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( + m.get("id"), + m.get("path"), + m.get("is_remote"), + Session::get().write().unwrap().as_mut(), + ) { + session.remove_dir( + id.parse().unwrap_or(0), + path.to_owned(), + is_remote.eq("true"), + ); + } + } + } + "cancel_job" => { + if let (Ok(id), Some(session)) = + (value.parse(), Session::get().write().unwrap().as_mut()) + { + session.cancel_job(id); + } + } + "create_dir" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( + m.get("id"), + m.get("path"), + m.get("is_remote"), + Session::get().write().unwrap().as_mut(), + ) { + session.create_dir( + id.parse().unwrap_or(0), + path.to_owned(), + is_remote.eq("true"), + ); + } + } + } + // Server Side + "ensure_init_event_queue" => { + Session::ensure_init_event_queue(); + } + "update_password" => { + if value.is_empty() { + Config::set_password(&Config::get_auto_password()); + } else { + Config::set_password(value); + } + } + #[cfg(target_os = "android")] + "chat_server_mode" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(Value::Number(id)), Some(Value::String(text))) = + (m.get("id"), m.get("text")) + { + let id = id.as_i64().unwrap_or(0); + connection_manager::send_chat(id as i32, text.to_owned()); + } + } + } + "home_dir" => { + *config::APP_HOME_DIR.write().unwrap() = value.to_owned(); + } + #[cfg(target_os = "android")] + "login_res" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let (Some(Value::Number(id)), Some(Value::Bool(res))) = + (m.get("id"), m.get("res")) + { + let id = id.as_i64().unwrap_or(0); + connection_manager::on_login_res(id as i32, *res); + } + } + } + #[cfg(target_os = "android")] + "stop_service" => { + Config::set_option("stop-service".into(), "Y".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[cfg(target_os = "android")] + "start_service" => { + Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[cfg(target_os = "android")] + "close_conn" => { + if let Ok(id) = value.parse::() { + connection_manager::close_conn(id); + }; + } + _ => { + log::error!("Unknown name of set_by_name: {}", name); + } + } + } + } +} + +#[repr(C)] +struct RgbaFrame { + len: u32, + data: *mut u8, +} + +#[no_mangle] +unsafe extern "C" fn get_rgba() -> *mut RgbaFrame { + if let Some(mut vec) = Session::rgba() { + if vec.is_empty() { + return std::ptr::null_mut(); + } + assert!(vec.len() == vec.capacity()); + vec.shrink_to_fit(); + let data = vec.as_mut_ptr(); + let len = vec.len(); + std::mem::forget(vec); + Box::into_raw(Box::new(RgbaFrame { + len: len as _, + data, + })) + } else { + std::ptr::null_mut() + } +} + +#[no_mangle] +extern "C" fn free_rgba(f: *mut RgbaFrame) { + if f.is_null() { + return; + } + unsafe { + let len = (*f).len as usize; + drop(Vec::from_raw_parts((*f).data, len, len)); + Box::from_raw(f); + } +} + +#[cfg(target_os = "android")] +pub mod server_side { + use hbb_common::{config::Config, log}; + use jni::{ + objects::{JClass, JString}, + sys::jstring, + JNIEnv, + }; + + use crate::start_server; + + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( + env: JNIEnv, + _class: JClass, + ) { + log::debug!("startServer from java"); + std::thread::spawn(move || start_server(true)); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale( + env: JNIEnv, + _class: JClass, + locale: JString, + input: JString, + ) -> jstring { + let res = if let (Ok(input), Ok(locale)) = (env.get_string(input), env.get_string(locale)) { + let input: String = input.into(); + let locale: String = locale.into(); + crate::client::translate_locale(input, &locale) + } else { + "".into() + }; + return env.new_string(res).unwrap_or(input).into_inner(); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_refreshScreen( + _env: JNIEnv, + _class: JClass, + ) { + crate::server::video_service::refresh() + } +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index a49b71a5a..79d87492a 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -536,7 +536,7 @@ pub fn run_as_user(arg: &str) -> ResultType> { // -E required for opensuse let task = std::process::Command::new("sudo") .args(vec![ - "-E", + "-E", &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str, "-u", &get_active_username(), @@ -587,7 +587,10 @@ pub fn get_pa_sources() -> Vec<(String, String)> { } pub fn lock_screen() { - std::process::Command::new("xdg-screensaver").arg("lock").spawn().ok(); + std::process::Command::new("xdg-screensaver") + .arg("lock") + .spawn() + .ok(); } pub fn toggle_blank_screen(_v: bool) { @@ -604,11 +607,7 @@ pub fn is_installed() -> bool { fn run_cmds(cmds: String) -> ResultType> { let mut tmp = std::env::temp_dir(); - tmp.push(format!( - "{}_{}", - hbb_common::config::APP_NAME, - crate::get_time() - )); + tmp.push(format!("{}_{}", crate::get_app_name(), crate::get_time())); let mut file = std::fs::File::create(&tmp)?; file.write_all(cmds.as_bytes())?; file.sync_all()?; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index d446f1206..37164de31 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,58 +1,76 @@ -#[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}; -#[cfg(not(target_os = "macos"))] -const SERVICE_INTERVAL: u64 = 300; - -pub fn is_xfce() -> bool { - #[cfg(target_os = "linux")] - { - return std::env::var_os("XDG_CURRENT_DESKTOP") == Some(std::ffi::OsString::from("XFCE")); - } - #[cfg(not(target_os = "linux"))] - { - return false; - } -} - -#[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()); - } - } -} +#[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}; +#[cfg(not(target_os = "macos"))] +const SERVICE_INTERVAL: u64 = 300; + +pub fn get_license_key() -> String { + #[cfg(windows)] + if let Some(lic) = windows::get_license() { + return lic.key; + } + Default::default() +} + +pub fn is_xfce() -> bool { + #[cfg(target_os = "linux")] + { + return std::env::var_os("XDG_CURRENT_DESKTOP") == Some(std::ffi::OsString::from("XFCE")); + } + #[cfg(not(target_os = "linux"))] + { + return false; + } +} + +// Android +#[cfg(target_os = "android")] +pub fn get_active_username() -> String { + // TODO + "android".into() +} + +#[cfg(target_os = "android")] +pub const PA_SAMPLE_RATE: u32 = 48000; + +#[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 index 341ba2cf4..3b959015c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,1097 +1,1337 @@ -use super::{CursorData, ResultType}; -use crate::ipc; -use hbb_common::{ - allow_err, bail, - config::{Config, APP_NAME}, - 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; - if cbits_size < 16 { - bail!("Invalid icon: too small"); // solve some crash - } - 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, - bm_mask.bmHeight, - ) > 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, - outline.len() as _, - ); - 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, - bm_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; - let bm_size = mbits.len(); - let c_size = cbits.len(); - - // Pack and invert bitmap data (mbits) - // borrow from tigervnc - for y in 0..height { - for x in 0..packed_width_bytes { - let a = y * packed_width_bytes + x; - let b = y * bm_width_bytes + x; - if a < bm_size && b < bm_size { - mbits[a] = !mbits[b]; - } - } - } - - // 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); - if mask_idx < bm_size { - let pix_idx = y * bytes_row + (x << 2); - if (mbits[mask_idx] & bitmask) == 0 { - for b1 in 0..4 { - let a = pix_idx + b1; - if a < c_size { - if cbits[a] != 0 { - mbits[mask_idx] ^= bitmask; - for b2 in b1..4 { - let b = pix_idx + b2; - if b < c_size { - cbits[b] = 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 mut alpha = 255; - if mask_idx < bm_size { - if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 { - alpha = 0; - } - } - 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, - bmHeight: i32, - ) -> i32; - fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32, out_size: i32); - fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32; - fn blank_screen(v: BOOL); -} - -extern "system" { - fn BlockInput(v: BOOL) -> BOOL; -} - -#[tokio::main(flavor = "current_thread")] -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 launch 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(flavor = "current_thread")] -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 "system" { - pub fn SendSAS(AsUser: BOOL); - } - unsafe { - log::info!("SAS received"); - SendSAS(FALSE); - } -} - -lazy_static::lazy_static! { - static ref SUPPRESS: 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 = SUPPRESS.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; - } - extern "C" { - fn get_active_user(path: *mut u16, n: u32) -> u32; - } - let buff_size = 256; - let mut buff: Vec = Vec::with_capacity(buff_size); - buff.resize(buff_size, 0); - let n = unsafe { get_active_user(buff.as_mut_ptr(), buff_size as _) }; - if n == 0 { - return "".to_owned(); - } - let sl = unsafe { std::slice::from_raw_parts(buff.as_ptr(), n as _) }; - String::from_utf16(sl) - .unwrap_or("??".to_owned()) - .trim_end_matches('\0') - .to_owned() -} - -/* -pub fn get_active_username() -> String { - use std::os::windows::process::CommandExt; - let name = crate::username(); - if name != "SYSTEM" { - return name; - } - const CREATE_NO_WINDOW: u32 = 0x08000000; - let mut cmd = std::process::Command::new("query"); - cmd.arg("user"); - cmd.creation_flags(CREATE_NO_WINDOW); - if let Ok(output) = cmd.output() { - for line in String::from_utf8_lossy(&output.stdout).lines() { - if let Some(name) = line.split_whitespace().next() { - if name.starts_with(">") { - return name.replace(">", ""); - } - } - } - } - 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 "system" { - 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!( - " - chcp 65001 - 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 tray_shortcut = write_cmds( - format!( - " -Set oWS = WScript.CreateObject(\"WScript.Shell\") -sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\" - -Set oLink = oWS.CreateShortcut(sLinkFile) - oLink.TargetPath = \"{exe}\" - oLink.Arguments = \"--tray\" -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; - 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!( - " -chcp 65001 -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}\" -\"{tray_shortcut}\" -copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\" -{shortcuts} -del /f \"{mk_shortcut}\" -del /f \"{uninstall_shortcut}\" -del /f \"{tray_shortcut}\" -del /f \"{tmp_path}\\{app_name}.lnk\" -del /f \"{tmp_path}\\Uninstall {app_name}.lnk\" -del /f \"{tmp_path}\\{app_name} Tray.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 -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, - tray_shortcut=tray_shortcut, - tmp_path=tmp_path, - shortcuts=shortcuts, - config_path=Config::file().to_str().unwrap_or(""), - ext=ext, - ); - run_cmds(cmds, false)?; - std::thread::sleep(std::time::Duration::from_millis(2000)); - std::process::Command::new(&exe).spawn()?; - std::process::Command::new(&exe).arg("--tray").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!( - " -chcp 65001 -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}*\" -del /f /q \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" -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, cmds.as_ptr(), 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)?; - // in case cmds mixed with \r\n and \n, make sure all ending with \r\n - // in some windows, \r\n required for cmd file to run - let cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n"); - 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_blank_screen(v: bool) { - let v = if v { TRUE } else { FALSE }; - unsafe { - blank_screen(v); - } -} - -pub fn block_input(v: bool) -> bool { - let v = if v { TRUE } else { FALSE }; - unsafe { BlockInput(v) == TRUE } -} - -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() -} - -pub fn create_shortcut(id: &str) -> ResultType<()> { - let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); - let shortcut = write_cmds( - format!( - " -Set oWS = WScript.CreateObject(\"WScript.Shell\") -strDesktop = oWS.SpecialFolders(\"Desktop\") -Set objFSO = CreateObject(\"Scripting.FileSystemObject\") -sLinkFile = objFSO.BuildPath(strDesktop, \"{id}.lnk\") -Set oLink = oWS.CreateShortcut(sLinkFile) - oLink.TargetPath = \"{exe}\" - oLink.Arguments = \"--connect {id}\" -oLink.Save - ", - exe = exe, - id = id, - ), - "vbs", - )? - .to_str() - .unwrap_or("") - .to_owned(); - std::process::Command::new("cscript") - .arg(&shortcut) - .output()?; - allow_err!(std::fs::remove_file(shortcut)); - Ok(()) -} - -pub fn quit_gui() { - unsafe { PostQuitMessage(0) }; -} +use super::{CursorData, ResultType}; +use crate::ipc; +use crate::license::*; +use hbb_common::{ + allow_err, bail, + config::{self, Config}, + 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}, +}; +use winreg::enums::*; +use winreg::RegKey; + +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; + if cbits_size < 16 { + bail!("Invalid icon: too small"); // solve some crash + } + 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, + bm_mask.bmHeight, + ) > 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, + outline.len() as _, + ); + 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, + bm_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; + let bm_size = mbits.len(); + let c_size = cbits.len(); + + // Pack and invert bitmap data (mbits) + // borrow from tigervnc + for y in 0..height { + for x in 0..packed_width_bytes { + let a = y * packed_width_bytes + x; + let b = y * bm_width_bytes + x; + if a < bm_size && b < bm_size { + mbits[a] = !mbits[b]; + } + } + } + + // 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); + if mask_idx < bm_size { + let pix_idx = y * bytes_row + (x << 2); + if (mbits[mask_idx] & bitmask) == 0 { + for b1 in 0..4 { + let a = pix_idx + b1; + if a < c_size { + if cbits[a] != 0 { + mbits[mask_idx] ^= bitmask; + for b2 in b1..4 { + let b = pix_idx + b2; + if b < c_size { + cbits[b] = 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 mut alpha = 255; + if mask_idx < bm_size { + if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 { + alpha = 0; + } + } + 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(crate::get_app_name(), ffi_service_main) + { + log::error!("start_service failed: {}", e); + } +} + +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +extern "C" { + fn has_rdp_service() -> BOOL; + fn get_current_session(rdp: BOOL) -> DWORD; + fn LaunchProcessWin(cmd: *const u16, session_id: DWORD, as_user: BOOL) -> HANDLE; + fn selectInputDesktop() -> BOOL; + fn inputDesktopSelected() -> BOOL; + fn is_windows_server() -> BOOL; + fn handleMask( + out: *mut u8, + mask: *const u8, + width: i32, + height: i32, + bmWidthBytes: i32, + bmHeight: i32, + ) -> i32; + fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32, out_size: i32); + fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32; + fn blank_screen(v: BOOL); + fn win32_enable_lowlevel_keyboard(hwnd: HWND) -> i32; + fn win32_disable_lowlevel_keyboard(hwnd: HWND); + fn win_stop_system_key_propagate(v: BOOL); + fn is_win_down() -> BOOL; +} + +extern "system" { + fn BlockInput(v: BOOL) -> BOOL; +} + +#[tokio::main(flavor = "current_thread")] +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(crate::get_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 { get_current_session(share_rdp()) }; + 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 = get_current_session(share_rdp()); + 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 { get_current_session(share_rdp()) }; + 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(flavor = "current_thread")] +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 "system" { + pub fn SendSAS(AsUser: BOOL); + } + unsafe { + log::info!("SAS received"); + SendSAS(FALSE); + } +} + +lazy_static::lazy_static! { + static ref SUPPRESS: 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 = SUPPRESS.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()); + } +} + +fn share_rdp() -> BOOL { + if get_reg("share_rdp") != "true" { + FALSE + } else { + TRUE + } +} + +pub fn is_share_rdp() -> bool { + share_rdp() == TRUE +} + +pub fn set_share_rdp(enable: bool) { + let (subkey, _, _, _) = get_install_info(); + let cmd = format!( + "reg add {} /f /v share_rdp /t REG_SZ /d \"{}\"", + subkey, + if enable { "true" } else { "false" } + ); + run_cmds(cmd, false).ok(); +} + +pub fn get_active_username() -> String { + let name = crate::username(); + if name != "SYSTEM" { + return name; + } + extern "C" { + fn get_active_user(path: *mut u16, n: u32, rdp: BOOL) -> u32; + } + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let n = unsafe { get_active_user(buff.as_mut_ptr(), buff_size as _, share_rdp()) }; + if n == 0 { + return "".to_owned(); + } + let sl = unsafe { std::slice::from_raw_parts(buff.as_ptr(), n as _) }; + String::from_utf16(sl) + .unwrap_or("??".to_owned()) + .trim_end_matches('\0') + .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 "system" { + pub fn LockWorkStation() -> BOOL; + } + unsafe { + LockWorkStation(); + } +} + +const IS1: &str = "{54E86BC2-6C85-41F3-A9EB-1A94AC9B1F93}_is1"; + +fn get_subkey(name: &str, wow: bool) -> String { + let tmp = format!( + "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + name + ); + if wow { + tmp.replace("Microsoft", "Wow6432Node\\Microsoft") + } else { + tmp + } +} + +fn get_valid_subkey() -> String { + let subkey = get_subkey(IS1, false); + if !get_reg_of(&subkey, "InstallLocation").is_empty() { + return subkey; + } + let subkey = get_subkey(IS1, true); + if !get_reg_of(&subkey, "InstallLocation").is_empty() { + return subkey; + } + let app_name = crate::get_app_name(); + let subkey = get_subkey(&app_name, true); + if !get_reg_of(&subkey, "InstallLocation").is_empty() { + return subkey; + } + return get_subkey(&app_name, false); +} + +pub fn get_install_info() -> (String, String, String, String) { + get_install_info_with_subkey(get_valid_subkey()) +} + +fn get_default_install_info() -> (String, String, String, String) { + get_install_info_with_subkey(get_subkey(&crate::get_app_name(), false)) +} + +fn get_default_install_path() -> String { + 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(); + } + } + #[cfg(target_pointer_width = "32")] + { + let tmp = pf.replace("Program Files", "Program Files (x86)"); + if std::path::Path::new(&tmp).exists() { + pf = tmp; + } + } + format!("{}\\{}", pf, crate::get_app_name()) +} + +fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String) { + let mut path = get_reg_of(&subkey, "InstallLocation"); + if path.is_empty() { + path = get_default_install_path(); + } + path = path.trim_end_matches('\\').to_owned(); + let start_menu = format!( + "%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\{}", + crate::get_app_name() + ); + let exe = format!("{}\\{}.exe", path, crate::get_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!( + " + chcp 65001 + sc stop {app_name} + taskkill /F /IM {app_name}.exe + copy /Y \"{src_exe}\" \"{exe}\" + sc start {app_name} + {lic} + ", + src_exe = src_exe, + exe = exe, + app_name = crate::get_app_name(), + lic = register_licence(), + ); + 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).arg("--tray").spawn().ok(); + std::process::Command::new(&exe).spawn().ok(); + std::process::Command::new(&exe) + .args(&["--remove", &src_exe]) + .spawn()?; + Ok(()) +} + +fn get_after_install(exe: &str) -> String { + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + format!(" + chcp 65001 + 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}\\\" --service\" start= auto DisplayName= \"{app_name} Service\" + netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes + sc start {app_name} + reg add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1 + ", ext=ext, exe=exe, app_name=app_name) +} + +pub fn install_me(options: &str, path: String) -> ResultType<()> { + let uninstall_str = get_uninstall(); + let mut path = path.trim_end_matches('\\').to_owned(); + let (subkey, _path, start_menu, exe) = get_default_install_info(); + let mut exe = exe; + if path.is_empty() { + path = _path; + } else { + exe = exe.replace(&_path, &path); + } + 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 = crate::get_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 = crate::get_app_name(), + exe = exe, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + let tray_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\" + +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--tray\" +oLink.Save + ", + tmp_path = tmp_path, + app_name = crate::get_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, + crate::get_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 = crate::get_app_name(), + ); + } + + let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; + let size = meta.len() / 1024; + // 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!( + " +{uninstall_str} +chcp 65001 +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 +{lic} +\"{mk_shortcut}\" +\"{uninstall_shortcut}\" +\"{tray_shortcut}\" +copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\" +{shortcuts} +copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" +del /f \"{mk_shortcut}\" +del /f \"{uninstall_shortcut}\" +del /f \"{tray_shortcut}\" +del /f \"{tmp_path}\\{app_name}.lnk\" +del /f \"{tmp_path}\\Uninstall {app_name}.lnk\" +del /f \"{tmp_path}\\{app_name} Tray.lnk\" +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} +{after_install} + ", + uninstall_str=uninstall_str, + path=path, + src_exe=std::env::current_exe()?.to_str().unwrap_or(""), + exe=exe, + subkey=subkey, + app_name=crate::get_app_name(), + version=crate::VERSION, + major=version_major, + minor=version_minor, + build=version_build, + size=size, + mk_shortcut=mk_shortcut, + uninstall_shortcut=uninstall_shortcut, + tray_shortcut=tray_shortcut, + tmp_path=tmp_path, + shortcuts=shortcuts, + config_path=Config::file().to_str().unwrap_or(""), + lic=register_licence(), + after_install=get_after_install(&exe), + ); + run_cmds(cmds, false)?; + std::thread::sleep(std::time::Duration::from_millis(2000)); + std::process::Command::new(&exe).spawn()?; + std::process::Command::new(&exe).arg("--tray").spawn()?; + std::thread::sleep(std::time::Duration::from_millis(1000)); + Ok(()) +} + +pub fn run_after_install() -> ResultType<()> { + let (_, _, _, exe) = get_install_info(); + run_cmds(get_after_install(&exe), true) +} + +pub fn run_before_uninstall() -> ResultType<()> { + run_cmds(get_before_uninstall(), true) +} + +fn get_before_uninstall() -> String { + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + format!( + " + chcp 65001 + sc stop {app_name} + sc delete {app_name} + taskkill /F /IM {app_name}.exe + reg delete HKEY_CLASSES_ROOT\\.{ext} /f + netsh advfirewall firewall delete rule name=\"{app_name} Service\" + ", + app_name = app_name, + ext = ext + ) +} + +fn get_uninstall() -> String { + let (subkey, path, start_menu, _) = get_install_info(); + format!( + " + {before_uninstall} + reg delete {subkey} /f + rd /s /q \"{path}\" + rd /s /q \"{start_menu}\" + del /f /q \"%PUBLIC%\\Desktop\\{app_name}*\" + del /f /q \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" + ", + before_uninstall=get_before_uninstall(), + subkey=subkey, + app_name = crate::get_app_name(), + path = path, + start_menu = start_menu, + ) +} + +pub fn uninstall_me() -> ResultType<()> { + run_cmds(get_uninstall(), true) +} + +fn write_cmds(cmds: String, ext: &str) -> ResultType { + let mut tmp = std::env::temp_dir(); + tmp.push(format!( + "{}_{:?}.{}", + crate::get_app_name(), + cmds.as_ptr(), + 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)?; + // in case cmds mixed with \r\n and \n, make sure all ending with \r\n + // in some windows, \r\n required for cmd file to run + let cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n"); + if ext == "vbs" { + let mut v: Vec = cmds.encode_utf16().collect(); + // utf8 -> utf16le which vbs support it only + file.write_all(to_le(&mut v))?; + } else { + file.write_all(cmds.as_bytes())?; + } + file.sync_all()?; + return Ok(tmp); +} + +fn to_le(v: &mut [u16]) -> &[u8] { + for b in v.iter_mut() { + *b = b.to_le() + } + unsafe { v.align_to().1 } +} + +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_blank_screen(v: bool) { + let v = if v { TRUE } else { FALSE }; + unsafe { + blank_screen(v); + } +} + +pub fn block_input(v: bool) -> bool { + let v = if v { TRUE } else { FALSE }; + unsafe { BlockInput(v) == TRUE } +} + +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(crate::get_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() +} + +fn get_reg(name: &str) -> String { + let (subkey, _, _, _) = get_install_info(); + get_reg_of(&subkey, name) +} + +fn get_reg_of(subkey: &str, name: &str) -> String { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + if let Ok(tmp) = hklm.open_subkey(subkey.replace("HKEY_LOCAL_MACHINE\\", "")) { + if let Ok(v) = tmp.get_value(name) { + return v; + } + } + "".to_owned() +} + +fn get_license_from_exe_name() -> ResultType { + let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + let tmp: Vec<&str> = exe.split("-licensed-").collect(); + if let Some(tmp) = tmp.last() { + let tmp: Vec<&str> = tmp.split(".").collect(); + if let Some(tmp) = tmp.first() { + return get_license_from_string(tmp); + } + } + Ok(Default::default()) +} + +#[inline] +pub fn is_win_server() -> bool { + unsafe { is_windows_server() > 0 } +} + +pub fn get_license() -> Option { + let mut lic: License = Default::default(); + if let Ok(tmp) = get_license_from_exe_name() { + lic = tmp; + } else { + lic.key = get_reg("Key"); + lic.host = get_reg("Host"); + lic.api = get_reg("Api"); + } + if lic.key.is_empty() || lic.host.is_empty() { + return None; + } + Some(lic) +} + +pub fn bootstrap() { + if let Some(lic) = get_license() { + *config::PROD_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); + #[cfg(feature = "hbbs")] + { + if !is_win_server() { + return; + } + crate::hbbs::bootstrap(&lic.key, &lic.host); + std::thread::spawn(move || loop { + let tmp = Config::get_option("stop-rendezvous-service"); + if tmp.is_empty() { + crate::hbbs::start(); + } else { + crate::hbbs::stop(); + } + std::thread::sleep(std::time::Duration::from_millis(100)); + }); + } + } +} + +fn register_licence() -> String { + let (subkey, _, _, _) = get_install_info(); + if let Ok(lic) = get_license_from_exe_name() { + format!( + " + reg add {subkey} /f /v Key /t REG_SZ /d \"{key}\" + reg add {subkey} /f /v Host /t REG_SZ /d \"{host}\" + reg add {subkey} /f /v Api /t REG_SZ /d \"{api}\" + ", + subkey = subkey, + key = &lic.key, + host = &lic.host, + api = &lic.api, + ) + } else { + "".to_owned() + } +} + +pub fn is_rdp_service_open() -> bool { + unsafe { has_rdp_service() == TRUE } +} + +pub fn create_shortcut(id: &str) -> ResultType<()> { + let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + let shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +strDesktop = oWS.SpecialFolders(\"Desktop\") +Set objFSO = CreateObject(\"Scripting.FileSystemObject\") +sLinkFile = objFSO.BuildPath(strDesktop, \"{id}.lnk\") +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--connect {id}\" +oLink.Save + ", + exe = exe, + id = id, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + std::process::Command::new("cscript") + .arg(&shortcut) + .output()?; + allow_err!(std::fs::remove_file(shortcut)); + Ok(()) +} + +pub fn enable_lowlevel_keyboard(hwnd: HWND) { + let ret = unsafe { win32_enable_lowlevel_keyboard(hwnd) }; + if ret != 0 { + log::error!("Failure grabbing keyboard"); + return; + } +} + +pub fn disable_lowlevel_keyboard(hwnd: HWND) { + unsafe { win32_disable_lowlevel_keyboard(hwnd) }; +} + +pub fn stop_system_key_propagate(v: bool) { + unsafe { win_stop_system_key_propagate(if v { TRUE } else { FALSE }) }; +} + +pub fn get_win_key_state() -> bool { + unsafe { is_win_down() == TRUE } +} + +pub fn quit_gui() { + unsafe { PostQuitMessage(0) }; +} \ No newline at end of file diff --git a/src/port_forward.rs b/src/port_forward.rs index 2cd5fd901..afc65f236 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -14,6 +14,26 @@ use hbb_common::{ }; fn run_rdp(port: u16) { + std::process::Command::new("cmdkey") + .arg("/delete:localhost") + .output() + .ok(); + let username = std::env::var("rdp_username").unwrap_or_default(); + let password = std::env::var("rdp_password").unwrap_or_default(); + if !username.is_empty() || !password.is_empty() { + let mut args = vec!["/generic:localhost".to_owned()]; + if !username.is_empty() { + args.push(format!("/user:{}", username)); + } + if !password.is_empty() { + args.push(format!("/pass:{}", password)); + } + println!("{:?}", args); + std::process::Command::new("cmdkey") + .args(&args) + .output() + .ok(); + } std::process::Command::new("mstsc") .arg(format!("/v:localhost:{}", port)) .spawn() @@ -25,6 +45,8 @@ pub async fn listen( port: i32, interface: impl Interface, ui_receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, ) -> ResultType<()> { let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?; let addr = listener.local_addr()?; @@ -40,7 +62,7 @@ pub async fn listen( 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, is_rdp).await { + match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp).await { Ok(Some(stream)) => { let interface = interface.clone(); tokio::spawn(async move { @@ -77,6 +99,8 @@ async fn connect_and_login( ui_receiver: &mut mpsc::UnboundedReceiver, interface: impl Interface, forward: &mut Framed, + key: &str, + token: &str, is_rdp: bool, ) -> ResultType> { let conn_type = if is_rdp { @@ -84,7 +108,7 @@ async fn connect_and_login( } else { ConnType::PORT_FORWARD }; - let (mut stream, _) = Client::start(id, conn_type).await?; + let (mut stream, _) = Client::start(id, key, token, conn_type).await?; let mut interface = interface; let mut buffer = Vec::new(); loop { diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index f454ad85d..f575f684f 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -58,6 +58,7 @@ impl RendezvousMediator { tokio::spawn(async move { direct_server(server_cloned).await; }); + #[cfg(not(any(target_os = "android", target_os = "ios")))] if crate::platform::is_installed() { std::thread::spawn(move || { allow_err!(lan_discovery()); @@ -385,12 +386,7 @@ impl RendezvousMediator { 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 uuid = crate::get_uuid(); let id = Config::get_id(); self.last_id_pk_registry = id.clone(); msg_out.set_register_pk(RegisterPk { @@ -548,11 +544,14 @@ pub fn get_broadcast_port() -> u16 { } pub fn get_mac() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Ok(Some(mac)) = mac_address::get_mac_address() { mac.to_string() } else { "".to_owned() } + #[cfg(any(target_os = "android", target_os = "ios"))] + "".to_owned() } fn lan_discovery() -> ResultType<()> { diff --git a/src/server.rs b/src/server.rs index 55f4d6648..f4758e3fb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,383 +1,413 @@ -use crate::ipc::Data; -pub use connection::*; -use hbb_common::{ - allow_err, - anyhow::{anyhow, Context}, - bail, - config::{Config, Config2, CONNECT_TIMEOUT, RELAY_PORT}, - log, - message_proto::*, - protobuf::{Message as _, ProtobufEnum}, - rendezvous_proto::*, - sleep, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, - timeout, tokio, ResultType, Stream, -}; -use service::{GenericService, Service, ServiceTmpl, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; - -pub 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.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 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? { - stream.set_nodelay(true).ok(); - let stream_addr = stream.local_addr()?; - create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; - } - Ok(()) -} - -pub 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 (our_pk_b, our_sk_b) = box_::gen_keypair(); - msg_out.set_signed_id(SignedId { - id: sign::sign( - &IdPk { - id: Config::get_id(), - pk: our_pk_b.0.to_vec(), - ..Default::default() - } - .write_to_bytes() - .unwrap_or_default(), - &sk, - ), - ..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() { - Config::set_key_confirmed(false); - log::info!("Force to update pk"); - } else { - 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 = socket_client::connect_tcp( - 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(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); - } - }); - input_service::fix_key_down_timeout_loop(); - #[cfg(target_os = "macos")] - tokio::spawn(async { sync_and_watch_config_dir().await }); - crate::RendezvousMediator::start_all().await; - } else { - match crate::ipc::connect(1000, "").await { - Ok(mut conn) => { - if conn.send(&Data::SyncConfig(None)).await.is_ok() { - if let Ok(Some(data)) = conn.next_timeout(1000).await { - match data { - Data::SyncConfig(Some((config, config2))) => { - if Config::set(config) { - log::info!("config synced"); - } - if Config2::set(config2) { - log::info!("config2 synced"); - } - } - _ => {} - } - } - } - } - Err(err) => { - log::info!("server not started (will try to start): {}", err); - std::thread::spawn(|| start_server(true, false)); - } - } - } -} - -#[cfg(target_os = "macos")] -async fn sync_and_watch_config_dir() { - if crate::platform::is_root() { - return; - } - - let mut cfg0 = (Config::get(), Config2::get()); - let mut synced = false; - let tries = - if std::env::args().len() == 2 && std::env::args().nth(1) == Some("--server".to_owned()) { - 30 - } else { - 3 - }; - log::debug!("#tries of ipc service connection: {}", tries); - for i in 1..=tries { - sleep(i as f32 * 0.3).await; - match crate::ipc::connect(1000, "_service").await { - Ok(mut conn) => { - if !synced { - if conn.send(&Data::SyncConfig(None)).await.is_ok() { - if let Ok(Some(data)) = conn.next_timeout(1000).await { - match data { - Data::SyncConfig(Some((config, config2))) => { - let _chk = crate::ipc::CheckIfRestart::new(); - if cfg0.0 != config { - cfg0.0 = config.clone(); - Config::set(config); - log::info!("sync config from root"); - } - if cfg0.1 != config2 { - cfg0.1 = config2.clone(); - Config2::set(config2); - log::info!("sync config2 from root"); - } - synced = true; - } - _ => {} - }; - }; - } - } - - loop { - sleep(0.3).await; - let cfg = (Config::get(), Config2::get()); - if cfg != cfg0 { - log::info!("config updated, sync to root"); - match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { - Err(e) => { - log::error!("sync config to root failed: {}", e); - break; - } - _ => { - cfg0 = cfg; - conn.next_timeout(1000).await.ok(); - } - } - } - } - } - Err(_) => { - log::info!("#{} try: failed to connect to ipc_service", i); - } - } - } - log::error!("skipped config sync"); -} +use crate::ipc::Data; +pub use connection::*; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + bail, + config::{Config, Config2, CONNECT_TIMEOUT, RELAY_PORT}, + log, + message_proto::*, + protobuf::{Message as _, ProtobufEnum}, + rendezvous_proto::*, + socket_client, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, tokio, ResultType, Stream, +}; +use service::{GenericService, Service, ServiceTmpl, Subscriber}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; +pub mod audio_service; +cfg_if::cfg_if! { +if #[cfg(not(any(target_os = "android", target_os = "ios")))] { +mod clipboard_service; +pub mod input_service; +} else { +mod clipboard_service { +pub const NAME: &'static str = ""; +} +pub mod input_service { +pub const NAME_CURSOR: &'static str = ""; +pub const NAME_POS: &'static str = ""; +} +} +} + +mod connection; +mod service; +pub 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())); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + 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.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 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? { + stream.set_nodelay(true).ok(); + let stream_addr = stream.local_addr()?; + create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; + } + Ok(()) +} + +pub 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 (our_pk_b, our_sk_b) = box_::gen_keypair(); + msg_out.set_signed_id(SignedId { + id: sign::sign( + &IdPk { + id: Config::get_id(), + pk: our_pk_b.0.to_vec(), + ..Default::default() + } + .write_to_bytes() + .unwrap_or_default(), + &sk, + ), + ..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() { + Config::set_key_confirmed(false); + log::info!("Force to update pk"); + } else { + 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 = socket_client::connect_tcp( + crate::check_port(relay_server, RELAY_PORT), + Config::get_any_listen_addr(), + CONNECT_TIMEOUT, + ) + .await?; + let mut msg_out = RendezvousMessage::new(); + let mut licence_key = Config::get_option("key"); + if licence_key.is_empty() { + licence_key = crate::platform::get_license_key(); + } + msg_out.set_request_relay(RequestRelay { + licence_key, + 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(Duration::from_millis(100)); + }); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +#[tokio::main] +pub async fn start_server(is_server: bool) { + crate::RendezvousMediator::start_all().await; +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main] +pub async fn start_server(is_server: 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); + } + }); + #[cfg(windows)] + crate::platform::windows::bootstrap(); + input_service::fix_key_down_timeout_loop(); + #[cfg(target_os = "macos")] + tokio::spawn(async { sync_and_watch_config_dir().await }); + crate::RendezvousMediator::start_all().await; + } else { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + if conn.send(&Data::SyncConfig(None)).await.is_ok() { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + Data::SyncConfig(Some((config, config2))) => { + if Config::set(config) { + log::info!("config synced"); + } + if Config2::set(config2) { + log::info!("config2 synced"); + } + } + _ => {} + } + } + } + } + Err(err) => { + log::info!("server not started (will try to start): {}", err); + std::thread::spawn(|| start_server(true)); + } + } + } +} + +#[cfg(target_os = "macos")] +async fn sync_and_watch_config_dir() { + if crate::platform::is_root() { + return; + } + + let mut cfg0 = (Config::get(), Config2::get()); + let mut synced = false; + let tries = + if std::env::args().len() == 2 && std::env::args().nth(1) == Some("--server".to_owned()) { + 30 + } else { + 3 + }; + log::debug!("#tries of ipc service connection: {}", tries); + use hbb_common::sleep; + for i in 1..=tries { + sleep(i as f32 * 0.3).await; + match crate::ipc::connect(1000, "_service").await { + Ok(mut conn) => { + if !synced { + if conn.send(&Data::SyncConfig(None)).await.is_ok() { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + Data::SyncConfig(Some((config, config2))) => { + let _chk = crate::ipc::CheckIfRestart::new(); + if cfg0.0 != config { + cfg0.0 = config.clone(); + Config::set(config); + log::info!("sync config from root"); + } + if cfg0.1 != config2 { + cfg0.1 = config2.clone(); + Config2::set(config2); + log::info!("sync config2 from root"); + } + synced = true; + } + _ => {} + }; + }; + } + } + + loop { + sleep(0.3).await; + let cfg = (Config::get(), Config2::get()); + if cfg != cfg0 { + log::info!("config updated, sync to root"); + match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { + Err(e) => { + log::error!("sync config to root failed: {}", e); + break; + } + _ => { + cfg0 = cfg; + conn.next_timeout(1000).await.ok(); + } + } + } + } + } + Err(_) => { + log::info!("#{} try: failed to connect to ipc_service", i); + } + } + } + log::error!("skipped config sync"); +} diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index 9b79dc3b2..08f4a5d2e 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -1,381 +1,426 @@ -// 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}; -use std::sync::atomic::{AtomicBool, Ordering}; - -pub const NAME: &'static str = "audio"; -pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo -static RESTARTING: AtomicBool = AtomicBool::new(false); - -#[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 -} - -pub fn restart() { - log::info!("restart the audio service, freezing now..."); - if RESTARTING.load(Ordering::SeqCst) { - return; - } - RESTARTING.store(true, Ordering::SeqCst); -} - -#[cfg(target_os = "linux")] -mod pa_impl { - use super::*; - #[tokio::main(flavor = "current_thread")] - pub async fn run(sp: GenericService) -> ResultType<()> { - hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc - RESTARTING.store(false, Ordering::SeqCst); - let mut stream = crate::ipc::connect(1000, "_pa").await?; - unsafe { - AUDIO_ZERO_COUNT = 0; - } - 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 - ); - let zero_audio_frame: Vec = vec![0.; AUDIO_DATA_SIZE_U8 / 4]; - while sp.ok() && !RESTARTING.load(Ordering::SeqCst) { - sp.snapshot(|sps| { - sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2)); - Ok(()) - })?; - if let Ok(data) = stream.next_raw().await { - if data.len() == 0 { - send_f32(&zero_audio_frame, &mut encoder, &sp); - continue; - } - if data.len() != AUDIO_DATA_SIZE_U8 { - continue; - } - let data = unsafe { - std::slice::from_raw_parts::(data.as_ptr() as _, data.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, - ) { - 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)> { - 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| { - // too many UnknownErrno, will improve later - log::trace!("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 - }; - log::debug!("Audio sample rate : {}", sample_rate); - unsafe { - AUDIO_ZERO_COUNT = 0; - } - 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 -} - -// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time -// every audio data length is set to 480 -// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows) -const MAX_AUDIO_ZERO_COUNT: u16 = 800; -static mut AUDIO_ZERO_COUNT: u16 = 0; - -fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { - if data.iter().filter(|x| **x != 0.).next().is_some() { - unsafe { - AUDIO_ZERO_COUNT = 0; - } - } else { - unsafe { - if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT { - if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 { - log::debug!("Audio Zero Gate Attack"); - AUDIO_ZERO_COUNT += 1; - } - return; - } - AUDIO_ZERO_COUNT += 1; - } - } - 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() { - 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"); - } -} +// 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}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub const NAME: &'static str = "audio"; +pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo +static RESTARTING: AtomicBool = AtomicBool::new(false); + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.repeat::(33, cpal_impl::run); + sp +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run(pa_impl::run); + sp +} + +pub fn restart() { + log::info!("restart the audio service, freezing now..."); + if RESTARTING.load(Ordering::SeqCst) { + return; + } + RESTARTING.store(true, Ordering::SeqCst); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod pa_impl { + use super::*; + #[tokio::main(flavor = "current_thread")] + pub async fn run(sp: GenericService) -> ResultType<()> { + hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc + RESTARTING.store(false, Ordering::SeqCst); + #[cfg(target_os = "linux")] + let mut stream = crate::ipc::connect(1000, "_pa").await?; + unsafe { + AUDIO_ZERO_COUNT = 0; + } + let mut encoder = Encoder::new(crate::platform::PA_SAMPLE_RATE, Stereo, LowDelay)?; + #[cfg(target_os = "linux")] + allow_err!( + stream + .send(&crate::ipc::Data::Config(( + "audio-input".to_owned(), + Some(Config::get_option("audio-input")) + ))) + .await + ); + let zero_audio_frame: Vec = vec![0.; AUDIO_DATA_SIZE_U8 / 4]; + while sp.ok() && !RESTARTING.load(Ordering::SeqCst) { + sp.snapshot(|sps| { + sps.send(create_format_msg(crate::platform::PA_SAMPLE_RATE, 2)); + Ok(()) + })?; + #[cfg(target_os = "linux")] + if let Ok(data) = stream.next_raw().await { + if data.len() == 0 { + send_f32(&zero_audio_frame, &mut encoder, &sp); + continue; + } + if data.len() != AUDIO_DATA_SIZE_U8 { + continue; + } + let data = unsafe { + std::slice::from_raw_parts::(data.as_ptr() as _, data.len() / 4) + }; + send_f32(data, &mut encoder, &sp); + } + #[cfg(target_os = "android")] + if let Some(data) = scrap::android::ffi::get_audio_raw() { + let data = unsafe { + std::slice::from_raw_parts::(data.as_ptr() as _, data.len() / 4) + }; + send_f32(data, &mut encoder, &sp); + } else { + hbb_common::sleep(0.1).await; + } + } + Ok(()) + } +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +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, + ) { + 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)> { + 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| { + // too many UnknownErrno, will improve later + log::trace!("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 + }; + log::debug!("Audio sample rate : {}", sample_rate); + unsafe { + AUDIO_ZERO_COUNT = 0; + } + 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 +} + +// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time +// every audio data length is set to 480 +// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows) +const MAX_AUDIO_ZERO_COUNT: u16 = 800; +static mut AUDIO_ZERO_COUNT: u16 = 0; + +fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { + if data.iter().filter(|x| **x != 0.).next().is_some() { + unsafe { + AUDIO_ZERO_COUNT = 0; + } + } else { + unsafe { + if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT { + if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 { + log::debug!("Audio Zero Gate Attack"); + AUDIO_ZERO_COUNT += 1; + } + return; + } + AUDIO_ZERO_COUNT += 1; + } + } + #[cfg(target_os = "android")] + { + // the permitted opus data size are 120, 240, 480, 960, 1920, and 2880 + // if data size is bigger than BATCH_SIZE, AND is an integer multiple of BATCH_SIZE + // then upload in batches + const BATCH_SIZE: usize = 960; + let input_size = data.len(); + if input_size > BATCH_SIZE && input_size % BATCH_SIZE == 0 { + let n = input_size / BATCH_SIZE; + for i in 0..n { + match encoder + .encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE) + { + Ok(data) => { + let mut msg_out = Message::new(); + msg_out.set_audio_frame(AudioFrame { + data, + ..Default::default() + }); + sp.send(msg_out); + } + Err(_) => {} + } + } + } else { + log::debug!("invalid audio data size:{} ", input_size); + return; + } + } + + #[cfg(not(target_os = "android"))] + 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 index e44b0ed75..25597d11c 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -3,119 +3,49 @@ pub use crate::common::{ check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, CONTENT, }; -use clipboard_master::{CallbackResult, ClipboardHandler, Master}; -use hbb_common::{anyhow, ResultType}; -use std::{ - io, sync, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc::SyncSender, - }, - time::Duration, -}; + +struct State { + ctx: Option, +} + +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 } + } +} + +impl super::service::Reset for State { + fn reset(&mut self) { + *CONTENT.lock().unwrap() = Default::default(); + } +} pub fn new() -> GenericService { let sp = GenericService::new(NAME, true); - sp.run::<_>(listen::run); + sp.repeat::(INTERVAL, run); sp } -mod listen { - use super::*; - - static RUNNING: AtomicBool = AtomicBool::new(true); - static WAIT: Duration = Duration::from_millis(33); - - struct ClipHandle { - tx: SyncSender<()>, - } - - impl ClipboardHandler for ClipHandle { - fn on_clipboard_change(&mut self) -> CallbackResult { - if !RUNNING.load(Ordering::SeqCst) { - return CallbackResult::Stop; - } - - let _ = self.tx.send(()); - CallbackResult::Next +fn run(sp: GenericService, state: &mut State) -> ResultType<()> { + if let Some(ctx) = state.ctx.as_mut() { + if let Some(msg) = check_clipboard(ctx, None) { + sp.send(msg); } - - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - if !RUNNING.load(Ordering::SeqCst) { - CallbackResult::Stop - } else { - CallbackResult::StopWithError(error) + sp.snapshot(|sps| { + let txt = crate::CONTENT.lock().unwrap().clone(); + if !txt.is_empty() { + let msg_out = crate::create_clipboard_msg(txt); + sps.send_shared(Arc::new(msg_out)); } - } - } - - #[tokio::main] - pub async fn run(sp: GenericService) -> ResultType<()> { - let mut ctx = match ClipboardContext::new() { - Ok(ctx) => ctx, - Err(err) => { - log::error!("Failed to start {}: {}", NAME, err); - return Err(anyhow::Error::from(err)); - } - }; - - if !RUNNING.load(Ordering::SeqCst) { - RUNNING.store(true, Ordering::SeqCst); - } - - let (tx, rx) = sync::mpsc::sync_channel(12); - let listener = tokio::spawn(async { - log::info!("Clipboard listener running!"); - let _ = Master::new(ClipHandle { tx }).run(); - }); - - check_clipboard(&mut ctx, None); // initialize CONTENT for snapshot - while sp.ok() { - let mut update = None; - sp.snapshot(|sps| { - if sps.has_subscribes() { - update = check_clipboard(&mut ctx, None); - } - // if there is update, msg will be later together, - // otherwise it will be only sent to new subscriber, - // but old subscribers ignored - if update.is_none() { - let txt = crate::CONTENT.lock().unwrap().clone(); - if !txt.is_empty() { - let msg_out = crate::create_clipboard_msg(txt); - sps.send_shared(Arc::new(msg_out)); - } - } - Ok(()) - })?; - if let Some(msg) = update { - sp.send(msg); - } - - if let Ok(_) = rx.recv_timeout(WAIT) { - if let Some(msg) = check_clipboard(&mut ctx, None) { - sp.send(msg); - } - } - } - - RUNNING.store(false, Ordering::SeqCst); - trigger(&mut ctx); - let _ = listener.await; - log::info!("Clipboard listener stopped!"); - - *CONTENT.lock().unwrap() = Default::default(); - Ok(()) - } - - fn trigger(ctx: &mut ClipboardContext) { - let mut old_text = "".to_owned(); - let _ = match ctx.get_text() { - Ok(text) => { - old_text = text; - } - Err(_) => {} - }; - ctx.set_text(old_text).ok(); + Ok(()) + })?; } + Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index e5a00d1c3..e726ba5b0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,7 +1,11 @@ use super::{input_service::*, *}; #[cfg(windows)] use crate::clipboard_file::*; -use crate::{common::update_clipboard, ipc}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::update_clipboard; +use crate::ipc; +#[cfg(any(target_os = "android", target_os = "ios"))] +use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel}; use hbb_common::{ config::Config, fs, @@ -15,14 +19,22 @@ use hbb_common::{ }, tokio_util::codec::{BytesCodec, Framed}, }; +#[cfg(any(target_os = "android", target_os = "ios"))] +use scrap::android::call_input_service_mouse_input; +use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; -use std::sync::mpsc as std_mpsc; +use std::sync::{ + atomic::{AtomicI64, Ordering}, + mpsc as std_mpsc, +}; pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); } +pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); +pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); #[derive(Clone, Default)] pub struct ConnInner { @@ -67,6 +79,8 @@ pub struct Connection { enable_file_transfer: bool, // by peer tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, + peer_info: (String, String), + api_server: String, } impl Subscriber for ConnInner { @@ -151,12 +165,18 @@ impl Connection { disable_clipboard: false, tx_input, video_ack_required: false, + peer_info: Default::default(), + api_server: "".to_owned(), }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] 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); } }); + #[cfg(target_os = "android")] + start_channel(rx_to_cm, tx_from_cm); + if !conn.on_open(addr).await { return; } @@ -184,6 +204,7 @@ impl Connection { }, ); + #[cfg(not(any(target_os = "android", target_os = "ios")))] std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned)); loop { @@ -252,9 +273,9 @@ impl Connection { ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } + #[cfg(windows)] ipc::Data::ClipbaordFile(_clip) => { if conn.file_transfer_enabled() { - #[cfg(windows)] allow_err!(conn.stream.send(&clip_2_msg(_clip)).await); } } @@ -291,6 +312,7 @@ impl Connection { } else { conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); } + conn.post_audit(json!({})); // heartbeat }, Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { @@ -345,9 +367,13 @@ impl Connection { conn.on_close(&err.to_string(), false); } + conn.post_audit(json!({ + "action": "close", + })); log::info!("#{} connection loop exited", id); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { let mut block_input_mode = false; let (tx_blank, rx_blank) = std_mpsc::channel(); @@ -398,6 +424,7 @@ impl Connection { } }, Err(err) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] if block_input_mode { let _ = crate::platform::block_input(true); } @@ -410,6 +437,7 @@ impl Connection { log::info!("Input thread exited"); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_blank(receiver: std_mpsc::Receiver) { let mut last_privacy = false; loop { @@ -443,7 +471,7 @@ impl Connection { rx_from_cm: &mut mpsc::UnboundedReceiver, ) -> ResultType<()> { let mut last_recv_time = Instant::now(); - if let Some(forward) = self.port_forward_socket.as_mut() { + if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); loop { @@ -476,6 +504,7 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } + self.post_audit(json!({})); // heartbeat } } } @@ -523,25 +552,78 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_hash(self.hash.clone()); self.send(msg_out).await; + self.get_api_server(); + self.post_audit(json!({ + "ip": addr.ip(), + "action": "new", + })); true } + fn get_api_server(&mut self) { + self.api_server = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + } + + fn post_audit(&self, v: Value) { + if self.api_server.is_empty() { + return; + } + let url = self.api_server.clone(); + let mut v = v; + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(base64::encode(crate::get_uuid())); + v["Id"] = json!(self.inner.id); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + #[inline] + async fn post_audit_async(url: String, v: Value) -> ResultType<()> { + crate::post_request(url, v.to_string(), "").await?; + Ok(()) + } + async fn send_logon_response(&mut self) { if self.authorized { return; } + let conn_type = if self.file_transfer.is_some() { + 1 + } else if self.port_forward_socket.is_some() { + 2 + } else { + 0 + }; + self.post_audit(json!({"peer": self.peer_info, "Type": conn_type})); #[allow(unused_mut)] let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); + + let mut pi = PeerInfo { + username: username.clone(), + conn_id: self.inner.id, + version: crate::VERSION.to_owned(), + ..Default::default() + }; + + #[cfg(not(target_os = "android"))] + { + pi.hostname = whoami::hostname(); + pi.platform = whoami::platform().to_string(); + } + #[cfg(target_os = "android")] + { + pi.hostname = MOBILE_INFO2.lock().unwrap().clone(); + pi.platform = "Android".into(); + } + 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() - }); + res.set_peer_info(pi); msg_out.set_login_response(res); self.send(msg_out).await; return; @@ -566,20 +648,15 @@ impl Connection { if crate::platform::is_root() { sas_enabled = true; } + #[cfg(not(any(target_os = "android", target_os = "ios")))] 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() - }; + pi.username = username; + pi.sas_enabled = sas_enabled; let mut sub_service = false; if self.file_transfer.is_some() { res.set_peer_info(pi); @@ -641,7 +718,8 @@ impl Connection { self.file && self.enable_file_transfer } - async fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { + fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { + self.peer_info = (peer_id.clone(), name.clone()); self.send_to_cm(ipc::Data::Login { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), @@ -753,7 +831,7 @@ impl Connection { if !crate::is_ip(&lr.username) && 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; + self.try_start_cm(lr.my_id, lr.my_name, false); } else { let mut hasher = Sha256::new(); hasher.update(&Config::get_password()); @@ -787,13 +865,13 @@ impl Connection { .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; + self.try_start_cm(lr.my_id, lr.my_name, false); } else { if failure.0 != 0 { LOGIN_FAILURES.lock().unwrap().remove(&self.ip); } + self.try_start_cm(lr.my_id, lr.my_name, true); 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; } @@ -814,12 +892,26 @@ impl Connection { } else if self.authorized { match msg.union { Some(message::Union::mouse_event(me)) => { + #[cfg(any(target_os = "android", target_os = "ios"))] + if let Err(e) = call_input_service_mouse_input(me.mask, me.x, me.y) { + log::debug!("call_input_service_mouse_input fail:{}", e); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.keyboard { + if is_left_up(&me) { + CLICK_TIME.store(crate::get_time(), Ordering::SeqCst); + } else { + MOUSE_MOVE_TIME.store(crate::get_time(), Ordering::SeqCst); + } self.input_mouse(me, self.inner.id()); } } Some(message::Union::key_event(me)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.keyboard { + if is_enter(&me) { + CLICK_TIME.store(crate::get_time(), Ordering::SeqCst); + } // handle all down as press // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which // make sure all key are released @@ -843,7 +935,9 @@ impl Connection { } } } - Some(message::Union::clipboard(cb)) => { + Some(message::Union::clipboard(cb)) => + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard { update_clipboard(cb, None); } @@ -868,18 +962,21 @@ impl Connection { self.send(fs::new_error(f.id, err, -1)).await; } Ok(files) => { - self.send(fs::new_dir(f.id, files)).await; + self.send(fs::new_dir(f.id, f.path, files)).await; } } } Some(file_action::Union::send(s)) => { let id = s.id; - match fs::TransferJob::new_read(id, s.path, s.include_hidden) { + let path = s.path; + match fs::TransferJob::new_read(id, path.clone(), 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.send(fs::new_dir(id, path, job.files().to_vec())) + .await; self.read_jobs.push(job); self.timer = time::interval(MILLI1); } @@ -1020,7 +1117,9 @@ impl Connection { if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; - self.send_to_cm(ipc::Data::ClipboardFileEnabled(self.file_transfer_enabled())); + self.send_to_cm(ipc::Data::ClipboardFileEnabled( + self.file_transfer_enabled(), + )); } } if let Ok(q) = o.disable_clipboard.enum_value() { @@ -1071,6 +1170,7 @@ impl Connection { } log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { + #[cfg(not(any(target_os = "android", target_os = "ios")))] lock_screen(); } self.tx_to_cm.send(ipc::Data::Close).ok(); @@ -1091,6 +1191,7 @@ impl Connection { } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, @@ -1148,7 +1249,16 @@ async fn start_ipc( return Err(err.into()); } Ok(Some(data)) => { - tx_from_cm.send(data)?; + match data { + ipc::Data::ClickTime(_)=> { + let ct = CLICK_TIME.load(Ordering::SeqCst); + let data = ipc::Data::ClickTime(ct); + stream.send(&data).await?; + } + _ => { + tx_from_cm.send(data)?; + } + } } _ => {} } diff --git a/src/server/service.rs b/src/server/service.rs index e3813c079..35cfdbd77 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -1,275 +1,275 @@ -use super::*; -use std::{ - collections::HashSet, - 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 HIBERNATE_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_to(&self, msg: Message, id: i32) { - if let Some(s) = self.0.write().unwrap().subscribes.get_mut(&id) { - s.send(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_video_frame(&self, msg: Message) -> HashSet { - self.send_video_frame_shared(Arc::new(msg)) - } - - pub fn send_video_frame_shared(&self, msg: Arc) -> HashSet { - let mut conn_ids = HashSet::new(); - let mut lock = self.0.write().unwrap(); - for s in lock.subscribes.values_mut() { - s.send(msg.clone()); - conn_ids.insert(s.id()); - } - conn_ids - } - - 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(); - let mut may_reset = false; - 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(); - } - if !may_reset { - may_reset = true; - } - } else if may_reset { - state.reset(); - may_reset = false; - } - 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 = HIBERNATE_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 = HIBERNATE_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(HIBERNATE_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(); - } -} +use super::*; +use std::{ + collections::HashSet, + 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 HIBERNATE_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_to(&self, msg: Message, id: i32) { + if let Some(s) = self.0.write().unwrap().subscribes.get_mut(&id) { + s.send(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_video_frame(&self, msg: Message) -> HashSet { + self.send_video_frame_shared(Arc::new(msg)) + } + + pub fn send_video_frame_shared(&self, msg: Arc) -> HashSet { + let mut conn_ids = HashSet::new(); + let mut lock = self.0.write().unwrap(); + for s in lock.subscribes.values_mut() { + s.send(msg.clone()); + conn_ids.insert(s.id()); + } + conn_ids + } + + 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(); + let mut may_reset = false; + 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(); + } + if !may_reset { + may_reset = true; + } + } else if may_reset { + state.reset(); + may_reset = false; + } + 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 = HIBERNATE_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 = HIBERNATE_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(HIBERNATE_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 index 9cdae2470..d2acaf4eb 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -32,7 +32,6 @@ use std::{ io::ErrorKind::WouldBlock, time::{self, Duration, Instant}, }; -use virtual_display; pub const NAME: &'static str = "video"; @@ -133,7 +132,7 @@ fn check_display_changed( last_width: usize, last_hegiht: usize, ) -> bool { - let displays = match try_get_displays() { + let displays = match Display::all() { Ok(d) => d, _ => return false, }; @@ -158,30 +157,20 @@ fn check_display_changed( } fn run(sp: GenericService) -> ResultType<()> { - let num_displays = Display::all()?.len(); - if num_displays == 0 { - // Device may sometimes be uninstalled by user in "Device Manager" Window. - // Closing device will clear the instance data. - virtual_display::close_device(); - } else if num_displays > 1 { - // Try close device, if display device changed. - if virtual_display::is_device_created() { - virtual_display::close_device(); - } - } - let fps = 30; let wait = 1000 / fps; let spf = time::Duration::from_secs_f32(1. / (fps as f32)); let (ndisplay, current, display) = get_current_display()?; let (origin, width, height) = (display.origin(), display.width(), display.height()); log::debug!( - "#displays={}, current={}, origin: {:?}, width={}, height={}", + "#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}", ndisplay, current, &origin, width, - height + height, + num_cpus::get_physical(), + num_cpus::get(), ); // Capturer object is expensive, avoiding to create it frequently. let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?; @@ -260,7 +249,31 @@ fn run(sp: GenericService) -> ResultType<()> { frame_controller.reset(); - match c.frame(wait as _) { + #[cfg(any(target_os = "android", target_os = "ios"))] + let res = match c.frame(wait as _) { + Ok(frame) => { + let time = now - start; + let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; + match frame { + scrap::Frame::VP9(data) => { + let send_conn_ids = handle_one_frame_encoded(&sp, data, ms)?; + frame_controller.set_send(now, send_conn_ids); + } + scrap::Frame::RAW(data) => { + if (data.len() != 0) { + let send_conn_ids = handle_one_frame(&sp, data, ms, &mut vpx)?; + frame_controller.set_send(now, send_conn_ids); + } + } + _ => {} + }; + Ok(()) + } + Err(err) => Err(err), + }; + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let res = match c.frame(wait as _) { Ok(frame) => { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; @@ -270,8 +283,14 @@ fn run(sp: GenericService) -> ResultType<()> { { try_gdi = 0; } + Ok(()) } - Err(ref e) if e.kind() == WouldBlock => { + Err(err) => Err(err), + }; + + match res { + Err(ref e) if e.kind() == WouldBlock => + { #[cfg(windows)] if try_gdi > 0 && !c.is_gdi() { if try_gdi > 3 { @@ -298,6 +317,7 @@ fn run(sp: GenericService) -> ResultType<()> { return Err(err.into()); } + _ => {} } // i love 3, 6, 8 @@ -310,7 +330,6 @@ fn run(sp: GenericService) -> ResultType<()> { std::thread::sleep(spf - elapsed); } } - Ok(()) } @@ -370,8 +389,33 @@ fn handle_one_frame( Ok(send_conn_ids) } +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn handle_one_frame_encoded( + sp: &GenericService, + frame: &[u8], + ms: i64, +) -> 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 send_conn_ids: HashSet = Default::default(); + let vp9_frame = VP9 { + data: frame.to_vec(), + key: true, + pts: ms, + ..Default::default() + }; + send_conn_ids = sp.send_video_frame(create_msg(vec![vp9_frame])); + Ok(send_conn_ids) +} + fn get_display_num() -> usize { - if let Ok(d) = try_get_displays() { + if let Ok(d) = Display::all() { d.len() } else { 0 @@ -385,7 +429,7 @@ pub fn get_displays() -> ResultType<(usize, Vec)> { } let mut displays = Vec::new(); let mut primary = 0; - for (i, d) in try_get_displays()?.iter().enumerate() { + for (i, d) in Display::all()?.iter().enumerate() { if d.is_primary() { primary = i; } @@ -416,11 +460,13 @@ pub fn switch_display(i: i32) { } pub fn refresh() { + #[cfg(target_os = "android")] + Display::refresh_size(); *SWITCH.lock().unwrap() = true; } fn get_primary() -> usize { - if let Ok(all) = try_get_displays() { + if let Ok(all) = Display::all() { for (i, d) in all.iter().enumerate() { if d.is_primary() { return i; @@ -434,42 +480,12 @@ pub fn switch_to_primary() { switch_display(get_primary() as _); } -fn try_get_displays() -> ResultType> { - let mut displays = Display::all()?; - if displays.len() == 0 { - log::debug!("no displays, create virtual display"); - // Try plugin monitor - if !virtual_display::is_device_created() { - if let Err(e) = virtual_display::create_device() { - log::debug!("Create device failed {}", e); - } - } - if virtual_display::is_device_created() { - if let Err(e) = virtual_display::plug_in_monitor() { - log::debug!("Plug in monitor failed {}", e); - } else { - if let Err(e) = virtual_display::update_monitor_modes() { - log::debug!("Update monitor modes failed {}", e); - } - } - } - displays = Display::all()?; - } else if displays.len() > 1 { - // If more than one displays exists, close RustDeskVirtualDisplay - if virtual_display::is_device_created() { - virtual_display::close_device() - } - } - Ok(displays) -} - fn get_current_display() -> ResultType<(usize, usize, Display)> { let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize; - let mut displays = try_get_displays()?; + let mut displays = Display::all()?; if displays.len() == 0 { bail!("No displays"); } - let n = displays.len(); if current >= n { current = 0; diff --git a/src/tray-icon.ico b/src/tray-icon.ico index 4df961d5d..fd2e61628 100644 Binary files a/src/tray-icon.ico and b/src/tray-icon.ico differ diff --git a/src/tray.rs b/src/tray.rs new file mode 100644 index 000000000..6b5ab388c --- /dev/null +++ b/src/tray.rs @@ -0,0 +1,77 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; +use trayicon::{MenuBuilder, TrayIconBuilder}; +use winit::{ + event::Event, + event_loop::{ControlFlow, EventLoop}, +}; + +#[derive(Clone, Eq, PartialEq, Debug)] +enum Events { + DoubleClickTrayIcon, + StopService, + StartService, +} + +pub fn start_tray(options: Arc>>) { + let event_loop = EventLoop::::with_user_event(); + let proxy = event_loop.create_proxy(); + let icon = include_bytes!("./tray-icon.ico"); + let mut tray_icon = TrayIconBuilder::new() + .sender_winit(proxy) + .icon_from_buffer(icon) + .tooltip("RustDesk") + .on_double_click(Events::DoubleClickTrayIcon) + .build() + .unwrap(); + let old_state = Arc::new(Mutex::new(0)); + event_loop.run(move |event, _, control_flow| { + if options.lock().unwrap().get("ipc-closed").is_some() { + *control_flow = ControlFlow::Exit; + return; + } else { + *control_flow = ControlFlow::Wait; + } + 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 old = *old_state.lock().unwrap(); + if stopped != old { + hbb_common::log::info!("State changed"); + let mut m = MenuBuilder::new(); + if stopped == 2 { + m = m.item( + &crate::client::translate("Start service".to_owned()), + Events::StartService, + ); + } else { + m = m.item( + &crate::client::translate("Stop service".to_owned()), + Events::StopService, + ); + } + tray_icon.set_menu(&m).ok(); + *old_state.lock().unwrap() = stopped; + } + + match event { + Event::UserEvent(e) => match e { + Events::DoubleClickTrayIcon => { + crate::run_me(Vec::<&str>::new()).ok(); + } + Events::StopService => { + crate::ipc::set_option("stop-service", "Y"); + } + Events::StartService => { + crate::ipc::set_option("stop-service", ""); + } + }, + _ => (), + } + }); +} diff --git a/src/ui.rs b/src/ui.rs index da3348c1c..130903399 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,14 +3,19 @@ mod cm; mod inline; #[cfg(target_os = "macos")] mod macos; -mod remote; +pub mod remote; use crate::common::SOFTWARE_UPDATE_URL; use crate::ipc; use hbb_common::{ allow_err, - config::{self, Config, LocalConfig, PeerConfig, APP_NAME, ICON}, - log, sleep, - tokio::{self, time}, + config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, + tcp::FramedStream, + tokio::{self, sync::mpsc, time}, }; use sciter::Value; use std::{ @@ -19,20 +24,23 @@ use std::{ process::Child, sync::{Arc, Mutex}, }; -use virtual_display; + +type Message = RendezvousMessage; pub type Childs = Arc)>>; +type Status = (i32, bool, i64, String); lazy_static::lazy_static! { // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ static ref STUPID_VALUES: Mutex>>> = Default::default(); } -#[derive(Default)] struct UI( Childs, - Arc>, + Arc>, Arc>>, + Arc>, + mpsc::UnboundedSender, ); struct UIHostHandler; @@ -53,20 +61,14 @@ pub fn start(args: &mut [String]) { allow_err!(sciter::set_options(sciter::RuntimeOptions::GfxLayer( sciter::GFX_LAYER::WARP ))); + #[cfg(all(windows, not(feature = "inline")))] + unsafe { + winapi::um::shellscalingapi::SetProcessDpiAwareness(2); + } #[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); + let options = check_connect_status(false).1; + crate::tray::start_tray(options); return; } use sciter::SCRIPT_RUNTIME_FEATURES::*; @@ -114,6 +116,11 @@ pub fn start(args: &mut [String]) { || args[0] == "--rdp") && args.len() > 1 { + #[cfg(windows)] + { + let hw = frame.get_host().get_hwnd(); + crate::platform::windows::enable_lowlevel_keyboard(hw as _); + } let mut iter = args.iter(); let cmd = iter.next().unwrap().clone(); let id = iter.next().unwrap().clone(); @@ -151,54 +158,10 @@ pub fn start(args: &mut [String]) { 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) + Self(childs, res.0, res.1, Default::default(), res.2) } fn recent_sessions_updated(&mut self) -> bool { @@ -211,7 +174,7 @@ impl UI { } } - fn get_id(&mut self) -> String { + fn get_id(&self) -> String { ipc::get_id() } @@ -239,10 +202,10 @@ impl UI { allow_err!(crate::run_me(vec!["--install"])); } - fn install_me(&mut self, _options: String) { + fn install_me(&mut self, _options: String, _path: String) { #[cfg(windows)] std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me(&_options)); + allow_err!(crate::platform::windows::install_me(&_options, _path)); std::process::exit(0); }); } @@ -273,8 +236,45 @@ impl UI { } } + fn run_without_install(&self) { + crate::run_me(vec!["--noinstall"]).ok(); + std::process::exit(0); + } + + fn show_run_without_install(&self) -> bool { + let mut it = std::env::args(); + if let Some(tmp) = it.next() { + if crate::is_setup(&tmp) { + return it.next() == None; + } + } + false + } + + fn has_rendezvous_service(&self) -> bool { + #[cfg(all(windows, feature = "hbbs"))] + return crate::platform::is_win_server() + && crate::platform::windows::get_license().is_some(); + return false; + } + + fn get_license(&self) -> String { + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + return format!( + "
Key: {}
Host: {} Api: {}", + lic.key, lic.host, lic.api + ); + } + Default::default() + } + fn get_option(&self, key: String) -> String { - if let Some(v) = self.2.lock().unwrap().get(&key) { + self.get_option_(&key) + } + + fn get_option_(&self, key: &str) -> String { + if let Some(v) = self.2.lock().unwrap().get(key) { v.to_owned() } else { "".to_owned() @@ -314,6 +314,10 @@ impl UI { c.store(&id); } + fn using_public_server(&self) -> bool { + crate::get_custom_rendezvous_server(self.get_option_("custom-rendezvous-server")).is_empty() + } + fn get_options(&self) -> Value { let mut m = Value::map(); for (k, v) in self.2.lock().unwrap().iter() { @@ -380,22 +384,6 @@ impl UI { ipc::set_options(options.clone()).ok(); } - // TODO: ui prompt - fn install_virtual_display(&self) { - let mut reboot_required = false; - match virtual_display::install_update_driver(&mut reboot_required) { - Ok(_) => { - log::info!( - "Virtual Display: install virtual display done, reboot is {} required", - if reboot_required { "" } else { "not" } - ); - } - Err(e) => { - log::error!("Virtual Display: install virtual display failed {}", e); - } - } - } - fn install_path(&mut self) -> String { #[cfg(windows)] return crate::platform::windows::get_install_info().1; @@ -426,10 +414,29 @@ impl UI { .ok(); } - fn is_installed(&mut self) -> bool { + fn is_installed(&self) -> bool { crate::platform::is_installed() } + fn is_rdp_service_open(&self) -> bool { + #[cfg(windows)] + return self.is_installed() && crate::platform::windows::is_rdp_service_open(); + #[cfg(not(windows))] + return false; + } + + fn is_share_rdp(&self) -> bool { + #[cfg(windows)] + return crate::platform::windows::is_share_rdp(); + #[cfg(not(windows))] + return false; + } + + fn set_share_rdp(&self, _enable: bool) { + #[cfg(windows)] + crate::platform::windows::set_share_rdp(_enable); + } + fn is_installed_lower_version(&self) -> bool { #[cfg(not(windows))] return false; @@ -457,11 +464,20 @@ impl UI { v } + fn get_mouse_time(&self) -> f64 { + self.1.lock().unwrap().2 as _ + } + + fn check_mouse_time(&self) { + allow_err!(self.4.send(ipc::Data::MouseMoveTime(0))); + } + fn get_connect_status(&mut self) -> Value { let mut v = Value::array(0); - let x = *self.1.lock().unwrap(); + let x = self.1.lock().unwrap().clone(); v.push(x.0); v.push(x.1); + v.push(x.3); v } @@ -508,7 +524,7 @@ impl UI { } fn get_icon(&mut self) -> String { - ICON.to_owned() + crate::get_icon() } fn remove_peer(&mut self, id: String) { @@ -572,7 +588,12 @@ impl UI { return "".to_owned(); } if dtype != "x11" { - return format!("Unsupported display server type {}, x11 expected!", dtype); + return format!( + "{} {}, {}", + self.t("Unsupported display server ".to_owned()), + dtype, + self.t("x11 expected".to_owned()), + ); } } return "".to_owned(); @@ -587,7 +608,7 @@ impl UI { fn fix_login_wayland(&mut self) { #[cfg(target_os = "linux")] - return crate::platform::linux::fix_login_wayland(); + crate::platform::linux::fix_login_wayland(); } fn current_is_wayland(&mut self) -> bool { @@ -617,7 +638,7 @@ impl UI { } fn get_app_name(&self) -> String { - APP_NAME.to_owned() + crate::get_app_name() } fn get_software_ext(&self) -> String { @@ -638,7 +659,7 @@ impl UI { .split("/") .last() .map(|x| x.to_owned()) - .unwrap_or(APP_NAME.to_owned()); + .unwrap_or(crate::get_app_name()); p.push(name); format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) } @@ -658,6 +679,10 @@ impl UI { config::LanPeers::load().peers } + fn get_uuid(&self) -> String { + base64::encode(crate::get_uuid()) + } + fn open_url(&self, url: String) { #[cfg(windows)] let p = "explorer"; @@ -672,6 +697,34 @@ impl UI { allow_err!(std::process::Command::new(p).arg(url).spawn()); } + fn change_id(&self, id: String) { + let status = self.3.clone(); + *status.lock().unwrap() = " ".to_owned(); + let old_id = self.get_id(); + std::thread::spawn(move || { + *status.lock().unwrap() = change_id(id, old_id).to_owned(); + }); + } + + fn post_request(&self, url: String, body: String, header: String) { + let status = self.3.clone(); + *status.lock().unwrap() = " ".to_owned(); + std::thread::spawn(move || { + *status.lock().unwrap() = match crate::post_request_sync(url, body, &header) { + Err(err) => err.to_string(), + Ok(text) => text, + }; + }); + } + + fn is_ok_change_id(&self) -> bool { + machine_uid::get().is_ok() + } + + fn get_async_job_status(&self) -> String { + self.3.clone().lock().unwrap().clone() + } + fn t(&self, name: String) -> String { crate::client::translate(name) } @@ -679,12 +732,21 @@ impl UI { fn is_xfce(&self) -> bool { crate::platform::is_xfce() } + + fn get_api_server(&self) -> String { + crate::get_api_server( + self.get_option_("api-server"), + self.get_option_("custom-rendezvous-server"), + ) + } } impl sciter::EventHandler for UI { sciter::dispatch_script_call! { fn t(String); + fn get_api_server(); fn is_xfce(); + fn using_public_server(); fn get_id(); fn get_password(); fn update_password(String); @@ -695,16 +757,21 @@ impl sciter::EventHandler for UI { fn new_remote(String, bool); fn remove_peer(String); fn get_connect_status(); + fn get_mouse_time(); + fn check_mouse_time(); fn get_recent_sessions(); fn get_peer(String); fn get_fav(); fn store_fav(Value); fn recent_sessions_updated(); fn get_icon(); - fn install_me(String); + fn install_me(String, String); fn is_installed(); fn set_socks(String, String, String); fn get_socks(); + fn is_rdp_service_open(); + fn is_share_rdp(); + fn set_share_rdp(bool); fn is_installed_lower_version(); fn install_path(); fn goto_install(); @@ -724,22 +791,30 @@ impl sciter::EventHandler for UI { fn peer_has_password(String); fn forget_password(String); fn set_peer_option(String, String, String); + fn has_rendezvous_service(); + fn get_license(); fn test_if_valid_server(String); fn get_sound_inputs(); fn set_options(Value); fn set_option(String, String); - fn install_virtual_display(); fn get_software_update_url(); fn get_new_version(); fn get_version(); fn update_me(String); + fn show_run_without_install(); + fn run_without_install(); fn get_app_name(); fn get_software_store_path(); fn get_software_ext(); fn open_url(String); + fn change_id(String); + fn get_async_job_status(); + fn post_request(String, String, String); + fn is_ok_change_id(); fn create_shortcut(String); fn discover(); fn get_lan_peers(); + fn get_uuid(); } } @@ -771,15 +846,19 @@ pub fn check_zombie(childs: Childs) { } } -// notice: avoiding create ipc connection repeatedly, +// notice: avoiding create ipc connecton repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] async fn check_connect_status_( reconnect: bool, - status: Arc>, + status: Arc>, options: Arc>>, + rx: mpsc::UnboundedReceiver, ) { let mut key_confirmed = false; + let mut rx = rx; + let mut mouse_time = 0; + let mut id = "".to_owned(); loop { if let Ok(mut c) = ipc::connect(1000, "").await { let mut timer = time::interval(time::Duration::from_secs(1)); @@ -791,30 +870,47 @@ async fn check_connect_status_( log::error!("ipc connection closed: {}", err); break; } + Ok(Some(ipc::Data::MouseMoveTime(v))) => { + mouse_time = v; + status.lock().unwrap().2 = v; + } Ok(Some(ipc::Data::Options(Some(v)))) => { *options.lock().unwrap() = v } + Ok(Some(ipc::Data::Config((name, Some(value))))) => { + if name == "id" { + id = value; + } + } Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { if x > 0 { x = 1 } key_confirmed = c; - *status.lock().unwrap() = (x as _, key_confirmed); + *status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); } _ => {} } } + Some(data) = rx.recv() => { + allow_err!(c.send(&data).await); + } _ = timer.tick() => { c.send(&ipc::Data::OnlineStatus(None)).await.ok(); c.send(&ipc::Data::Options(None)).await.ok(); + c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); } } } } if !reconnect { - std::process::exit(0); + options + .lock() + .unwrap() + .insert("ipc-closed".to_owned(), "Y".to_owned()); + break; } - *status.lock().unwrap() = (-1, key_confirmed); + *status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); sleep(1.).await; } } @@ -847,13 +943,113 @@ fn get_sound_inputs() -> Vec { fn check_connect_status( reconnect: bool, -) -> (Arc>, Arc>>) { - let status = Arc::new(Mutex::new((0, false))); +) -> ( + Arc>, + Arc>>, + mpsc::UnboundedSender, +) { + let status = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); let options = Arc::new(Mutex::new(Config::get_options())); let cloned = status.clone(); let cloned_options = options.clone(); - std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options)); - (status, options) + let (tx, rx) = mpsc::unbounded_channel::(); + std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options, rx)); + (status, options, tx) +} + +const INVALID_FORMAT: &'static str = "Invalid format"; +const UNKNOWN_ERROR: &'static str = "Unknown error"; + +#[tokio::main(flavor = "current_thread")] +async fn change_id(id: String, old_id: String) -> &'static str { + if !hbb_common::is_valid_custom_id(&id) { + return INVALID_FORMAT; + } + let uuid = machine_uid::get().unwrap_or("".to_owned()); + if uuid.is_empty() { + return UNKNOWN_ERROR; + } + let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; + let mut futs = Vec::new(); + let err: Arc> = Default::default(); + for rendezvous_server in rendezvous_servers { + let err = err.clone(); + let id = id.to_owned(); + let uuid = uuid.clone(); + let old_id = old_id.clone(); + futs.push(tokio::spawn(async move { + let tmp = check_id(rendezvous_server, old_id, id, uuid).await; + if !tmp.is_empty() { + *err.lock().unwrap() = tmp; + } + })); + } + join_all(futs).await; + let err = *err.lock().unwrap(); + if err.is_empty() { + crate::ipc::set_config_async("id", id.to_owned()).await.ok(); + } + err +} + +async fn check_id( + rendezvous_server: String, + old_id: String, + id: String, + uuid: String, +) -> &'static str { + let any_addr = Config::get_any_listen_addr(); + if let Ok(mut socket) = FramedStream::new( + crate::check_port(rendezvous_server, RENDEZVOUS_PORT), + any_addr, + RENDEZVOUS_TIMEOUT, + ) + .await + { + let mut msg_out = Message::new(); + msg_out.set_register_pk(RegisterPk { + old_id, + id, + uuid: uuid.into(), + ..Default::default() + }); + let mut ok = false; + if socket.send(&msg_out).await.is_ok() { + if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::register_pk_response(rpr)) => { + match rpr.result.enum_value_or_default() { + register_pk_response::Result::OK => { + ok = true; + } + register_pk_response::Result::ID_EXISTS => { + return "Not available"; + } + register_pk_response::Result::TOO_FREQUENT => { + return "Too frequent"; + } + register_pk_response::Result::NOT_SUPPORT => { + return "This function is turned off by the server"; + } + register_pk_response::Result::INVALID_ID_FORMAT => { + return INVALID_FORMAT; + } + _ => {} + } + } + _ => {} + } + } + } + } + if !ok { + return UNKNOWN_ERROR; + } + } else { + return "Failed to connect to rendezvous server"; + } + "" } // sacrifice some memory diff --git a/src/ui/ab.tis b/src/ui/ab.tis index 2310e9ba4..2dfa87f47 100644 --- a/src/ui/ab.tis +++ b/src/ui/ab.tis @@ -1,3 +1,210 @@ +var selectTags = []; +var ab = { tags: [], peers: [] }; +var abLoading; +var abError; +var current_menu_peer_id = ''; +var current_menu_tag = ''; + +class AddressBook: Reactor.Component +{ + this var style; + this var selectedTags = function() { + var tags = handler.get_local_option("selected-tags"); + if (tags) return tags.split(","); + return []; + }(); + + function render() { + if (!handler.get_local_option("access_token")) { + return
{translate("Login")}
; + } + if (abLoading) { + return
; + } else if (abError) { + return
{abError} +
{translate("Retry")}
+
; + } + var peers = this.getPeers(); + var me = this; + return
+ + +
  • {translate('Add ID')}
  • +
  • {translate('Add Tag')}
  • +
  • {translate('Unselect all tags')}
  • + + +
  • {translate('Remove')}
  • + +
    +
    +
    {translate('Tags')}{svg_menu}
    +
    + {ab.tags.map(function(t) { + return = 0 ? "active" : "inactive"}>{t}; + })} +
    +
    +
    +
    + +
    +
    +
    ; + } + + event mouseup $(#tags span) (evt, me) { + if(evt.propButton) { + current_menu_tag = me.text; + me.popup($(#tag-context)); + return true; + } + } + + event click $(#retry) (_, __) { + refreshCurrentUser(); + } + + event click $(#login-link) (_, __) { + login(); + } + + event click $(#tags-label svg#menu) (_, me) { + me.popup($(#ab-context)); + } + + event click $(#add-id) (_, __) { + var me = this; + msgbox("custom-add-id", translate("Add ID"),
    +
    {translate("whitelist_sep")}
    + +
    , function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + var values = value.split(/[\s,;\n]+/g); + if (values.length == 0) return; + for (var v in values) { + var found; + for (var i = 0; i < ab.peers.length; ++i) { + if (ab.peers[i].id == v) { + found = true; + break; + } + } + if (found) continue; + ab.peers.push({ id: v }); + } + updateAb(); + me.update(); + }, 300); + } + + event click $(#add-tag) (_, __) { + var me = this; + msgbox("custom-add-tag", translate("Add Tag"),
    +
    {translate("whitelist_sep")}
    + +
    , function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + var values = value.split(/[\s,;\n]+/g); + if (values.length == 0) return; + for (var v in values) { + if (ab.tags.indexOf(v) < 0) { + ab.tags.push(v); + } + } + updateAb(); + me.update(); + }, 300); + } + + event click $(#remove-tag) (_, me) { + var tag = current_menu_tag; + var i = ab.tags.indexOf(tag); + ab.tags.splice(i, 1); + for (var p in ab.peers) { + if (p.tags) { + i = p.tags.indexOf(tag); + if (i >= 0) p.tags.splice(i, 1); + } + } + updateAb(); + this.update(); + } + + event click $(#unselect-tags) (_, me) { + this.selectedTags = []; + handler.set_local_option("selected-tags", ""); + this.update(); + } + + event click $(#tags span) (_, me) { + me.attributes.toggleClass('active'); + if (me.attributes.hasClass('active')) { + this.selectedTags.push(me.text); + } else { + this.selectedTags.splice(this.selectedTags.indexOf(me.text), 1); + } + handler.set_local_option("selected-tags", this.selectedTags.join(',')); + this.update(); + } + + function getPeers() { + var tags = []; + for (var t in this.selectedTags) { + if (ab.tags.indexOf(t) >= 0) tags.push(t); + } + if (tags.length != this.selectedTags.length) { + this.selectedTags = tags; + handler.set_local_option("selected-tags", tags.join(",")); + stdout.println("updated selected tags"); + } + if (tags.length == 0) return ab.peers; + var peers = []; + if (tags.length > 0) { + for (var p in ab.peers) { + for (var t in (p.tags || [])) { + if (tags.indexOf(t) >= 0) { + peers.push(p); + break; + } + } + } + } else { + peers = ab.peers; + } + return peers; + } +} + +class SelectTags: Reactor.Component { + function this(params) { + selectTags = this; + this.tags = params.tags; + } + + function render() { + var me = this; + return
    + {ab.tags.map(function(t) { + return = 0 ? "active" : "inactive"}>{t}; + })} +
    ; + } + + event click $(#tags span) (_, me) { + me.attributes.toggleClass('active'); + var i = this.tags.indexOf(me.text); + if (i < 0) { + this.tags.push(me.text); + } else { + this.tags.splice(i, 1); + } + } +} + var svg_tile = ; var svg_list = ; var search_icon = ; @@ -14,7 +221,6 @@ function getSessionsStyle(type) { } var searchPatterns = {}; -var current_menu_peer_id = ''; class SearchBar: Reactor.Component { this var type = ""; @@ -67,7 +273,11 @@ class SessionStyle: Reactor.Component { var option = getSessionsStyleOption(this.type); var sessionsStyle = getSessionsStyle(this.type); handler.set_local_option(option, sessionsStyle == "tile" ? "list" : "tile"); - app.multipleSessions.update(); + if (is_linux) { + app.multipleSessions.stupidUpdate(); + } else { + app.multipleSessions.update(); + } } } @@ -106,6 +316,7 @@ class SessionList: Reactor.Component {
  • {translate('Connect')}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • + {false && !handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • }
  • RDP
  • {translate('Rename')}
  • @@ -114,6 +325,7 @@ class SessionList: Reactor.Component {
  • {translate('Unremember Password')}
  • {(!this.type || this.type == "fav") &&
  • {translate('Add to Favorites')}
  • } {(!this.type || this.type == "fav") &&
  • {translate('Remove from Favorites')}
  • } + {this.type == "ab" &&
  • {translate('Edit Tag')}
  • } {sessions} @@ -179,6 +391,12 @@ class SessionList: Reactor.Component { // https://sciter.com/forums/topic/replacecustomize-context-menu/ var menu = this.$(menu#remote-context); current_menu_peer_id = id; + var el = this.$(li#force-always-relay); + if (el) { + var force = handler.get_peer_option(id, "force-always-relay"); + el.attributes.toggleClass("selected", force == "Y"); + el.attributes.toggleClass("line-through", force != "Y"); + } var conn = this.$(menu #connect); if (conn) { var alias = me.parent.parent.$(#alias); @@ -202,7 +420,16 @@ class SessionList: Reactor.Component { } else if (action == "transfer") { createNewConnect(id, "file-transfer"); } else if (action == "remove") { - if (!this.type) { + if (this.type == "ab") { + for (var i = 0; i < ab.peers.length; ++i) { + if (ab.peers[i].id == id) { + ab.peers.splice(i, 1); + app.update(); + updateAb(); + break; + } + } + } else { handler.remove_peer(id); app.update(); } @@ -211,6 +438,10 @@ class SessionList: Reactor.Component { } else if (action == "shortcut") { handler.create_shortcut(id); } else if (action == "rdp") { + if (is_edit_rdp_port) { + is_edit_rdp_port = false; + return; + } createNewConnect(id, "rdp"); } else if (action == "add-fav") { var favs = handler.get_fav(); @@ -230,6 +461,15 @@ class SessionList: Reactor.Component { createNewConnect(id, "port-forward"); } else if (action == "rename") { var old_name = handler.get_peer_option(id, "alias"); + var abPeer; + if (this.type == "ab") { + for (var v in ab.peers) { + if (v.id == id) { + abPeer = v; + old_name = v.alias || ""; + } + } + } msgbox("custom-rename", "Rename", "
    \
    \
    \ @@ -237,10 +477,30 @@ class SessionList: Reactor.Component { if (!res) return; var name = (res.name || "").trim(); if (name != old_name) { + if (abPeer) { + abPeer.alias = name; + updateAb(); + } handler.set_peer_option(id, "alias", name); } app.update(); }); + } else if (action == "force-always-relay") { + var force = handler.get_peer_option(id, "force-always-relay"); + handler.set_peer_option(id, "force-always-relay", force == "Y" ? "" : "Y"); + } else if (action == "edit-tag") { + var peer; + for (var v in ab.peers) { + if (v.id == id) { + peer = v; + } + } + if (!peer) return; + msgbox("custom-edit-tag", "Edit Tag", , function(res=null) { + if (!res) return; + peer.tags = selectTags.tags; + updateAb(); + }, 260, 500, 0, "size: *; margin: 2em 0;"); } } } @@ -267,6 +527,7 @@ class MultipleSessions: Reactor.Component { {translate('Recent Sessions')} {translate('Favorites')} {handler.is_installed() && {translate('Discovered')}} + {translate('Address Book')}
    {!this.hidden && } {!this.hidden && } @@ -274,6 +535,7 @@ class MultipleSessions: Reactor.Component { {!this.hidden && ((type == "fav" && ) || (type == "lan" && handler.is_installed() && ) || + (type == "ab" && ) || )} ; } @@ -349,3 +611,146 @@ class LanPeers: Reactor.Component { } view.on("size", function() { if (app && app.multipleSessions) app.multipleSessions.onSize(); }); + +/* +{ + peers: [{id: "abcd", username: "", hostname: "", platform: "", alias: "", tags: ["", "", ...]}, ...], + tags: [], +} +*/ + +function handleAbError(err) { + abLoading = false; + err = translate(err); + stderr.println(err); + abError = err; + app.update(); +} + +function getAb() { + abLoading = true; + abError = ""; + app.update(); + httpRequest(handler.get_api_server() + "/api/ab/get", #post, {}, function(data) { + if (data) { + if (data.error) { + handleAbError(data.error); + return; + } + var tm = data.updated_at; + ab = JSON.parse(data.data); + if (!ab.tags) ab.tags = []; + if (!ab.peers) ab.peers = []; + } + abLoading = false; + app.update(); + }, function(err, status) { + handleAbError(err); + }, getHttpHeaders()); +} + +function updateAb() { + httpRequest(handler.get_api_server() + "/api/ab", #post, { data: JSON.stringify(ab) }, function(data) { + }, function(err, status) { + }, getHttpHeaders()); +} + +function resetAb() { + ab = { tags: [], peers: [] }; + app.update(); +} + +function updateAbPeer() { + if (ab.peers.length == 0) return; + // to-do: inefficient + var sessions = handler.get_recent_sessions(); + if (sessions.length == 0) return; + var s = sessions[0]; + var id = s[0] || ""; + var p; + for (var tmp in ab.peers) { + if (tmp.id == id) p = tmp; + } + if (!p) return; + var username = s[1] || ""; + var hostname = s[2] || ""; + var platform = s[3] || ""; + var alias = s[4] || ""; + var updated; + if (username != (p.username || "")) { + p.username = username; + updated = true; + } + if (hostname != (p.hostname || "")) { + p.hostname = hostname; + updated = true; + } + if (platform != (p.platform || "")) { + p.platform = platform; + updated = true; + } + if (alias != (p.alias || "")) { + if (alias) { + p.alias = alias; + } else if (p.alias) { + handler.set_peer_option(id, "alias", p.alias); + } + updated = true; + } + if (updated) { + updateAb(); + stdout.println("Ab peer updated"); + } +} + +var is_edit_rdp_port; +class EditRdpPort: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_rdp_port = true; + editRdpPort(); + } + } +} + +function editRdpPort() { + var id = current_menu_peer_id; + var p0 = handler.get_peer_option(id, "rdp_port"); + var port = p0 ? : + ; + var name0 = handler.get_peer_option(id, "rdp_username"); + var pass0 = handler.get_peer_option(id, "rdp_password"); + msgbox("custom-rdp-port", 'RDP ' + translate('Settings'),
    +
    {translate('Port')}:{port}
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    +
    , function(res=null) { + if (!res) return; + var p = (res.port || '').trim(); + if (p != p0) { + if (!p) p = '0'; + p = p.toNumber(); + if (p < 0 || p != p.toInteger()) { + return translate("Invalid port"); + } + if (p == 0) p = ""; + else p = p.toInteger() + ''; + handler.set_peer_option(id, "rdp_port", p); + } + + var name = (res.username || '').trim(); + if (name != name0) { + handler.set_peer_option(id, "rdp_username", name); + } + + var pass = (res.password || '').trim(); + if (pass != pass0) { + handler.set_peer_option(id, "rdp_password", pass); + } + }, 240); +} + diff --git a/src/ui/cm.rs b/src/ui/cm.rs index f7e56d6a0..1fafedde7 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -5,7 +5,7 @@ use clipboard::{ }; use hbb_common::{ allow_err, - config::{Config, ICON}, + config::Config, fs, log, message_proto::*, protobuf::Message as _, @@ -21,6 +21,7 @@ use std::{ pub struct ConnectionManagerInner { root: Option, senders: HashMap>, + click_time: i64, } #[derive(Clone)] @@ -41,6 +42,7 @@ impl ConnectionManager { let inner = ConnectionManagerInner { root: None, senders: HashMap::new(), + click_time: Default::default(), }; let cm = Self(Arc::new(RwLock::new(inner))); let cloned = cm.clone(); @@ -49,7 +51,18 @@ impl ConnectionManager { } fn get_icon(&mut self) -> String { - ICON.to_owned() + crate::get_icon() + } + + 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(0))); + } + } + + fn get_click_time(&self) -> f64 { + self.read().unwrap().click_time as _ } #[inline] @@ -112,6 +125,9 @@ impl ConnectionManager { Data::ChatMessage { text } => { self.call("newMessage", &make_args!(id, text)); } + Data::ClickTime(ms) => { + self.write().unwrap().click_time = ms; + } Data::FS(v) => match v { ipc::FS::ReadDir { dir, @@ -330,6 +346,8 @@ impl sciter::EventHandler for ConnectionManager { sciter::dispatch_script_call! { fn t(String); + fn check_click_time(i32); + fn get_click_time(); fn get_icon(); fn close(i32); fn authorize(i32); @@ -421,7 +439,6 @@ async fn start_ipc(cm: ConnectionManager) { #[tokio::main(flavor = "current_thread")] async fn start_pa() { use crate::audio_service::AUDIO_DATA_SIZE_U8; - use hbb_common::config::APP_NAME; match new_listener("_pa").await { Ok(mut incoming) => { @@ -448,14 +465,14 @@ async fn start_pa() { let spec = pulse::sample::Spec { format: pulse::sample::Format::F32le, channels: 2, - rate: crate::platform::linux::PA_SAMPLE_RATE, + rate: crate::platform::PA_SAMPLE_RATE, }; log::info!("pa monitor: {:?}", device); // systemctl --user status pulseaudio.service let mut buf: Vec = vec![0; AUDIO_DATA_SIZE_U8]; match psimple::Simple::new( None, // Use the default server - APP_NAME, // Our application’s name + &crate::get_app_name(), // Our application’s name pulse::stream::Direction::Record, // We want a record stream Some(&device), // Use the default device "record", // Description of our stream diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 81ace88b6..dfb853436 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -226,7 +226,13 @@ event click $(div.chaticon) { } function checkClickTime(callback) { - callback(); + var click_callback_time = getTime(); + handler.check_click_time(body.cid); + self.timer(120ms, function() { + var d = click_callback_time - handler.get_click_time(); + if (d > 120) + callback(); + }); } function adaptSizeForChat() { @@ -234,10 +240,10 @@ function adaptSizeForChat() { display: show_chat ? "block" : "none", }; 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); + if (show_chat && w < scaleIt(600)) { + view.move(x - (scaleIt(600) - w), y, scaleIt(600), h); + } else if (!show_chat && w > scaleIt(450)) { + view.move(x + (w - scaleIt(300)), y, scaleIt(300), h); } } @@ -327,8 +333,8 @@ view << event statechange { function self.ready() { adjustBorder(); var (sw, sh) = view.screenBox(#workarea, #dimension); - var w = 300; - var h = 400; + var w = scaleIt(300); + var h = scaleIt(400); view.move(sw - w, 0, w, h); } @@ -372,7 +378,7 @@ function self.closing() { function adjustHeader() { - var hw = $(header).box(#width); + var hw = $(header).box(#width) / scaleFactor; var tabswrapper = $(div.tabs-wrapper); var tabs = $(div.tabs); var arrows = $(div.tab-arrows); @@ -380,7 +386,7 @@ function adjustHeader() { var n = connections.length; var wtab = 80; var max = hw - 98; - var need_width = n * wtab + 2; // include border of active tab + var need_width = n * wtab + scaleIt(2); // include border of active tab if (need_width < max) { arrows.style.set { display: "none", diff --git a/src/ui/common.css b/src/ui/common.css index de724f298..bc5f95a67 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -324,6 +324,33 @@ menu li.line-through { color: red; } +#tags { + border: color(border) 1px solid; + size: *; + padding: 0.5em; + overflow-y: scroll-indicator; + border-spacing: 0.5em; + flow: horizontal-flow; +} + +#tags span { + display: inline-block; + border: color(border) 1px solid; + border-radius: 6px; + padding: 3px 0.5em; + word-wrap: normal; +} + +#tags span.active { + background: color(button); + border-color: color(button); + color: white; +} + +#tags span:hover { + border-color: color(hover-border); +} + div#msgbox .msgbox-icon svg { size: 80px; background: white; diff --git a/src/ui/common.tis b/src/ui/common.tis index 4f0e699a4..46adc3288 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -16,6 +16,19 @@ function isEnterKey(evt) { (is_linux && evt.keyCode == 65421)); } +function getScaleFactor() { + if (!is_win) return 1; + return self.toPixels(10000dip) / 10000.; +} +var scaleFactor = getScaleFactor(); +view << event resolutionchange { + scaleFactor = getScaleFactor(); +} +function scaleIt(x) { + return (x * scaleFactor).toInteger(); +} +stdout.println("scaleFactor", scaleFactor); + function translate(name) { try { return handler.t(name); @@ -226,6 +239,8 @@ function msgbox(type, title, content, callback=null, height=180, width=500, hasR } var remember = false; try { remember = handler.get_remember(); } catch(e) {} + var auto_login = false; + try { auto_login = handler.get_option("auto-login") != ''; } catch(e) {} width += is_xfce ? 50 : 0; height += is_xfce ? 50 : 0; @@ -248,7 +263,7 @@ function msgbox(type, title, content, callback=null, height=180, width=500, hasR } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } } - $(#msgbox).content(); + $(#msgbox).content(); } function connecting() { @@ -361,10 +376,32 @@ class PasswordComponent: Reactor.Component { } } +// type: #post, #get, #delete, #put +function httpRequest(url, type, params, _onSuccess, _onError, headers="") { + if (type != #post) { + stderr.println("only post ok"); + } + handler.post_request(url, JSON.stringify(params), headers); + function check_status() { + var status = handler.get_async_job_status(); + if (status == " ") self.timer(0.1s, check_status); + else { + try { + var data = JSON.parse(status); + _onSuccess(data); + } catch (e) { + _onError(status, 0); + } + } + } + check_status(); +} + function isReasonableSize(r) { var x = r[0]; var y = r[1]; - return !(x < -3200 || x > 3200 || y < -3200 || y > 3200); + var n = scaleIt(3200); + return !(x < -n || x > n || y < -n || y > n); } function awake() { diff --git a/src/ui/header.tis b/src/ui/header.tis index ba0b79b84..2520549b7 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -60,7 +60,39 @@ function stateChanged() { var header; var old_window_state = View.WINDOW_SHOWN; +var is_edit_os_password; +class EditOsPassword: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_os_password = true; + editOSPassword(); + } + } +} + +function editOSPassword(login=false) { + var p0 = handler.get_option('os-password'); + msgbox("custom-os-password", 'OS Password', p0, function(res=null) { + if (!res) return; + var a0 = handler.get_option('auto-login') != ''; + var p = (res.password || '').trim(); + var a = res.auto_login || false; + if (p == p0 && a == a0) return; + if (p != p0) handler.set_option('os-password', p); + if (a != a0) handler.set_option('auto-login', a ? 'Y' : ''); + if (p && login) { + handler.input_os_password(p, true); + } + }); +} + class Header: Reactor.Component { + this var conn_note = ""; + function this() { header = this; } @@ -138,8 +170,10 @@ class Header: Reactor.Component { function renderActionPop() { return + {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • + {handler.get_audit_server() &&
  • {translate('Note')}
  • }
    {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} @@ -224,11 +258,33 @@ class Header: Reactor.Component { event click $(#transfer-file) { handler.transfer_file(); } + + event click $(#os-password) (evt) { + if (is_edit_os_password) { + is_edit_os_password = false; + return; + } + var p = handler.get_option('os-password'); + if (!p) editOSPassword(true); + else handler.input_os_password(p, true); + } event click $(#tunnel) { handler.tunnel(); } + event click $(#note) { + var self = this; + msgbox("custom", "Note",
    + +
    , function(res=null) { + if (!res) return; + if (!res.text) return; + self.conn_note = res.text; + handler.send_note(res.text); + }, 280); + } + event click $(#ctrl-alt-del) { handler.ctrl_alt_del(); } @@ -355,7 +411,7 @@ function updateWindowToolbarPosition() { var el = $(div.window-toolbar); var w1 = el.box(#width, #border); var w2 = $(header).box(#width, #border); - var x = (w2 - w1) / 2; + var x = (w2 - w1) / 2 / scaleFactor; el.style.set { left: x + "px", display: "block", @@ -391,10 +447,10 @@ function startChat() { } var icon = handler.get_icon(); var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); - var w = 300; - var h = 400; + var w = scaleIt(300); + var h = scaleIt(400); var x = (sx + sw - w) / 2; - var y = sy + 80; + var y = sy + scaleIt(80); var params = { type: View.FRAME_WINDOW, x: x, diff --git a/src/ui/index.css b/src/ui/index.css index a71de4848..75b9f5b52 100644 --- a/src/ui/index.css +++ b/src/ui/index.css @@ -34,6 +34,20 @@ body { border-right: color(border) 1px solid; } +#ab .left-pane { + background: white; + border-radius: 1em; + padding: 1em; +} + +#ab .right-pane { + background: none; +} + +#ab .right-content { + overflow: unset; +} + .left-pane > div:nth-child(1) { border-spacing: 1em; padding: 20px; @@ -358,15 +372,19 @@ div.trust-me > div:nth-child(5) { text-align: center; } -div#myid { +div#myid, div#tags-label { position: relative; } -div#myid svg#menu { +div#myid svg#menu, div#tags-label svg#menu { position: absolute; right: -1em; } +div#tags-label svg#menu:hover { + background-color: #ddd; +} + div.remote-session svg#menu { position: absolute; right: 0; diff --git a/src/ui/index.tis b/src/ui/index.tis index a77746c56..cc4952b6c 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1,13 +1,16 @@ if (is_osx) view.windowBlurbehind = #light; stdout.println("current platform:", OS); +stdout.println("is_xfce: ", is_xfce); // html min-width, min-height not working on mac, below works for all -view.windowMinSize = (500, 300); +view.windowMinSize = (scaleIt(500), scaleIt(300)); var app; var tmp = handler.get_connect_status(); var connect_status = tmp[0]; var service_stopped = handler.get_option("stop-service") == "Y"; +var rendezvous_service_stopped = false; +var using_public_server = handler.using_public_server(); var software_update_url = ""; var key_confirmed = tmp[1]; var system_error = ""; @@ -42,12 +45,17 @@ class ConnectStatus: Reactor.Component { } else if (connect_status == 0) { return translate('connecting_status'); } - return translate("Ready"); + if (!handler.using_public_server()) return translate('Ready'); + return {translate("Ready")}, {translate("setup_server_tip")}; } event click $(#start-service) () { handler.set_option("stop-service", ""); } + + event click $(#setup-server) () { + handler.open_url("https://rustdesk.com/blog/id-relay-set/"); + } } function createNewConnect(id, type) { @@ -62,6 +70,19 @@ function createNewConnect(id, type) { handler.new_remote(id, type); } +class ShareRdp: Reactor.Component { + function render() { + var rdp_shared_string = translate("Enable RDP session sharing"); + var cls = handler.is_share_rdp() ? "selected" : "line-through"; + return
  • {svg_checkmark}{rdp_shared_string}
  • ; + } + + function onClick() { + handler.set_share_rdp(!handler.is_share_rdp()); + this.update(); + } +} + var direct_server; class DirectServer: Reactor.Component { function this() { @@ -144,6 +165,13 @@ class AudioInputs: Reactor.Component { } } +function getUserName() { + try { + return JSON.parse(handler.get_local_option("user_info")).name; + } catch(e) {} + return ''; +} + class MyIdMenu: Reactor.Component { function this() { myIdMenu = this; @@ -152,11 +180,12 @@ class MyIdMenu: Reactor.Component { function render() { return
    {this.renderPop()} - {translate("ID")}{svg_menu} + ID{svg_menu}
    ; } function renderPop() { + var username = handler.get_local_option("access_token") ? getUserName() : ''; return
  • {svg_checkmark}{translate('Enable Keyboard/Mouse')}
  • @@ -164,16 +193,24 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate('Enable File Transfer')}
  • {svg_checkmark}{translate('Enable TCP Tunneling')}
  • +
  • {svg_checkmark}{translate('Enable remote configuration modification')}
  • {translate('ID/Relay Server')}
  • {translate('IP Whitelisting')}
  • {translate('Socks5 Proxy')}
  • - {is_win ?
  • Install virtual display
  • : ""}
    -
  • {svg_checkmark}{translate("Enable Service")}
  • +
  • {svg_checkmark}{translate("Enable Service")}
  • + {handler.is_rdp_service_open() ? : ""} + {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connected via relay')}
  • } + {handler.has_rendezvous_service() ?
  • {translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}
  • : ""} + {handler.is_ok_change_id() ?
    : ""} + {username ? +
  • {translate('Logout')} ({username})
  • : +
  • {translate('Login')}
  • } + {handler.is_ok_change_id() && key_confirmed ?
  • {translate('Change ID')}
  • : ""}
    -
  • {translate('About')} {" "} {handler.get_app_name()}
  • +
  • {translate('About')} {" "}{handler.get_app_name()}
  • ; } @@ -190,15 +227,25 @@ class MyIdMenu: Reactor.Component { this.$(svg#menu).popup(menu); } + event click $(li#login) () { + login(); + } + + event click $(li#logout) () { + logout(); + } + function toggleMenuState() { for (var el in $$(menu#config-options>li)) { if (el.id && el.id.indexOf("enable-") == 0) { var enabled = handler.get_option(el.id) != "N"; el.attributes.toggleClass("selected", enabled); el.attributes.toggleClass("line-through", !enabled); - } else if (el.id && el.id === "stop-service") { - el.attributes.toggleClass("selected", !service_stopped); - el.attributes.toggleClass("line-through", service_stopped); + } + if (el.id && el.id.indexOf("allow-") == 0) { + var enabled = handler.get_option(el.id) == "Y"; + el.attributes.toggleClass("selected", enabled); + el.attributes.toggleClass("line-through", !enabled); } } } @@ -207,9 +254,10 @@ class MyIdMenu: Reactor.Component { var name = handler.get_app_name(); msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \
    Version: " + handler.get_version() + " \ -
    Privacy Statement
    \ -
    Website
    \ +
    Privacy Statement
    \ +
    Website
    \
    Copyright © 2022 Purslane Ltd.\ +
    " + handler.get_license() + " \

    Made with heart in this chaotic world!

    \
    \
    ", function(el) { @@ -223,11 +271,14 @@ class MyIdMenu: Reactor.Component { if (me.id && me.id.indexOf("enable-") == 0) { handler.set_option(me.id, handler.get_option(me.id) == "N" ? "" : "N"); } + if (me.id && me.id.indexOf("allow-") == 0) { + handler.set_option(me.id, handler.get_option(me.id) == "Y" ? "" : "Y"); + } if (me.id == "whitelist") { var old_value = handler.get_option("whitelist").split(",").join("\n"); msgbox("custom-whitelist", translate("IP Whitelisting"), "
    \
    " + translate("whitelist_sep") + "
    \ - \ + \
    \ ", function(res=null) { if (!res) return; @@ -248,16 +299,22 @@ class MyIdMenu: Reactor.Component { } else if (me.id == "custom-server") { var configOptions = handler.get_options(); var old_relay = configOptions["relay-server"] || ""; + var old_api = configOptions["api-server"] || ""; var old_id = configOptions["custom-rendezvous-server"] || ""; + var old_key = configOptions["key"] || ""; msgbox("custom-server", "ID/Relay Server", "
    \
    " + translate("ID Server") + ":
    \
    " + translate("Relay Server") + ":
    \ +
    " + translate("API Server") + ":
    \ +
    " + translate("Key") + ":
    \
    \ ", function(res=null) { if (!res) return; var id = (res.id || "").trim(); var relay = (res.relay || "").trim(); - if (id == old_id && relay == old_relay) return; + var api = (res.api || "").trim().toLowerCase(); + var key = (res.key || "").trim(); + if (id == old_id && relay == old_relay && key == old_key && api == old_api) return; if (id) { var err = handler.test_if_valid_server(id); if (err) return translate("ID Server") + ": " + err; @@ -266,10 +323,17 @@ class MyIdMenu: Reactor.Component { var err = handler.test_if_valid_server(relay); if (err) return translate("Relay Server") + ": " + err; } + if (api) { + if (0 != api.indexOf("https://") && 0 != api.indexOf("http://")) { + return translate("API Server") + ": " + translate("invalid_http"); + } + } configOptions["custom-rendezvous-server"] = id; configOptions["relay-server"] = relay; + configOptions["api-server"] = api; + configOptions["key"] = key; handler.set_options(configOptions); - }, 240); + }, 260); } else if (me.id == "socks5-server") { var socks5 = handler.get_socks() || {}; var old_proxy = socks5[0] || ""; @@ -292,10 +356,33 @@ class MyIdMenu: Reactor.Component { } handler.set_socks(proxy, username, password); }, 240); - } else if (me.id == "install-virtual-display") { - handler.install_virtual_display(); } else if (me.id == "stop-service") { handler.set_option("stop-service", service_stopped ? "" : "Y"); + } else if (me.id == "stop-rendezvous-service") { + handler.set_option("stop-rendezvous-service", rendezvous_service_stopped ? "" : "Y"); + } else if (me.id == "change-id") { + msgbox("custom-id", translate("Change ID"), "
    \ +
    " + translate('id_change_tip') + "
    \ +
    ID:
    \ +
    \ + ", function(res=null, show_progress) { + if (!res) return; + show_progress(); + var id = (res.id || "").trim(); + if (!id) return; + if (id == my_id) return; + handler.change_id(id); + function check_status() { + var status = handler.get_async_job_status(); + if (status == " ") self.timer(0.1s, check_status); + else { + if (status) show_progress(false, translate(status)); + else show_progress(-1); + } + } + check_status(); + return " "; + }); } else if (me.id == "about") { this.showAbout() } @@ -387,6 +474,7 @@ class App: Reactor.Component
    +
    ; } @@ -418,48 +506,28 @@ class InstallMe: Reactor.Component { } } -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; - } +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 - }; - -}(); + } + view.request(rqp); +} +// current running version is higher than installed class UpgradeMe: Reactor.Component { function render() { var update_or_download = is_osx ? "download" : "update"; @@ -509,7 +577,7 @@ class UpdateMe: Reactor.Component { el.content("Downloading %" + (loaded * 100 / total)); }; stdout.println("Downloading " + url + " to " + path); - http.download( + download( url, self.url(path), onsuccess, onerror, onprogress); @@ -778,7 +846,6 @@ event keydown (evt) { $(body).content(); function self.closing() { - // return false; // can prevent window close var (x, y, w, h) = view.box(#rectw, #border, #screen); handler.closing(x, y, w, h); return true; @@ -787,13 +854,19 @@ function self.closing() { function self.ready() { var r = handler.get_size(); if (isReasonableSize(r) && r[2] > 0) { - view.move(r[0], r[1], r[2], r[3]); + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (r[2] >= sw && r[3] >= sh) { + self.timer(1ms, function() { view.windowState = View.WINDOW_MAXIMIZED; }); + } else { + view.move(r[0], r[1], r[2], r[3]); + } } else { - centerize(800, 600); + centerize(scaleIt(800), scaleIt(600)); } if (!handler.get_remote_id()) { view.focus = $(#remote_id); } + refreshCurrentUser(); } function showAbout() { @@ -801,6 +874,7 @@ function showAbout() { } function showSettings() { + if ($(#overlay).style#display == 'block') return; myIdMenu.showSettingMenu(); } @@ -811,6 +885,16 @@ function checkConnectStatus() { service_stopped = tmp; app.update(); } + tmp = !!handler.get_option("stop-rendezvous-service"); + if (tmp != rendezvous_service_stopped) { + rendezvous_service_stopped = tmp; + myIdMenu.update(); + } + tmp = handler.using_public_server(); + if (tmp != using_public_server) { + using_public_server = tmp; + app.connect_status.update(); + } tmp = handler.get_connect_status(); if (tmp[0] != connect_status) { connect_status = tmp[0]; @@ -836,10 +920,126 @@ function checkConnectStatus() { } if (handler.recent_sessions_updated()) { stdout.println("recent sessions updated"); + updateAbPeer(); app.update(); } - checkConnectStatus(); - }); + check_if_overlay(); + checkConnectStatus(); + }); +} + +var enter = false; +function self.onMouse(evt) { + switch(evt.type) { + case Event.MOUSE_ENTER: + enter = true; + check_if_overlay(); + break; + case Event.MOUSE_LEAVE: + $(#overlay).style#display = 'none'; + enter = false; + break; + } +} + +function check_if_overlay() { + if (!handler.get_option('allow-remote-config-modification')) { + var time0 = getTime(); + handler.check_mouse_time(); + self.timer(120ms, function() { + if (!enter) return; + var d = time0 - handler.get_mouse_time(); + if (d < 120) $(#overlay).style#display = 'block'; + }); + } } checkConnectStatus(); + +function login() { + var name0 = getUserName(); + var pass0 = ''; + msgbox("custom-login", translate('Login'),
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    +
    , function(res=null, show_progress) { + if (!res) return; + show_progress(); + var name = (res.username || '').trim(); + if (!name) { + show_progress(false, translate("Username missed")); + return " "; + } + var pass = (res.password || '').trim(); + if (!pass) { + show_progress(false, translate("Password missed")); + return " "; + } + abLoading = true; + var url = handler.get_api_server(); + httpRequest(url + "/api/login", #post, {username: name, password: pass, id: my_id, uuid: handler.get_uuid()}, function(data) { + if (data.error) { + abLoading = false; + var err = translate(data.error); + show_progress(false, err); + return; + } + handler.set_local_option("access_token", data.access_token); + handler.set_local_option("user_info", JSON.stringify(data.user)); + show_progress(-1); + myIdMenu.update(); + getAb(); + }, function(err, status) { + abLoading = false; + err = translate(err); + if (url.indexOf('rustdesk') < 0) err = url + ', ' + err; + show_progress(false, err); + }); + return " "; + }); +} + +function reset_token() { + handler.set_local_option("access_token", ""); + handler.set_local_option("user_info", ""); + handler.set_local_option("selected-tags", ""); + myIdMenu.update(); + resetAb(); + if (abComponent) { + abComponent.update(); + } +} + +function logout() { + var url = handler.get_api_server(); + httpRequest(url + "/api/logout", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) { + }, function(err, status) { + msgbox("custom-error", translate('Error'), err); + }, getHttpHeaders()); + reset_token(); +} + +function refreshCurrentUser() { + if (!handler.get_local_option("access_token")) return; + abLoading = true; + abError = ""; + app.update(); + httpRequest(handler.get_api_server() + "/api/currentUser", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) { + if (data.error) { + handleAbError(data.error); + return; + } + handler.set_local_option("user_info", JSON.stringify(data)); + myIdMenu.update(); + getAb(); + }, function(err, status) { + if (status == 401 || status == 400) { + reset_token(); + } + handleAbError(err); + }, getHttpHeaders()); +} + +function getHttpHeaders() { + return "Authorization: Bearer " + handler.get_local_option("access_token"); +} diff --git a/src/ui/install.html b/src/ui/install.html index c86861ba0..bd9653e97 100644 --- a/src/ui/install.html +++ b/src/ui/install.html @@ -5,7 +5,7 @@ div.content { size: *; background: white; - padding:2em 10em; + padding:2em 8em; border-spacing: 1em; } input { diff --git a/src/ui/install.tis b/src/ui/install.tis index 59c104354..39301fd02 100644 --- a/src/ui/install.tis +++ b/src/ui/install.tis @@ -1,12 +1,16 @@ function self.ready() { - centerize(800, 600); + centerize(scaleIt(800), scaleIt(600)); } +var install_path = ""; + class Install: Reactor.Component { function render() { return
    {translate('Installation')}
    -
    {translate('Installation Path')} {": "}
    +
    {translate('Installation Path')} {": "} + +
    {translate('Create start menu shortcuts')}
    {translate('Create desktop icon')}
    {translate('End-user license agreement')}
    @@ -16,6 +20,9 @@ class Install: Reactor.Component { + {handler.show_run_without_install() && }
    ; } @@ -24,6 +31,21 @@ class Install: Reactor.Component { view.close(); } + event click $(#run-without-install) { + handler.run_without_install(); + } + + event click $(#path) { + install_path = view.selectFolder() || ""; + if (install_path) { + install_path = install_path.urlUnescape(); + install_path = install_path.replace("file://", "").replace("/", "\\"); + if (install_path[install_path.length - 1] != "\\") install_path += "\\"; + install_path += handler.get_app_name(); + $(#path_input).value = install_path; + } + } + event click $(#aggrement) { view.open_url("http://rustdesk.com/privacy"); } @@ -38,7 +60,7 @@ class Install: Reactor.Component { if ($(#desktopicon).value) { args += "desktopicon "; } - view.install_me(args); + view.install_me(args, install_path); } } diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 5662b1891..b19c9a039 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -25,6 +25,7 @@ class MsgboxComponent: Reactor.Component { this.remember = params.remember; this.callback = params.callback; this.hasRetry = params.hasRetry; + this.auto_login = params.auto_login; this.contentStyle = params.contentStyle; try { this.content = translate_text(this.content); } catch (e) {} } @@ -58,11 +59,18 @@ class MsgboxComponent: Reactor.Component { if (this.type == "input-password") { return this.getInputPasswordContent(); } + if (this.type == "custom-os-password") { + var ts = this.auto_login ? { checked: true } : {}; + return
    + +
    {translate('Auto Login')}
    +
    ; + } return this.content; } function getColor() { - if (this.type == "input-password") { + if (this.type == "input-password" || this.type == "custom-os-password") { return "#AD448E"; } if (this.type == "success") { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 08084e965..de9ca032c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -12,7 +12,7 @@ use clipboard::{ use enigo::{self, Enigo, KeyboardControllable}; use hbb_common::{ allow_err, - config::{self, Config, PeerConfig}, + config::{Config, LocalConfig, PeerConfig}, fs, log, message_proto::{permission_info::Permission, *}, protobuf::Message as _, @@ -88,6 +88,8 @@ impl Deref for Handler { } } +impl FileManager for Handler {} + impl sciter::EventHandler for Handler { fn get_subscription(&mut self) -> Option { Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) @@ -155,12 +157,15 @@ impl sciter::EventHandler for Handler { } sciter::dispatch_script_call! { + fn get_audit_server(); + fn send_note(String); fn is_xfce(); fn get_id(); fn get_default_pi(); fn get_option(String); fn t(String); fn set_option(String, String); + fn input_os_password(String, bool); fn save_close_state(String, String); fn is_file_transfer(); fn is_port_forward(); @@ -243,6 +248,8 @@ impl Handler { let mut me = self.clone(); let peer = self.peer_platform(); let is_win = peer == "Windows"; + #[cfg(windows)] + crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); std::thread::spawn(move || { // This will block. std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev @@ -276,6 +283,9 @@ impl Handler { #[cfg(not(windows))] let ctrl = get_key_state(enigo::Key::Control); let shift = get_key_state(enigo::Key::Shift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] let command = get_key_state(enigo::Key::Meta); let control_key = match key { Key::Alt => Some(ControlKey::Alt), @@ -530,6 +540,27 @@ impl Handler { crate::client::translate(name) } + fn get_audit_server(&self) -> String { + if self.lc.read().unwrap().conn_id <= 0 + || LocalConfig::get_option("access_token").is_empty() + { + return "".to_owned(); + } + crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ) + } + + fn send_note(&self, note: String) { + let url = self.get_audit_server(); + let id = self.id.clone(); + let conn_id = self.lc.read().unwrap().conn_id; + std::thread::spawn(move || { + send_note(url, id, conn_id, note); + }); + } + fn is_xfce(&self) -> bool { crate::platform::is_xfce() } @@ -659,6 +690,10 @@ impl Handler { self.lc.write().unwrap().set_option(k, v); } + fn input_os_password(&mut self, pass: String, activate: bool) { + input_os_password(pass, activate, self.clone()); + } + fn save_close_state(&self, k: String, v: String) { self.write().unwrap().close_state.insert(k, v); } @@ -671,38 +706,7 @@ impl Handler { } 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)); + crate::get_icon() } fn send_chat(&mut self, text: String) { @@ -727,45 +731,6 @@ impl Handler { 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" } @@ -859,13 +824,6 @@ impl Handler { 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))); } @@ -875,12 +833,16 @@ impl Handler { } fn enter(&mut self) { + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(true); unsafe { IS_IN = true; } } fn leave(&mut self) { + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(false); unsafe { IS_IN = false; } @@ -896,28 +858,17 @@ impl Handler { 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()); + #[allow(unused_mut)] + let mut command = command; + #[cfg(windows)] + { + if !command && crate::platform::windows::get_win_key_state() { + command = true; + } } - 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 + + send_mouse(mask, x, y, alt, ctrl, shift, command, self); + // on macos, ctrl + left button down = right button down, up won't 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") { @@ -1199,10 +1150,19 @@ async fn start_one_port_forward( remote_host: String, remote_port: i32, receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, ) { 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 + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + port, + handler.clone(), + receiver, + key, + token, + ) + .await { handler.on_error(&format!("Failed to listen on {}: {}", port, err)); } @@ -1213,9 +1173,28 @@ async fn start_one_port_forward( async fn io_loop(handler: Handler) { let (sender, mut receiver) = mpsc::unbounded_channel::(); handler.write().unwrap().sender = Some(sender.clone()); + let mut options = crate::ipc::get_options_async().await; + let mut key = options.remove("key").unwrap_or("".to_owned()); + let token = LocalConfig::get_option("access_token"); + if key.is_empty() { + key = crate::platform::get_license_key(); + } if handler.is_port_forward() { if handler.is_rdp() { - start_one_port_forward(handler, 0, "".to_owned(), 3389, receiver).await; + let port = handler + .get_option("rdp_port".to_owned()) + .parse::() + .unwrap_or(3389); + std::env::set_var( + "rdp_username", + handler.get_option("rdp_username".to_owned()), + ); + std::env::set_var( + "rdp_password", + handler.get_option("rdp_password".to_owned()), + ); + log::info!("Remote rdp port: {}", port); + start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; } else if handler.args.len() == 0 { let pfs = handler.lc.read().unwrap().port_forwards.clone(); let mut queues = HashMap::>::new(); @@ -1231,6 +1210,8 @@ async fn io_loop(handler: Handler) { let (sender, receiver) = mpsc::unbounded_channel::(); queues.insert(port, sender); let handler = handler.clone(); + let key = key.clone(); + let token = token.clone(); tokio::spawn(async move { start_one_port_forward( handler, @@ -1238,6 +1219,8 @@ async fn io_loop(handler: Handler) { remote_host, remote_port, receiver, + &key, + &token, ) .await; }); @@ -1268,7 +1251,16 @@ async fn io_loop(handler: Handler) { } 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; + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; } return; } @@ -1296,7 +1288,7 @@ async fn io_loop(handler: Handler) { #[cfg(windows)] clipboard_file_context: None, }; - remote.io_loop().await; + remote.io_loop(&key, &token).await; } struct RemoveJob { @@ -1339,7 +1331,7 @@ struct Remote { } impl Remote { - async fn io_loop(&mut self) { + async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); let conn_type = if self.handler.is_file_transfer() { @@ -1347,7 +1339,7 @@ impl Remote { } else { ConnType::default() }; - match Client::start(&self.handler.id, conn_type).await { + match Client::start(&self.handler.id, key, token, conn_type).await { Ok((mut peer, direct)) => { unsafe { SERVER_KEYBOARD_ENABLED = true; @@ -1934,7 +1926,7 @@ impl Remote { } } -fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { +pub 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); @@ -1963,6 +1955,12 @@ fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { #[async_trait] impl Interface for Handler { + fn send(&self, data: Data) { + if let Some(ref sender) = self.read().unwrap().sender { + sender.send(data).ok(); + } + } + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { let retry = check_if_retry(msgtype, title, text); self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); @@ -2019,6 +2017,10 @@ impl Interface for Handler { ); log::info!("[video] initialized: {:?}", ok); }); + let p = self.lc.read().unwrap().should_auto_login(); + if !p.is_empty() { + input_os_password(p, true, self.clone()); + } } self.lc.write().unwrap().handle_peer_info(username, pi); self.call("updatePi", &make_args!(pi_sciter)); @@ -2031,7 +2033,7 @@ impl Interface for Handler { { let mut path = std::env::temp_dir(); path.push(&self.id); - let path = path.with_extension(config::APP_NAME.to_lowercase()); + let path = path.with_extension(crate::get_app_name().to_lowercase()); std::fs::File::create(&path).ok(); if let Some(path) = path.to_str() { crate::platform::windows::add_recent_document(&path); @@ -2058,3 +2060,9 @@ impl Handler { self.msgbox("error", "Error", err); } } + +#[tokio::main(flavor = "current_thread")] +async fn send_note(url: String, id: String, conn_id: i32, note: String) { + let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); + allow_err!(crate::post_request(url, body.to_string(), "").await); +} diff --git a/src/ui/remote.tis b/src/ui/remote.tis index d1bcb86fb..0dae07664 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -22,7 +22,7 @@ handler.setDisplay = function(x, y, w, h) { } // in case toolbar not shown correclty -view.windowMinSize = (500, 300); +view.windowMinSize = (scaleIt(500), scaleIt(300)); function adaptDisplay() { var w = display_width; @@ -43,7 +43,7 @@ function adaptDisplay() { var (x, y) = view.box(#position, #border, #screen); // extra for border var extra = is_win ? 4 : 2; - view.move(x, y, w + extra, h + hh + extra); + view.move(x, y, (w + extra).toInteger(), (h + hh + extra).toInteger()); } } } @@ -67,8 +67,8 @@ function adaptDisplay() { } } handler.style.set { - width: w + "px", - height: h + "px", + width: w / scaleFactor + "px", + height: h / scaleFactor + "px", }; } @@ -389,8 +389,8 @@ handler.setCursorPosition = function(x, y) { cur_y = y - display_origin_y; var x = cur_x - cur_hotx; var y = cur_y - cur_hoty; - x *= display_scale; - y *= display_scale; + x *= display_scale / scaleFactor; + y *= display_scale / scaleFactor; cursor_img.style.set { left: x + "px", top: y + "px", @@ -401,13 +401,8 @@ handler.setCursorPosition = function(x, y) { } function self.ready() { - // https://sciter.com/forums/topic/focus_lost-and-focus_got-events/ - // not got a good way to detect focus/blur in Sciter - // below not work until I click on toolbar on Mac - self.on("focus", "*", function() { stdout.println(this,"got focus") }); - self.on("blur", "*", function() { stdout.println(this,"lost focus") }); - var w = 960; - var h = 640; + var w = scaleIt(960); + var h = scaleIt(640); if (is_file_transfer || is_port_forward) { var r = handler.get_size(); if (isReasonableSize(r) && r[2] > 0) { diff --git a/src/windows.cc b/src/windows.cc index 162c8a0a7..81df780a2 100644 --- a/src/windows.cc +++ b/src/windows.cc @@ -8,6 +8,7 @@ #include #include // NOLINT(build/include_order) #include +#include void flog(char const *fmt, ...) { @@ -66,9 +67,12 @@ BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL a return bResult; } -// START the app as system extern "C" { + bool is_windows_server() + { + return IsWindowsServer(); + } HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user) { HANDLE hProcess = NULL; @@ -89,7 +93,7 @@ extern "C" CreateEnvironmentBlock(&lpEnvironment, // Environment block hToken, // New token - TRUE); // Inheritance + TRUE); // Inheritence } if (lpEnvironment) { @@ -259,13 +263,16 @@ extern "C" auto n = 4 * 3; auto p = out - (width + 2) * 4 - 4; // Outline above... - if (p >= out0 && p + n <= out0_end) memset(p, 0xff, n); + if (p >= out0 && p + n <= out0_end) + memset(p, 0xff, n); // ...besides... p = out - 4; - if (p + n <= out0_end) memset(p, 0xff, n); + if (p + n <= out0_end) + memset(p, 0xff, n); // ...and above p = out + (width + 2) * 4 - 4; - if (p + n <= out0_end) memset(p, 0xff, n); + if (p + n <= out0_end) + memset(p, 0xff, n); } in += 4; out += 4; @@ -373,20 +380,210 @@ extern "C" SHAddToRecentDocs(SHARD_PATHW, path); } - uint32_t get_active_user(PWSTR bufin, uint32_t nin) - { - uint32_t nout = 0; - auto id = WTSGetActiveConsoleSessionId(); - PWSTR buf = NULL; - DWORD n = 0; - if (WTSQuerySessionInformationW(NULL, id, WTSUserName, &buf, &n)) - { - if (buf) { - nout = min(nin, n); - memcpy(bufin, buf, nout); - WTSFreeMemory(buf); - } - } - return nout; - } + DWORD get_current_session(BOOL include_rdp) + { + auto rdp_or_console = WTSGetActiveConsoleSessionId(); + if (!include_rdp) + return rdp_or_console; + PWTS_SESSION_INFOA pInfos; + DWORD count; + auto rdp = "rdp"; + auto nrdp = strlen(rdp); + if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count)) + { + for (DWORD i = 0; i < count; i++) + { + auto info = pInfos[i]; + if (info.State == WTSActive) + { + if (info.pWinStationName == NULL) + continue; + if (!stricmp(info.pWinStationName, "console")) + { + return info.SessionId; + } + if (!strnicmp(info.pWinStationName, rdp, nrdp)) + { + rdp_or_console = info.SessionId; + } + } + } + WTSFreeMemory(pInfos); + } + return rdp_or_console; + } + + uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp) + { + uint32_t nout = 0; + auto id = get_current_session(rdp); + PWSTR buf = NULL; + DWORD n = 0; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, id, WTSUserName, &buf, &n)) + { + if (buf) + { + nout = min(nin, n); + memcpy(bufin, buf, nout); + WTSFreeMemory(buf); + } + } + return nout; + } + + BOOL has_rdp_service() + { + PWTS_SESSION_INFOA pInfos; + DWORD count; + auto rdp = "rdp"; + auto nrdp = strlen(rdp); + auto rdp_or_console = WTSGetActiveConsoleSessionId(); + if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count)) + { + for (DWORD i = 0; i < count; i++) + { + auto info = pInfos[i]; + if (!strnicmp(info.pWinStationName, rdp, nrdp)) + { + return TRUE; + } + } + WTSFreeMemory(pInfos); + } + return FALSE; + } } // end of extern "C" + +// below copied from https://github.com/TigerVNC/tigervnc/blob/master/vncviewer/win32.c +extern "C" +{ + static HANDLE thread; + static DWORD thread_id; + + static HHOOK hook = 0; + static HWND target_wnd = 0; + static HWND default_hook_wnd = 0; + static bool win_down = false; + static bool stop_system_key_propagate = false; + + bool is_win_down() + { + return win_down; + } + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(*a)) + + static int is_system_hotkey(int vkCode, WPARAM wParam) + { + switch (vkCode) + { + case VK_LWIN: + case VK_RWIN: + win_down = wParam == WM_KEYDOWN; + case VK_SNAPSHOT: + return 1; + case VK_TAB: + if (GetAsyncKeyState(VK_MENU) & 0x8000) + return 1; + case VK_ESCAPE: + if (GetAsyncKeyState(VK_MENU) & 0x8000) + return 1; + if (GetAsyncKeyState(VK_CONTROL) & 0x8000) + return 1; + } + return 0; + } + + static LRESULT CALLBACK keyboard_hook(int nCode, WPARAM wParam, LPARAM lParam) + { + if (nCode >= 0) + { + KBDLLHOOKSTRUCT *msgInfo = (KBDLLHOOKSTRUCT *)lParam; + + // Grabbing everything seems to mess up some keyboard state that + // FLTK relies on, so just grab the keys that we normally cannot. + if (stop_system_key_propagate && is_system_hotkey(msgInfo->vkCode, wParam)) + { + PostMessage(target_wnd, wParam, msgInfo->vkCode, + (msgInfo->scanCode & 0xff) << 16 | + (msgInfo->flags & 0xff) << 24); + return 1; + } + } + + return CallNextHookEx(hook, nCode, wParam, lParam); + } + + static DWORD WINAPI keyboard_thread(LPVOID data) + { + MSG msg; + + target_wnd = (HWND)data; + + // Make sure a message queue is created + PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD); + + hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook, GetModuleHandle(0), 0); + // If something goes wrong then there is not much we can do. + // Just sit around and wait for WM_QUIT... + + while (GetMessage(&msg, NULL, 0, 0)) + ; + + if (hook) + UnhookWindowsHookEx(hook); + + target_wnd = 0; + + return 0; + } + + int win32_enable_lowlevel_keyboard(HWND hwnd) + { + if (!default_hook_wnd) + { + default_hook_wnd = hwnd; + } + if (!hwnd) + { + hwnd = default_hook_wnd; + } + // Only one target at a time for now + if (thread != NULL) + { + if (hwnd == target_wnd) + return 0; + + return 1; + } + + // We create a separate thread as it is crucial that hooks are processed + // in a timely manner. + thread = CreateThread(NULL, 0, keyboard_thread, hwnd, 0, &thread_id); + if (thread == NULL) + return 1; + + return 0; + } + + void win32_disable_lowlevel_keyboard(HWND hwnd) + { + if (!hwnd) + { + hwnd = default_hook_wnd; + } + if (hwnd != target_wnd) + return; + + PostThreadMessage(thread_id, WM_QUIT, 0, 0); + + CloseHandle(thread); + thread = NULL; + } + + void win_stop_system_key_propagate(bool v) + { + stop_system_key_propagate = v; + } + +} // end of extern "C" \ No newline at end of file