diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8e2b0af94..1745f9ba6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ github: [rustdesk] +ko_fi: rustdesk diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fbcf6eaf9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to RustDesk + +RustDesk welcomes contribution from everyone. Here are the guidelines if you are +thinking of helping us: + + +## Contributions + +Contributions to RustDesk or its dependencies should be made in the form of GitHub +pull requests. Each pull request will be reviewed by a core contributor +(someone with permission to land patches) and either landed in the main tree or +given feedback for changes that would be required. All contributions should +follow this format, even those from core contributors. + +Should you wish to work on an issue, please claim it first by commenting on +the GitHub issue that you want to work on it. This is to prevent duplicated +efforts from contributors on the same issue. + +## Pull Request Checklist + +- Branch from the master branch and, if needed, rebase to the current master + branch before submitting your pull request. If it doesn't merge cleanly with + master you may be asked to rebase your changes. + +- Commits should be as small as possible, while ensuring that each commit is + correct independently (i.e., each commit should compile and pass tests). + +- Commits should be accompanied by a Developer Certificate of Origin + (http://developercertificate.org) sign-off, which indicates that you (and + your employer if applicable) agree to be bound by the terms of the + [project license](LICENSE.md). In git, this is the `-s` option to `git commit` + +- If your patch is not getting reviewed or you need a specific person to review + it, you can @-reply a reviewer asking for a review in the pull request or a + comment, or you can ask for a review via [email](mailto:info@rustdesk.com). + +- Add tests relevant to the fixed bug or new feature. + +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). + +## Conduct + +We follow the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). + + +## Communication + +RustDesk contributors frequent the [Discord](https://discord.gg/nDceKgxnkV). + diff --git a/Cargo.lock b/Cargo.lock index 06a12f53b..069688c9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,10 +461,11 @@ dependencies = [ [[package]] name = "confy" version = "0.4.1" -source = "git+https://github.com/open-trade/confy#27fa12941291b44ccd856aef4a5452c1eb646047" dependencies = [ "directories", "serde 1.0.125", + "serde_derive", + "serde_yaml", "toml", ] @@ -1763,6 +1764,12 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.3.4" @@ -3196,6 +3203,18 @@ dependencies = [ "serde 1.0.125", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde 1.0.125", + "yaml-rust", +] + [[package]] name = "sha2" version = "0.9.3" @@ -4115,6 +4134,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.0" diff --git a/README.md b/README.md index f6224386e..f31a96d05 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # RustDesk | Your Remote Desktop Software -The best open-source remote desktop client software, written in Rust. Works out of the box, no configuration required. Great alternative to TeamViewer and AnyDesk! You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/blog/id-relay-set/), or write your own rendezvous/relay server. +Chat with us: [Discord](https://discord.gg/nDceKgxnkV) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +An open-source remote desktop client software, written in Rust. Works out of the box, no configuration required. Great alternative to TeamViewer and AnyDesk! You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/blog/id-relay-set/), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for help getting started. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -8,10 +14,9 @@ The best open-source remote desktop client software, written in Rust. Works out Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself. -[Windows](https://github.com/c-smile/sciter-sdk/blob/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.win/x64/sciter.dll) -[Linux](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.lnx/x64/libsciter-gtk.so) -[Osx](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.osx/sciter-osx-64.dylib) - +[Windows](https://github.com/c-smile/sciter-sdk/blob/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.win/x64/sciter.dll) | +[Linux](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://github.com/c-smile/sciter-sdk/raw/dc65744b66389cd5a0ff6bdb7c63a8b7b05a708b/bin.osx/sciter-osx-64.dylib) ## Raw steps to build * Prepare your Rust development env and C++ build env @@ -37,7 +42,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ``` -sudo pacman -Syu unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` ### Install vcpkg @@ -94,5 +99,3 @@ RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/e ![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) ![image](https://user-images.githubusercontent.com/71636191/113112990-65e2fd80-923c-11eb-840e-349b4d6e340d.png) - - diff --git a/libs/confy/.gitignore b/libs/confy/.gitignore new file mode 100644 index 000000000..55e830d9e --- /dev/null +++ b/libs/confy/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +/target +**/*.rs.bk +Cargo.lock diff --git a/libs/confy/Cargo.toml b/libs/confy/Cargo.toml new file mode 100644 index 000000000..749ab44a8 --- /dev/null +++ b/libs/confy/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "confy" +version = "0.4.1" +authors = ["Katharina Fey "] +description = "Boilerplate-free configuration management" +license = "MIT/X11 OR Apache-2.0" +documentation = "https://docs.rs/confy" +repository = "https://github.com/rust-clique/confy" +edition = "2018" + +[dependencies] +serde = "^1.0" +toml = { version = "^0.5", optional = true } +directories = "^2.0" +serde_yaml = { version = "0.8", optional = true } + +[features] +default = ["toml_conf"] +toml_conf = ["toml"] +yaml_conf = ["serde_yaml"] + +[[example]] +name = "simple" + +[dev-dependencies] +serde_derive = "^1.0" diff --git a/libs/confy/LICENSE b/libs/confy/LICENSE new file mode 100644 index 000000000..74c2a263c --- /dev/null +++ b/libs/confy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 rust-clique + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/confy/README.md b/libs/confy/README.md new file mode 100644 index 000000000..1b4cebb52 --- /dev/null +++ b/libs/confy/README.md @@ -0,0 +1,39 @@ +# confy + +Chat with us: [Discord](https://discord.gg/dwq4Zme) + +Zero-boilerplate configuration management. + +Focus on storing the right data, instead of worrying about how or where to store it. + +```rust +use serde_derive::{Serialize, Deserialize}; + +#[derive(Default, Debug, Serialize, Deserialize)] +struct MyConfig { + version: u8, + api_key: String, +} + +fn main() -> Result<(), ::std::io::Error> { + let cfg: MyConfig = confy::load("my-app-name")?; + dbg!(cfg); + Ok(()) +} +``` + +## Using yaml +Enabling the `yaml_conf` feature while disabling the default `toml_conf` +feature causes confy to use a YAML config file instead of TOML. + +``` +[dependencies.confy] +features = ["yaml_conf"] +default-features = false +``` + +## Breakings changes +Starting with version 0.4.0 the configuration file are stored in the expected place for your system. See the [`directories`] crates for more information. +Before version 0.4.0, the configuration file was written in the current directory. + +[`directories`]: https://crates.io/crates/directories diff --git a/libs/confy/examples/simple.rs b/libs/confy/examples/simple.rs new file mode 100644 index 000000000..f95bb6416 --- /dev/null +++ b/libs/confy/examples/simple.rs @@ -0,0 +1,29 @@ +//! The most simplest examples of how to use confy + +extern crate confy; + +#[macro_use] +extern crate serde_derive; + +#[derive(Debug, Serialize, Deserialize)] +struct ConfyConfig { + name: String, + comfy: bool, + foo: i64, +} + +impl Default for ConfyConfig { + fn default() -> Self { + ConfyConfig { + name: "Unknown".to_string(), + comfy: true, + foo: 42, + } + } +} + +fn main() -> Result<(), confy::ConfyError> { + let cfg: ConfyConfig = confy::load("confy_simple_app")?; + println!("{:#?}", cfg); + Ok(()) +} diff --git a/libs/confy/src/lib.rs b/libs/confy/src/lib.rs new file mode 100644 index 000000000..eaccaae9c --- /dev/null +++ b/libs/confy/src/lib.rs @@ -0,0 +1,300 @@ +//! Zero-boilerplate configuration management +//! +//! ## Why? +//! +//! There are a lot of different requirements when +//! selecting, loading and writing a config, +//! depending on the operating system and other +//! environment factors. +//! +//! In many applications this burden is left to you, +//! the developer of an application, to figure out +//! where to place the configuration files. +//! +//! This is where `confy` comes in. +//! +//! ## Idea +//! +//! `confy` takes care of figuring out operating system +//! specific and environment paths before reading and +//! writing a configuration. +//! +//! It gives you easy access to a configuration file +//! which is mirrored into a Rust `struct` via [serde]. +//! This way you only need to worry about the layout of +//! your configuration, not where and how to store it. +//! +//! [serde]: https://docs.rs/crates/serde +//! +//! `confy` uses the [`Default`] trait in Rust to automatically +//! create a new configuration, if none is available to read +//! from yet. +//! This means that you can simply assume your application +//! to have a configuration, which will be created with +//! default values of your choosing, without requiring +//! any special logic to handle creation. +//! +//! [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html +//! +//! ```rust,no_run +//! use serde_derive::{Serialize, Deserialize}; +//! +//! #[derive(Serialize, Deserialize)] +//! struct MyConfig { +//! version: u8, +//! api_key: String, +//! } +//! +//! /// `MyConfig` implements `Default` +//! impl ::std::default::Default for MyConfig { +//! fn default() -> Self { Self { version: 0, api_key: "".into() } } +//! } +//! +//! fn main() -> Result<(), confy::ConfyError> { +//! let cfg = confy::load("my-app-name")?; +//! Ok(()) +//! } +//! ``` +//! +//! Updating the configuration is then done via the [`store`] function. +//! +//! [`store`]: fn.store.html +//! + +mod utils; +use utils::*; + +use directories::ProjectDirs; +use serde::{de::DeserializeOwned, Serialize}; +use std::error::Error; +use std::fmt; +use std::fs::{self, File, OpenOptions}; +use std::io::{ErrorKind::NotFound, Write}; +use std::path::{Path, PathBuf}; + +#[cfg(not(any(feature = "toml_conf", feature = "yaml_conf")))] +compile_error!("Exactly one config language feature must be enabled to use \ +confy. Please enable one of either the `toml_conf` or `yaml_conf` \ +features."); + +#[cfg(all(feature = "toml_conf", feature = "yaml_conf"))] +compile_error!("Exactly one config language feature must be enabled to compile \ +confy. Please disable one of either the `toml_conf` or `yaml_conf` features. \ +NOTE: `toml_conf` is a default feature, so disabling it might mean switching off \ +default features for confy in your Cargo.toml"); + +#[cfg(all(feature = "toml_conf", not(feature = "yaml_conf")))] +const EXTENSION: &str = "toml"; + +#[cfg(feature = "yaml_conf")] +const EXTENSION: &str = "yml"; + +/// Load an application configuration from disk +/// +/// A new configuration file is created with default values if none +/// exists. +/// +/// Errors that are returned from this function are I/O related, +/// for example if the writing of the new configuration fails +/// or `confy` encounters an operating system or environment +/// that it does not support. +/// +/// **Note:** The type of configuration needs to be declared in some way +/// that is inferrable by the compiler. Also note that your +/// configuration needs to implement `Default`. +/// +/// ```rust,no_run +/// # use confy::ConfyError; +/// # use serde_derive::{Serialize, Deserialize}; +/// # fn main() -> Result<(), ConfyError> { +/// #[derive(Default, Serialize, Deserialize)] +/// struct MyConfig {} +/// +/// let cfg: MyConfig = confy::load("my-app-name")?; +/// # Ok(()) +/// # } +/// ``` +pub fn load(name: &str) -> Result { + let project = ProjectDirs::from("rs", "", name).ok_or(ConfyError::BadConfigDirectoryStr)?; + + let config_dir_str = get_configuration_directory_str(&project)?; + + let path: PathBuf = [config_dir_str, &format!("{}.{}", name, EXTENSION)].iter().collect(); + + load_path(path) +} + +/// Load an application configuration from a specified path. +/// +/// This is an alternate version of [`load`] that allows the specification of +/// an aritrary path instead of a system one. For more information on errors +/// and behavior, see [`load`]'s documentation. +/// +/// [`load`]: fn.load.html +pub fn load_path(path: impl AsRef) -> Result { + match File::open(&path) { + Ok(mut cfg) => { + let cfg_string = cfg + .get_string() + .map_err(ConfyError::ReadConfigurationFileError)?; + + #[cfg(feature = "toml_conf")] { + let cfg_data = toml::from_str(&cfg_string); + cfg_data.map_err(ConfyError::BadTomlData) + } + #[cfg(feature = "yaml_conf")] { + let cfg_data = serde_yaml::from_str(&cfg_string); + cfg_data.map_err(ConfyError::BadYamlData) + } + + } + Err(ref e) if e.kind() == NotFound => { + if let Some(parent) = path.as_ref().parent() { + fs::create_dir_all(parent) + .map_err(ConfyError::DirectoryCreationFailed)?; + } + store_path(path, T::default())?; + Ok(T::default()) + } + Err(e) => Err(ConfyError::GeneralLoadError(e)), + } +} + +/// The errors the confy crate can encounter. +#[derive(Debug)] +pub enum ConfyError { + #[cfg(feature = "toml_conf")] + BadTomlData(toml::de::Error), + + #[cfg(feature = "yaml_conf")] + BadYamlData(serde_yaml::Error), + + DirectoryCreationFailed(std::io::Error), + GeneralLoadError(std::io::Error), + BadConfigDirectoryStr, + + #[cfg(feature = "toml_conf")] + SerializeTomlError(toml::ser::Error), + + #[cfg(feature = "yaml_conf")] + SerializeYamlError(serde_yaml::Error), + + WriteConfigurationFileError(std::io::Error), + ReadConfigurationFileError(std::io::Error), + OpenConfigurationFileError(std::io::Error), +} + +impl fmt::Display for ConfyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + + #[cfg(feature = "toml_conf")] + ConfyError::BadTomlData(e) => write!(f, "Bad TOML data: {}", e), + #[cfg(feature = "toml_conf")] + ConfyError::SerializeTomlError(_) => write!(f, "Failed to serialize configuration data into TOML."), + + #[cfg(feature = "yaml_conf")] + ConfyError::BadYamlData(e) => write!(f, "Bad YAML data: {}", e), + #[cfg(feature = "yaml_conf")] + ConfyError::SerializeYamlError(_) => write!(f, "Failed to serialize configuration data into YAML."), + + ConfyError::DirectoryCreationFailed(e) => write!(f, "Failed to create directory: {}", e), + ConfyError::GeneralLoadError(_) => write!(f, "Failed to load configuration file."), + ConfyError::BadConfigDirectoryStr => write!(f, "Failed to convert directory name to str."), + ConfyError::WriteConfigurationFileError(_) => write!(f, "Failed to write configuration file."), + ConfyError::ReadConfigurationFileError(_) => write!(f, "Failed to read configuration file."), + ConfyError::OpenConfigurationFileError(_) => write!(f, "Failed to open configuration file."), + } + } +} + +impl Error for ConfyError {} + +/// Save changes made to a configuration object +/// +/// This function will update a configuration, +/// with the provided values, and create a new one, +/// if none exists. +/// +/// You can also use this function to create a new configuration +/// with different initial values than which are provided +/// by your `Default` trait implementation, or if your +/// configuration structure _can't_ implement `Default`. +/// +/// ```rust,no_run +/// # use serde_derive::{Serialize, Deserialize}; +/// # use confy::ConfyError; +/// # fn main() -> Result<(), ConfyError> { +/// #[derive(Serialize, Deserialize)] +/// struct MyConf {} +/// +/// let my_cfg = MyConf {}; +/// confy::store("my-app-name", my_cfg)?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Errors returned are I/O errors related to not being +/// able to write the configuration file or if `confy` +/// encounters an operating system or environment it does +/// not support. +pub fn store(name: &str, cfg: T) -> Result<(), ConfyError> { + let project = ProjectDirs::from("rs", "", name).ok_or(ConfyError::BadConfigDirectoryStr)?; + fs::create_dir_all(project.config_dir()).map_err(ConfyError::DirectoryCreationFailed)?; + + let config_dir_str = get_configuration_directory_str(&project)?; + + let path: PathBuf = [config_dir_str, &format!("{}.{}", name, EXTENSION)].iter().collect(); + + store_path(path, cfg) +} + +/// Save changes made to a configuration object at a specified path +/// +/// This is an alternate version of [`store`] that allows the specification of +/// an aritrary path instead of a system one. For more information on errors +/// and behavior, see [`store`]'s documentation. +/// +/// [`store`]: fn.store.html +pub fn store_path(path: impl AsRef, cfg: T) -> Result<(), ConfyError> { + let path = path.as_ref(); + let mut path_tmp = path.to_path_buf(); + use std::time::{SystemTime, UNIX_EPOCH}; + let mut i = 0; + loop { + i += 1; + path_tmp.set_extension(SystemTime::now().duration_since(UNIX_EPOCH).map(|x| x.as_nanos()).unwrap_or(i).to_string()); + if !path_tmp.exists() { + break; + } + } + let mut f = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&path_tmp) + .map_err(ConfyError::OpenConfigurationFileError)?; + + let s; + #[cfg(feature = "toml_conf")] { + s = toml::to_string(&cfg).map_err(ConfyError::SerializeTomlError)?; + } + #[cfg(feature = "yaml_conf")] { + s = serde_yaml::to_string(&cfg).map_err(ConfyError::SerializeYamlError)?; + } + + f.write_all(s.as_bytes()) + .map_err(ConfyError::WriteConfigurationFileError)?; + std::fs::rename(path_tmp, path) + .map_err(ConfyError::WriteConfigurationFileError)?; + Ok(()) +} + +fn get_configuration_directory_str(project: &ProjectDirs) -> Result<&str, ConfyError> { + let config_dir_option = project.config_dir().to_str(); + + match config_dir_option { + Some(x) => Ok(x), + None => Err(ConfyError::BadConfigDirectoryStr), + } +} diff --git a/libs/confy/src/utils.rs b/libs/confy/src/utils.rs new file mode 100644 index 000000000..d2d4263e7 --- /dev/null +++ b/libs/confy/src/utils.rs @@ -0,0 +1,16 @@ +//! Some storage utilities + +use std::fs::File; +use std::io::{Error as IoError, Read}; + +pub trait CheckedStringRead { + fn get_string(&mut self) -> Result; +} + +impl CheckedStringRead for File { + fn get_string(&mut self) -> Result { + let mut s = String::new(); + self.read_to_string(&mut s)?; + Ok(s) + } +} diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index bf289c8ed..e5a19750c 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -347,7 +347,7 @@ pub enum Key { /// Clear, /// - Menu, + Menu, // deprecated, use alt instead /// Pause, /// @@ -409,6 +409,12 @@ pub enum Key { /// NumpadEnter, /// + RightShift, + /// + RightControl, + /// + RightAlt, + /// /// Function, /// mac /// keyboard layout dependent key Layout(char), @@ -485,7 +491,7 @@ impl Enigo { /// ``` pub fn new() -> Self { #[cfg(any(target_os = "android", target_os = "ios"))] - return Enigo{}; + return Enigo {}; #[cfg(not(any(target_os = "android", target_os = "ios")))] Self::default() } diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index 0e8078b10..58f134a18 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -260,7 +260,7 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> { Key::Mute => "", Key::Scroll => "Scroll_Lock", Key::NumLock => "Num_Lock", - Key::RWin => "", + Key::RWin => "Super_R", Key::Apps => "", Key::Multiply => "KP_Multiply", Key::Add => "KP_Add", @@ -268,6 +268,9 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> { Key::Divide => "KP_Divide", Key::Equals => "KP_Equal", Key::NumpadEnter => "KP_Enter", + Key::RightShift => "Shift_R", + Key::RightControl => "Control_R", + Key::RightAlt => "Alt_R", Key::Command | Key::Super | Key::Windows | Key::Meta => "Super", diff --git a/libs/enigo/src/macos/keycodes.rs b/libs/enigo/src/macos/keycodes.rs index e946789b1..8dcd2191b 100644 --- a/libs/enigo/src/macos/keycodes.rs +++ b/libs/enigo/src/macos/keycodes.rs @@ -70,3 +70,4 @@ pub const kVK_ANSI_KeypadDivide: u16 = 0x4B; pub const kVK_ANSI_KeypadEnter: u16 = 0x4C; pub const kVK_ANSI_KeypadMinus: u16 = 0x4E; pub const kVK_ANSI_KeypadEquals: u16 = 0x51; +pub const kVK_RIGHT_COMMAND: u16 = 0x36; diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 746526dcd..bb4875c08 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -142,6 +142,8 @@ pub const kCFStringEncodingUTF8: u32 = 134_217_984; #[link(name = "Carbon", kind = "framework")] extern "C" { fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; + fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + fn TISCopyCurrentASCIICapableKeyboardLayoutInputSource() -> TISInputSourceRef; // extern void * // TISGetInputSourceProperty( @@ -583,6 +585,10 @@ impl Enigo { Key::Subtract => kVK_ANSI_KeypadMinus, Key::Equals => kVK_ANSI_KeypadEquals, Key::NumLock => kVK_ANSI_KeypadClear, + Key::RWin => kVK_RIGHT_COMMAND, + Key::RightShift => kVK_RightShift, + Key::RightControl => kVK_RightControl, + Key::RightAlt => kVK_RightOption, Key::Raw(raw_keycode) => raw_keycode, Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()), @@ -600,6 +606,7 @@ impl Enigo { } fn init_map(&mut self) { + println!("init_map"); self.keycode_to_string_map.insert("".to_owned(), 0); // loop through every keycode (0 - 127) for keycode in 0..128 { @@ -649,14 +656,44 @@ impl Enigo { fn create_string_for_key(&self, keycode: u16, modifier: u32) -> CFStringRef { let current_keyboard = unsafe { TISCopyCurrentKeyboardInputSource() }; - let layout_data = unsafe { - TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData) - }; + let mut layout_data = std::ptr::null_mut(); + if !current_keyboard.is_null() { + layout_data = unsafe { + TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData) + }; + } + if layout_data.is_null() { + // https://github.com/microsoft/vscode/issues/23833 + let current_keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() }; + if !current_keyboard.is_null() { + layout_data = unsafe { + TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData) + }; + } + } + if layout_data.is_null() { + let current_keyboard = unsafe { TISCopyCurrentASCIICapableKeyboardLayoutInputSource() }; + if !current_keyboard.is_null() { + layout_data = unsafe { + TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData) + }; + } + } + if layout_data.is_null() { + // to-do: try out manual mapping in https://github.com/stweil/OSXvnc + // we do see crash like this, also not easy to reproduce, no sure if it related +/* +0 rustdesk 0x000000010f921bc9 std::collections::hash::map::HashMap$LT$K$C$V$C$S$GT$::insert::h84e28c51a3292e7a + 473 +1 rustdesk 0x000000010f921884 enigo::macos::macos_impl::Enigo::key_to_keycode::h85ead82e9b1075ae + 1428 +2 rustdesk 0x000000010f922a8c _$LT$enigo..macos..macos_impl..Enigo$u20$as$u20$enigo..KeyboardControllable$GT$::key_down::h54f24da6d274b948 + 44 +*/ + return std::ptr::null() as _; + } let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) }; let mut keys_down: UInt32 = 0; - // let mut chars: *mut c_void;//[UniChar; 4]; let mut chars: u16 = 0; + // let mut chars: *mut c_void;//[UniChar; 4]; let mut real_length: UniCharCount = 0; unsafe { UCKeyTranslate( diff --git a/libs/enigo/src/win/keycodes.rs b/libs/enigo/src/win/keycodes.rs index 2dc99275a..58122cb83 100644 --- a/libs/enigo/src/win/keycodes.rs +++ b/libs/enigo/src/win/keycodes.rs @@ -7,9 +7,14 @@ pub const EVK_BACK: u16 = 0x08; pub const EVK_ESCAPE: u16 = 0x1b; pub const EVK_LWIN: u16 = 0x5b; pub const EVK_SHIFT: u16 = 0x10; +//pub const EVK_LSHIFT: u16 = 0xa0; +pub const EVK_RSHIFT: u16 = 0xa1; +//pub const EVK_LMENU: u16 = 0xa4; +pub const EVK_RMENU: u16 = 0xa5; pub const EVK_CAPITAL: u16 = 0x14; pub const EVK_MENU: u16 = 0x12; pub const EVK_LCONTROL: u16 = 0xa2; +pub const EVK_RCONTROL: u16 = 0xa3; pub const EVK_HOME: u16 = 0x24; pub const EVK_PRIOR: u16 = 0x21; pub const EVK_NEXT: u16 = 0x22; diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index df636e978..73917cde4 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -338,10 +338,14 @@ impl Enigo { Key::Divide => EVK_DIVIDE, Key::NumpadEnter => EVK_RETURN, Key::Equals => '=' as _, + Key::RightShift => EVK_RSHIFT, + Key::RightControl => EVK_RCONTROL, + Key::RightAlt => EVK_RMENU, Key::Raw(raw_keycode) => raw_keycode, Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()), Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN, + _ => 0, } } diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 6dc5319c8..5faff841a 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -24,7 +24,7 @@ rand = "0.7" serde_derive = "1.0" serde = "1.0" lazy_static = "1.4" -confy = { git = "https://github.com/open-trade/confy" } +confy = { path = "../confy" } dirs-next = "2.0" filetime = "0.2" sodiumoxide = "0.2" diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 6a2a03299..a8e8d34b5 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -129,7 +129,7 @@ enum ControlKey { Numpad9 = 42; Cancel = 43; Clear = 44; - Menu = 45; + Menu = 45; // deprecated, use Alt instead Pause = 46; Kana = 47; Hangul = 48; @@ -157,6 +157,9 @@ enum ControlKey { Divide = 70; Equals = 71; NumpadEnter = 72; + RShift= 73; + RControl = 74; + RAlt = 75; CtrlAltDel = 100; LockScreen = 101; } diff --git a/src/client.rs b/src/client.rs index d2fea14c3..e62eb19c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -135,7 +135,7 @@ impl Client { if ph.socket_addr.is_empty() { match ph.failure.enum_value_or_default() { punch_hole_response::Failure::ID_NOT_EXIST => { - bail!("ID not exist"); + bail!("ID does not exist"); } punch_hole_response::Failure::OFFLINE => { bail!("Remote desktop is offline"); @@ -318,6 +318,7 @@ impl Client { } } else { // fall back to non-secure connection in case pk mismatch + // to-do: pop up a warning dialog to let user choose if continue let mut msg_out = Message::new(); msg_out.set_public_key(PublicKey::new()); timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; @@ -1068,7 +1069,7 @@ lazy_static::lazy_static! { ("VK_RETURN", Key::ControlKey(ControlKey::Return)), ("VK_SHIFT", Key::ControlKey(ControlKey::Shift)), ("VK_CONTROL", Key::ControlKey(ControlKey::Control)), - ("VK_MENU", Key::ControlKey(ControlKey::Menu)), + ("VK_MENU", Key::ControlKey(ControlKey::Alt)), ("VK_PAUSE", Key::ControlKey(ControlKey::Pause)), ("VK_CAPITAL", Key::ControlKey(ControlKey::CapsLock)), ("VK_KANA", Key::ControlKey(ControlKey::Kana)), @@ -1107,6 +1108,10 @@ lazy_static::lazy_static! { ("VK_NUMPAD7", Key::ControlKey(ControlKey::Numpad7)), ("VK_NUMPAD8", Key::ControlKey(ControlKey::Numpad8)), ("VK_NUMPAD9", Key::ControlKey(ControlKey::Numpad9)), + ("RAlt", Key::ControlKey(ControlKey::RAlt)), + ("RWin", Key::ControlKey(ControlKey::RWin)), + ("RControl", Key::ControlKey(ControlKey::RControl)), + ("RShift", Key::ControlKey(ControlKey::RShift)), ("CTRL_ALT_DEL", Key::ControlKey(ControlKey::CtrlAltDel)), ("LOCK_SCREEN", Key::ControlKey(ControlKey::LockScreen)), ].iter().cloned().collect(); diff --git a/src/common.rs b/src/common.rs index 87e0b98ff..bed64ecaf 100644 --- a/src/common.rs +++ b/src/common.rs @@ -50,6 +50,20 @@ pub fn valid_for_capslock(evt: &KeyEvent) -> bool { } } +pub fn create_clipboard_msg(content: String) -> Message { + let bytes = content.into_bytes(); + let compressed = compress_func(&bytes, COMPRESS_LEVEL); + let compress = compressed.len() < bytes.len(); + let content = if compress { compressed } else { bytes }; + let mut msg = Message::new(); + msg.set_clipboard(Clipboard { + compress, + content, + ..Default::default() + }); + msg +} + pub fn check_clipboard( ctx: &mut ClipboardContext, old: Option<&Arc>>, @@ -61,18 +75,8 @@ pub fn check_clipboard( let changed = content != *old.lock().unwrap(); if changed { log::info!("{} update found on {}", CLIPBOARD_NAME, side); - let bytes = content.clone().into_bytes(); - *old.lock().unwrap() = content; - let compressed = compress_func(&bytes, COMPRESS_LEVEL); - let compress = compressed.len() < bytes.len(); - let content = if compress { compressed } else { bytes }; - let mut msg = Message::new(); - msg.set_clipboard(Clipboard { - compress, - content, - ..Default::default() - }); - return Some(msg); + *old.lock().unwrap() = content.clone(); + return Some(create_clipboard_msg(content)); } } } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 15c619378..9fbddb826 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -164,7 +164,7 @@ pub fn start_os_service() { uid = tmp; log::info!("uid of seat0: {}", uid); let gdm = format!("/run/user/{}/gdm/Xauthority", uid); - let mut auth = get_env("XAUTHORITY", &uid); + let mut auth = get_env_tries("XAUTHORITY", &uid, 10); if auth.is_empty() { auth = if std::path::Path::new(&gdm).exists() { gdm @@ -187,8 +187,9 @@ pub fn start_os_service() { d = get_display(); } if d.is_empty() { - d = ":0".to_owned() + d = ":0".to_owned(); } + d = d.replace(&whoami::hostname(), "").replace("localhost", ""); log::info!("DISPLAY: {}", d); log::info!("XAUTHORITY: {}", auth); std::env::set_var("XAUTHORITY", auth); @@ -289,6 +290,7 @@ fn get_display() -> String { } // above not work for gdm user log::debug!("ls -l /tmp/.X11-unix/"); + let mut last = "".to_owned(); if let Ok(output) = std::process::Command::new("ls") .args(vec!["-l", "/tmp/.X11-unix/"]) .output() @@ -296,16 +298,18 @@ fn get_display() -> String { for line in String::from_utf8_lossy(&output.stdout).lines() { log::debug!(" {}", line); let mut iter = line.split_whitespace(); - if iter.nth(2) == Some(&user) { - if let Some(x) = iter.last() { - if x.starts_with("X") { - return x.replace("X", ":").to_owned(); + let user_field = iter.nth(2); + if let Some(x) = iter.last() { + if x.starts_with("X") { + last = x.replace("X", ":").to_owned(); + if user_field == Some(&user) { + return last; } } } } } - "".to_owned() + last } fn get_value_of_seat0(i: usize) -> String { @@ -322,13 +326,32 @@ fn get_value_of_seat0(i: usize) -> String { } } } + + // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 + if let Ok(output) = std::process::Command::new("loginctl").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(sid) = line.split_whitespace().nth(0) { + let d = get_display_server_of_session(sid); + if is_active(sid) && d != "tty" { + if let Some(uid) = line.split_whitespace().nth(i) { + return uid.to_owned(); + } + } + } + } + } + return "".to_owned(); } pub fn get_display_server() -> String { let session = get_value_of_seat0(0); + get_display_server_of_session(&session) +} + +fn get_display_server_of_session(session: &str) -> String { if let Ok(output) = std::process::Command::new("loginctl") - .args(vec!["show-session", "-p", "Type", &session]) + .args(vec!["show-session", "-p", "Type", session]) .output() { String::from_utf8_lossy(&output.stdout) @@ -343,18 +366,24 @@ pub fn get_display_server() -> String { pub fn is_login_wayland() -> bool { if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { contents.contains("#WaylandEnable=false") + } else if let Ok(contents) = std::fs::read_to_string("/etc/gdm/custom.conf") { + contents.contains("#WaylandEnable=false") } else { false } } pub fn fix_login_wayland() { + let mut file = "/etc/gdm3/custom.conf".to_owned(); + if !std::path::Path::new(&file).exists() { + file = "/etc/gdm/custom.conf".to_owned(); + } match std::process::Command::new("pkexec") .args(vec![ "sed", "-i", "s/#WaylandEnable=false/WaylandEnable=false/g", - "/etc/gdm3/custom.conf", + &file ]) .output() { @@ -495,6 +524,17 @@ fn run_cmds(cmds: String) -> ResultType> { } } +fn get_env_tries(name: &str, uid: &str, n: usize) -> String { + for _ in 0..n { + let x = get_env(name, uid); + if !x.is_empty() { + return x; + } + std::thread::sleep(std::time::Duration::from_millis(300)); + } + "".to_owned() +} + fn get_env(name: &str, uid: &str) -> String { let cmd = format!("ps -u {} -o pid= | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep -m1 '^{}=' | sed 's/{}=//g'", uid, name, name); log::debug!("Run: {}", &cmd); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 0d64a8238..043c73321 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1037,3 +1037,33 @@ pub fn get_installed_version() -> String { } "".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(()) +} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 98c1a2642..cda13250c 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -109,46 +109,48 @@ impl RendezvousMediator { let mut old_latency = 0; let mut ema_latency = 0; loop { + let mut update_latency = || { + last_register_resp = SystemTime::now(); + let mut latency = last_register_resp + .duration_since(last_register_sent) + .map(|d| d.as_micros() as i64) + .unwrap_or(0); + if ema_latency == 0 { + ema_latency = latency; + } else { + ema_latency = latency / 30 + (ema_latency * 29 / 30); + latency = ema_latency; + } + let mut n = latency / 5; + if n < 3000 { + n = 3000; + } + if (latency - old_latency).abs() > n || old_latency <= 0 { + Config::update_latency(&host, latency); + log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); + old_latency = latency; + } + fails = 0; + }; select! { Some(Ok((bytes, _))) = socket.next() => { if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { match msg_in.union { Some(rendezvous_message::Union::register_peer_response(rpr)) => { + update_latency(); if rpr.request_pk { log::info!("request_pk received from {}", host); allow_err!(rz.register_pk(&mut socket).await); continue; } - last_register_resp = SystemTime::now(); - let mut latency = last_register_resp.duration_since(last_register_sent).map(|d| d.as_micros() as i64).unwrap_or(0); - if ema_latency == 0 { - ema_latency = latency; - } else { - ema_latency = latency / 30 + (ema_latency * 29 / 30); - latency = ema_latency; - } - let mut n = latency / 5; - if n < 3000 { - n = 3000; - } - if (latency - old_latency).abs() > n || old_latency <= 0 { - Config::update_latency(&host, latency); - log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); - old_latency = latency; - } - fails = 0; } Some(rendezvous_message::Union::register_pk_response(rpr)) => { + update_latency(); match rpr.result.enum_value_or_default() { register_pk_response::Result::OK => { Config::set_key_confirmed(true); Config::set_host_key_confirmed(&rz.host_prefix, true); *SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned(); - last_register_resp = SystemTime::now(); - let latency = last_register_resp.duration_since(last_register_sent).map(|d| d.as_micros() as i64).unwrap_or(0); - Config::update_latency(&host, latency); - log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); - fails = 0; } register_pk_response::Result::UUID_MISMATCH => { allow_err!(rz.handle_uuid_mismatch(&mut socket).await); diff --git a/src/server.rs b/src/server.rs index 8f13901b1..359c3378b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -121,7 +121,10 @@ async fn create_tcp_connection_( 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() { + } else if pk.asymmetric_value.is_empty() { + // force a trial to update_pk to rendezvous server + Config::set_key_confirmed(false); + } else { bail!("Handshake failed: invalid public sign key length from peer"); } } else { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 5393d1f7d..25597d11c 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -6,7 +6,6 @@ pub use crate::common::{ struct State { ctx: Option, - initialized: bool, } impl Default for State { @@ -18,22 +17,18 @@ impl Default for State { None } }; - Self { - ctx, - initialized: false, - } + Self { ctx } } } impl super::service::Reset for State { fn reset(&mut self) { *CONTENT.lock().unwrap() = Default::default(); - self.initialized = false; } } pub fn new() -> GenericService { - let sp = GenericService::new(NAME, false); + let sp = GenericService::new(NAME, true); sp.repeat::(INTERVAL, run); sp } @@ -41,13 +36,16 @@ pub fn new() -> GenericService { fn run(sp: GenericService, state: &mut State) -> ResultType<()> { if let Some(ctx) = state.ctx.as_mut() { if let Some(msg) = check_clipboard(ctx, None) { - if !state.initialized { - state.initialized = true; - // ignore clipboard update before service start - return Ok(()); - } sp.send(msg); } + 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)); + } + Ok(()) + })?; } Ok(()) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 363164ee8..879a7dea3 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -174,7 +174,6 @@ pub fn is_left_up(evt: &MouseEvent) -> bool { #[cfg(windows)] pub fn mouse_move_relative(x: i32, y: i32) { - #[cfg(windows)] crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); en.mouse_move_relative(x, y); @@ -187,6 +186,19 @@ fn modifier_sleep() { std::thread::sleep(std::time::Duration::from_nanos(1)); } +#[cfg(not(target_os = "macos"))] +#[inline] +fn get_modifier_state(key: enigo::Key, en: &mut Enigo) -> bool { + let x = en.get_key_state(key.clone()); + match key { + enigo::Key::Shift => x || en.get_key_state(enigo::Key::RightShift), + enigo::Key::Control => x || en.get_key_state(enigo::Key::RightControl), + enigo::Key::Alt => x || en.get_key_state(enigo::Key::RightAlt), + enigo::Key::Meta => x || en.get_key_state(enigo::Key::RWin), + _ => x, + } +} + pub fn handle_mouse(evt: &MouseEvent, conn: i32) { #[cfg(target_os = "macos")] if !*IS_SERVER { @@ -221,7 +233,7 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) { en.add_flag(key); #[cfg(not(target_os = "macos"))] if key != &enigo::Key::CapsLock && key != &enigo::Key::NumLock { - if !en.get_key_state(key.clone()) { + if !get_modifier_state(key.clone(), &mut en) { en.key_down(key.clone()).ok(); modifier_sleep(); to_release.push(key); @@ -340,7 +352,7 @@ lazy_static::lazy_static! { (ControlKey::Numpad9, enigo::Key::Numpad9), (ControlKey::Cancel, enigo::Key::Cancel), (ControlKey::Clear, enigo::Key::Clear), - (ControlKey::Menu, enigo::Key::Menu), + (ControlKey::Menu, enigo::Key::Alt), (ControlKey::Pause, enigo::Key::Pause), (ControlKey::Kana, enigo::Key::Kana), (ControlKey::Hangul, enigo::Key::Hangul), @@ -368,6 +380,10 @@ lazy_static::lazy_static! { (ControlKey::Divide, enigo::Key::Divide), (ControlKey::Equals, enigo::Key::Equals), (ControlKey::NumpadEnter, enigo::Key::NumpadEnter), + (ControlKey::RAlt, enigo::Key::RightAlt), + (ControlKey::RWin, enigo::Key::RWin), + (ControlKey::RControl, enigo::Key::RightControl), + (ControlKey::RShift, enigo::Key::RightShift), ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); static ref NUMPAD_KEY_MAP: HashMap = [ @@ -425,7 +441,7 @@ fn handle_key_(evt: &KeyEvent) { has_numlock = true; } } else { - if !en.get_key_state(key.clone()) { + if !get_modifier_state(key.clone(), &mut en) { en.key_down(key.clone()).ok(); modifier_sleep(); to_release.push(key); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 5c466683e..ddf497b9a 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -46,11 +46,11 @@ pub fn new() -> GenericService { fn run(sp: GenericService) -> ResultType<()> { let fps = 30; let spf = time::Duration::from_secs_f32(1. / (fps as f32)); - let (n, current, display) = get_current_display()?; + let (ndisplay, current, display) = get_current_display()?; let (origin, width, height) = (display.origin(), display.width(), display.height()); log::debug!( "#displays={}, current={}, origin: {:?}, width={}, height={}", - n, + ndisplay, current, &origin, width, @@ -74,7 +74,11 @@ fn run(sp: GenericService) -> ResultType<()> { speed, }; let mut vpx; - match Encoder::new(&cfg, 1) { + let mut n = ((width * height) as f64 / (1920 * 1080) as f64).round() as u32; + if n < 1 { + n = 1; + } + match Encoder::new(&cfg, n) { Ok(x) => vpx = x, Err(err) => bail!("Failed to create encoder: {}", err), } @@ -113,6 +117,7 @@ fn run(sp: GenericService) -> ResultType<()> { let start = time::Instant::now(); let mut crc = (0, 0); let mut last_sent = time::Instant::now(); + let mut last_check_displays = time::Instant::now(); while sp.ok() { if *SWITCH.lock().unwrap() { bail!("SWITCH"); @@ -131,6 +136,14 @@ fn run(sp: GenericService) -> ResultType<()> { } } let now = time::Instant::now(); + if last_check_displays.elapsed().as_millis() > 1000 { + last_check_displays = now; + if ndisplay != get_display_num() { + log::info!("Displays changed"); + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + } *LAST_ACTIVE.lock().unwrap() = now; if get_latency() < 1000 || last_sent.elapsed().as_millis() > 1000 { match c.frame(wait as _) { @@ -228,6 +241,14 @@ fn handle_one_frame( Ok(()) } +fn get_display_num() -> usize { + if let Ok(d) = Display::all() { + d.len() + } else { + 0 + } +} + pub fn get_displays() -> ResultType<(usize, Vec)> { // switch to primary display if long time (30 seconds) no users if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 { diff --git a/src/ui.rs b/src/ui.rs index a8e1450ff..4f4506d00 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -486,6 +486,11 @@ impl UI { format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) } + fn create_shortcut(&self, id: String) { + #[cfg(windows)] + crate::platform::windows::create_shortcut(&id).ok(); + } + fn open_url(&self, url: String) { #[cfg(windows)] let p = "explorer"; @@ -541,6 +546,7 @@ impl sciter::EventHandler for UI { fn get_software_store_path(); fn get_software_ext(); fn open_url(String); + fn create_shortcut(String); } } diff --git a/src/ui/header.tis b/src/ui/header.tis index 951236c89..2c3f39131 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -36,7 +36,9 @@ function stateChanged() { cur_window_state = view.windowState; adjustBorder(); adaptDisplay(); - if (!is_linux) view.focus = handler; // this cause windows always topmost on linux + if (cur_window_state != View.WINDOW_MINIMIZED) { + view.focus = handler; // to make focus away from restore/maximize button, so that enter key work + } var fs = view.windowState == View.WINDOW_FULL_SCREEN; var el = $(#fullscreen); if (el) el.attributes.toggleClass("active", fs); @@ -133,8 +135,13 @@ class Header: Reactor.Component { return
  • Transfer File
  • -
  • TCP Tunneling
  • +
  • TCP Tunneling
  • +
    {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • Insert Ctrl + Alt + Del
  • : ""} +
  • Insert Ctrl + Space
  • +
  • Insert Alt + Tab
  • + {false &&
  • Insert Win/Super + ...
  • } +
    {keyboard_enabled ?
  • Insert Lock
  • : ""} {false && pi.platform == "Windows" ?
  • Block user input
  • : ""} {handler.support_refresh() ?
  • Refresh
  • : ""} @@ -221,6 +228,18 @@ class Header: Reactor.Component { event click $(#ctrl-alt-del) { handler.ctrl_alt_del(); } + + event click $(#alt-tab) { + handler.alt_tab(); + } + + event click $(#ctrl-space) { + handler.ctrl_space(); + } + + event click $(#super-x) { + handler.super_x(); + } event click $(#lock-screen) { handler.lock_screen(); diff --git a/src/ui/index.html b/src/ui/index.html index 638e5d09d..c51c64d84 100644 --- a/src/ui/index.html +++ b/src/ui/index.html @@ -9,22 +9,9 @@ include "common.tis"; include "index.tis"; - - -
  • Connect
  • -
  • Transfer File
  • -
  • TCP Tunneling
  • -
  • RDP
  • -
  • Remove
  • - -
    - -
  • Refresh random password
  • -
  • Set your own password
  • -
    - \ No newline at end of file + diff --git a/src/ui/index.tis b/src/ui/index.tis index 63e77798c..1af803470 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -103,6 +103,8 @@ event click $(menu#remote-context li) (evt, me) { } else if (action == "remove") { handler.remove_peer(id); app.recent_sessions.update(); + } else if (action == "shortcut") { + handler.create_shortcut(id); } else if (action == "rdp") { createNewConnect(id, "rdp"); } else if (action == "tunnel") { @@ -115,7 +117,7 @@ function createNewConnect(id, type) { app.remote_id.value = formatId(id); if (!id) return; if (id == handler.get_id()) { - handler.msgbox("custom-error", "Error", "Sorry, it is yourself"); + handler.msgbox("custom-error", "Error", "You cannot connect to your own computer"); return; } handler.set_remote_id(id); @@ -310,6 +312,22 @@ class App: Reactor.Component var is_can_screen_recording = handler.is_can_screen_recording(false); return
    + + +
  • Connect
  • +
  • Transfer File
  • +
  • TCP Tunneling
  • +
  • RDP
  • +
  • Remove
  • + {is_win &&
  • Create Desktop Shortcut
  • } + +
    + + +
  • Refresh random password
  • +
  • Set your own password
  • + +
    Your Desktop
    @@ -421,7 +439,7 @@ class UpgradeMe: Reactor.Component { var update_or_download = is_osx ? "download" : "update"; return
    {handler.get_app_name()} Status
    -
    Your installation is lower version.
    +
    An update is available for RustDesk.
    Click to upgrade
    ; } @@ -444,7 +462,7 @@ class UpdateMe: Reactor.Component { event click $(#install-me) { if (is_osx) { - handler.open_url("http://rustdesk.com"); + handler.open_url("https://rustdesk.com"); return; } var url = software_update_url + '.' + handler.get_software_ext(); diff --git a/src/ui/port_forward.tis b/src/ui/port_forward.tis index 986d22de1..d30367710 100644 --- a/src/ui/port_forward.tis +++ b/src/ui/port_forward.tis @@ -22,7 +22,7 @@ class PortForward: Reactor.Component { return
    {pfs.length ?
    Listening ...
    - Don't close this window while your are using tunnel + Don't close this window while you are using the tunnel
    : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f18600fc9..b4dd201f1 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -9,6 +9,7 @@ use hbb_common::{ fs, log, message_proto::*, protobuf::Message as _, + sleep, tokio::{ self, sync::mpsc, @@ -62,6 +63,7 @@ pub struct Handler { id: String, args: Vec, lc: Arc>, + super_on: bool, } impl Deref for Handler { @@ -152,6 +154,9 @@ impl sciter::EventHandler for Handler { fn send_mouse(i32, i32, i32, bool, bool, bool, bool); fn key_down_or_up(bool, String, i32, bool, bool, bool, bool, bool); fn ctrl_alt_del(); + fn ctrl_space(); + fn alt_tab(); + fn super_x(); fn transfer_file(); fn tunnel(); fn lock_screen(); @@ -710,6 +715,7 @@ impl Handler { 0x4C => key_event.set_control_key(ControlKey::NumpadEnter), // numpad enter 0x69 => key_event.set_control_key(ControlKey::Snapshot), 0x72 => key_event.set_control_key(ControlKey::Help), + 0x6E => key_event.set_control_key(ControlKey::Apps), 0x47 => { key_event.set_control_key(if self.peer_platform() == "Mac OS" { ControlKey::Clear @@ -731,6 +737,7 @@ impl Handler { 0x91 => key_event.set_control_key(ControlKey::Scroll), 0x90 => key_event.set_control_key(ControlKey::NumLock), 0x5C => key_event.set_control_key(ControlKey::RWin), + 0x5B => key_event.set_control_key(ControlKey::Meta), 0x5D => key_event.set_control_key(ControlKey::Apps), 0xBE => key_event.set_chr('.' as _), 0xC0 => key_event.set_chr('`' as _), @@ -739,6 +746,44 @@ impl Handler { return None; } } + } else if cfg!(target_os = "linux") { + match code { + 65300 => key_event.set_control_key(ControlKey::Scroll), + 65421 => key_event.set_control_key(ControlKey::NumpadEnter), // numpad enter + 65407 => key_event.set_control_key(ControlKey::NumLock), + 65516 => key_event.set_control_key(ControlKey::RWin), + 65513 => key_event.set_control_key(ControlKey::Alt), + 65514 => key_event.set_control_key(ControlKey::RAlt), + 65508 => key_event.set_control_key(ControlKey::RControl), + 65506 => key_event.set_control_key(ControlKey::RShift), + 96 => key_event.set_chr('`' as _), + 46 => key_event.set_chr('.' as _), + 126 => key_event.set_chr('`' as _), + 33 => key_event.set_chr('1' as _), + 64 => key_event.set_chr('2' as _), + 35 => key_event.set_chr('3' as _), + 36 => key_event.set_chr('4' as _), + 37 => key_event.set_chr('5' as _), + 94 => key_event.set_chr('6' as _), + 38 => key_event.set_chr('7' as _), + 42 => key_event.set_chr('8' as _), + 40 => key_event.set_chr('9' as _), + 41 => key_event.set_chr('0' as _), + 95 => key_event.set_chr('-' as _), + 43 => key_event.set_chr('=' as _), + 123 => key_event.set_chr('[' as _), + 125 => key_event.set_chr(']' as _), + 124 => key_event.set_chr('\\' as _), + 58 => key_event.set_chr(';' as _), + 34 => key_event.set_chr('\'' as _), + 60 => key_event.set_chr(',' as _), + 62 => key_event.set_chr('.' as _), + 63 => key_event.set_chr('/' as _), + _ => { + log::error!("Unknown key code {}", code); + return None; + } + } } else { log::error!("Unknown key code {}", code); return None; @@ -772,6 +817,20 @@ impl Handler { } } + fn super_x(&mut self) { + self.super_on = true; + } + + fn ctrl_space(&mut self) { + let key = "VK_SPACE".to_owned(); + self.key_down_or_up(3, key, 0, false, true, false, false, false); + } + + fn alt_tab(&mut self) { + let key = "VK_TAB".to_owned(); + self.key_down_or_up(3, key, 0, true, false, false, false, false); + } + fn lock_screen(&mut self) { let lock = "LOCK_SCREEN".to_owned(); self.key_down_or_up(1, lock, 0, false, false, false, false, false); @@ -819,6 +878,26 @@ impl Handler { extended, ); + let mut command = command; + if self.super_on { + command = true; + } + + if down_or_up == 0 { + self.super_on = false; + } + + let mut name = name; + + if extended { + match name.as_ref() { + "VK_CONTROL" => name = "RControl".to_owned(), + "VK_MENU" => name = "RAlt".to_owned(), + "VK_SHIFT" => name = "RShift".to_owned(), + _ => {} + } + } + if let Some(mut key_event) = self.get_key_event(down_or_up, &name, code) { // Linux has different repeated key down handling from mac and windows /* // below cause hang some time, not find why, so disable. so shift + repeat char not work for mac->linux, win->linux works fine, linux->linux not test yet @@ -847,16 +926,28 @@ impl Handler { } } */ - if alt && !crate::is_control_key(&key_event, &ControlKey::Alt) { + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { key_event.modifiers.push(ControlKey::Alt.into()); } - if shift && !crate::is_control_key(&key_event, &ControlKey::Shift) { + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { key_event.modifiers.push(ControlKey::Shift.into()); } - if ctrl && !crate::is_control_key(&key_event, &ControlKey::Control) { + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { key_event.modifiers.push(ControlKey::Control.into()); } - if command && !crate::is_control_key(&key_event, &ControlKey::Meta) { + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { key_event.modifiers.push(ControlKey::Meta.into()); } if crate::is_control_key(&key_event, &ControlKey::CapsLock) { @@ -1145,7 +1236,7 @@ impl Remote { } fn start_clipboard(&mut self) -> Option> { - if self.handler.is_file_transfer() { + if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } let (tx, rx) = std::sync::mpsc::channel(); @@ -1430,6 +1521,23 @@ impl Remote { } Some(login_response::Union::peer_info(pi)) => { self.handler.handle_peer_info(pi); + if !(self.handler.is_file_transfer() + || self.handler.is_port_forward() + || !*self.clipboard.read().unwrap() + || !*self.keyboard.read().unwrap() + || self.handler.lc.read().unwrap().disable_clipboard) + { + let txt = self.old_clipboard.lock().unwrap().clone(); + if !txt.is_empty() { + let msg_out = crate::create_clipboard_msg(txt); + let sender = self.sender.clone(); + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + sender.send(Data::Message(msg_out)).ok(); + }); + } + } } _ => {} }, @@ -1587,7 +1695,7 @@ impl Interface for Handler { pi_sciter.set_item("sas_enabled", pi.sas_enabled); if self.is_file_transfer() { if pi.username.is_empty() { - self.on_error("No active user logged on, please connect and logon first."); + self.on_error("No active console user logged on, please connect and logon first."); return; } } else if !self.is_port_forward() {