forked from mirrors/rustdesk
100% open source
This commit is contained in:
parent
9098619162
commit
c1bad84a86
58 changed files with 8397 additions and 3292 deletions
1359
Cargo.lock
generated
1359
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
40
Cargo.toml
40
Cargo.toml
|
|
@ -5,9 +5,19 @@ authors = ["rustdesk <info@rustdesk.com>"]
|
|||
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
|
||||
|
|
|
|||
1
libs/enigo/.gitattributes
vendored
Normal file
1
libs/enigo/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* text=auto
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "hbb_common"
|
||||
version = "0.1.0"
|
||||
authors = ["rustdesk<info@rustdesk.com>"]
|
||||
authors = ["open-trade <info@opentradesolutions.com>"]
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ message PeerInfo {
|
|||
int32 current_display = 5;
|
||||
bool sas_enabled = 6;
|
||||
string version = 7;
|
||||
int32 conn_id = 8;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<RwLock<String>> = 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<RwLock<Config2>> = Arc::new(RwLock::new(Config2::load()));
|
||||
static ref LOCAL_CONFIG: Arc<RwLock<LocalConfig>> = Arc::new(RwLock::new(LocalConfig::load()));
|
||||
pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = Default::default();
|
||||
pub static ref PROD_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Default::default();
|
||||
pub static ref APP_NAME: Arc<RwLock<String>> = 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<RwLock<String>> = Default::default();
|
||||
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = 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<String> = Self::get_option("rendezvous-servers")
|
||||
|
|
@ -446,7 +465,13 @@ impl Config {
|
|||
|
||||
fn get_auto_id() -> Option<String> {
|
||||
#[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<String, String> {
|
||||
CONFIG2.read().unwrap().options.clone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,10 +430,11 @@ pub fn new_error<T: std::string::ToString>(id: i32, err: T, file_num: i32) -> Me
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_dir(id: i32, files: Vec<FileEntry>) -> Message {
|
||||
pub fn new_dir(id: i32, path: String, files: Vec<FileEntry>) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_dir(FileDirectory {
|
||||
id,
|
||||
path,
|
||||
entries: files.into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(".") {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub enum FramedSocket {
|
|||
ProxySocks(Socks5UdpFramed),
|
||||
}
|
||||
|
||||
fn new_socket(addr: SocketAddr, reuse: bool) -> Result<Socket, std::io::Error> {
|
||||
fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket, std::io::Error> {
|
||||
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, std::io::Error> {
|
|||
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<T: std::net::ToSocketAddrs>(addr: T) -> ResultType<Self> {
|
||||
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<T: std::net::ToSocketAddrs>(
|
||||
addr: T,
|
||||
buf_size: usize,
|
||||
) -> ResultType<Self> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -4,35 +4,40 @@ use std::{
|
|||
};
|
||||
|
||||
fn find_package(name: &str) -> Vec<PathBuf> {
|
||||
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");
|
||||
|
|
|
|||
235
libs/scrap/src/android/ffi.rs
Normal file
235
libs/scrap/src/android/ffi.rs
Normal file
|
|
@ -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<Option<JavaVM>> = RwLock::new(None);
|
||||
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
|
||||
static ref INPUT_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None);
|
||||
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
|
||||
static ref AUDIO_RAW: Mutex<FrameRaw> = 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<u8>,
|
||||
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<String> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
6
libs/scrap/src/android/mod.rs
Normal file
6
libs/scrap/src/android/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod ffi;
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub use ffi::*;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
129
libs/scrap/src/common/android.rs
Normal file
129
libs/scrap/src/common/android.rs
Normal file
|
|
@ -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<u8>,
|
||||
saved_raw_data: Vec<u128>, // for faster compare and copy
|
||||
}
|
||||
|
||||
impl Capturer {
|
||||
pub fn new(display: Display, _yuv: bool) -> io::Result<Capturer> {
|
||||
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<Frame<'a>> {
|
||||
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<Display> {
|
||||
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<Vec<Display>> {
|
||||
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::<u16>().ok()?;
|
||||
let h = sp.next()?.parse::<u16>().ok()?;
|
||||
return Some((w, h));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -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<u128>, b: &[u8]) -> std::io::Result<()
|
|||
old.resize(b.len(), 0);
|
||||
old.copy_from_slice(b);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -20,4 +20,7 @@ pub mod wayland;
|
|||
#[cfg(dxgi)]
|
||||
pub mod dxgi;
|
||||
|
||||
#[cfg(android)]
|
||||
pub mod android;
|
||||
|
||||
mod common;
|
||||
|
|
|
|||
10
src/cli.rs
10
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::<Data>();
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
153
src/client.rs
153
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<u8>, conn: &mut Stream) -> ResultType<()> {
|
||||
let rs_pk = get_rs_pk("OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=");
|
||||
async fn secure_connection(
|
||||
peer_id: &str,
|
||||
signed_id_pk: Vec<u8>,
|
||||
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<Stream> {
|
||||
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<Stream> {
|
||||
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<u8>, // 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::<HashMap<String, String>>(&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<u8>) -> 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<RwLock<LoginConfigHandler>>,
|
||||
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);
|
||||
|
|
|
|||
88
src/client/file_trait.rs
Normal file
88
src/client/file_trait.rs
Normal file
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
144
src/common.rs
144
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<Mutex<String>>>,
|
||||
|
|
@ -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<Mutex<String>>>) {
|
||||
let content = if clipboard.compress {
|
||||
decompress(&clipboard.content)
|
||||
|
|
@ -80,15 +83,16 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>)
|
|||
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<bool> {
|
||||
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<u8> {
|
||||
#[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<String> {
|
||||
#[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<String> {
|
||||
post_request(url, body, header).await
|
||||
}
|
||||
|
|
|
|||
101
src/ipc.rs
101
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<String>),
|
||||
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<Option<Stri
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn set_config(name: &str, value: String) -> 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<HashMap<String, String>> {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn get_options() -> HashMap<String, String> {
|
||||
pub async fn get_options_async() -> HashMap<String, String> {
|
||||
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<String, String> {
|
||||
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<String> {
|
||||
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<Option<config::Socks5Server>> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
12
src/lang.rs
12
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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
72
src/lib.rs
72
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;
|
||||
|
|
|
|||
46
src/lic_main.rs
Normal file
46
src/lic_main.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
mod license;
|
||||
use hbb_common::{sodiumoxide::crypto::sign, ResultType};
|
||||
use license::*;
|
||||
|
||||
fn gen_license(lic: &License) -> ResultType<String> {
|
||||
let tmp = serde_json::to_vec::<License>(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]));
|
||||
}
|
||||
}
|
||||
30
src/license.rs
Normal file
30
src/license.rs
Normal file
|
|
@ -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<License> {
|
||||
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::<License>(&data) {
|
||||
return Ok(lic);
|
||||
}
|
||||
if let Ok(data) = sign::verify(&data, &pk) {
|
||||
Ok(serde_json::from_slice::<License>(&data)?)
|
||||
} else {
|
||||
bail!("sign:verify failed");
|
||||
}
|
||||
}
|
||||
57
src/main.rs
57
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<flexi_logger::LoggerHandle> = None;
|
||||
let mut args: Vec<String> = 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1306
src/mobile.rs
Normal file
1306
src/mobile.rs
Normal file
File diff suppressed because it is too large
Load diff
561
src/mobile_ffi.rs
Normal file
561
src/mobile_ffi.rs
Normal file
|
|
@ -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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<usize>().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::<HashMap<String, String>>(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::<i32>() {
|
||||
Session::switch_display(v);
|
||||
}
|
||||
}
|
||||
"remove" => {
|
||||
PeerConfig::remove(value);
|
||||
}
|
||||
"input_key" => {
|
||||
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<i32>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
let y = m
|
||||
.get("y")
|
||||
.map(|x| x.parse::<i32>().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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, String>>(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::<HashMap<String, Value>>(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::<HashMap<String, Value>>(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::<i32>() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -536,7 +536,7 @@ pub fn run_as_user(arg: &str) -> ResultType<Option<std::process::Child>> {
|
|||
// -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<Option<String>> {
|
||||
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()?;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<Data>,
|
||||
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<Data>,
|
||||
interface: impl Interface,
|
||||
forward: &mut Framed<TcpStream, BytesCodec>,
|
||||
key: &str,
|
||||
token: &str,
|
||||
is_rdp: bool,
|
||||
) -> ResultType<Option<Stream>> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
796
src/server.rs
796
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<Mutex<Vec<std::process::Child>>>;
|
||||
type ConnMap = HashMap<i32, ConnInner>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CHILD_PROCESS: Childs = Default::default();
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
connections: ConnMap,
|
||||
services: HashMap<&'static str, Box<dyn Service>>,
|
||||
id_count: i32,
|
||||
}
|
||||
|
||||
pub type ServerPtr = Arc<RwLock<Server>>;
|
||||
pub type ServerPtrWeak = Weak<RwLock<Server>>;
|
||||
|
||||
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<dyn Service>) {
|
||||
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<Mutex<Vec<std::process::Child>>>;
|
||||
type ConnMap = HashMap<i32, ConnInner>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CHILD_PROCESS: Childs = Default::default();
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
connections: ConnMap,
|
||||
services: HashMap<&'static str, Box<dyn Service>>,
|
||||
id_count: i32,
|
||||
}
|
||||
|
||||
pub type ServerPtr = Arc<RwLock<Server>>;
|
||||
pub type ServerPtrWeak = Weak<RwLock<Server>>;
|
||||
|
||||
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<dyn Service>) {
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<cpal_impl::State, _>(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<f32> = 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::<f32>(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<dyn StreamTrait>, Arc<Message>)>,
|
||||
}
|
||||
|
||||
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<dyn StreamTrait>, Arc<Message>)> {
|
||||
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<u8> = 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::<f32>(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::<cpal_impl::State, _>(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<f32> = 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::<f32>(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::<f32>(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<dyn StreamTrait>, Arc<Message>)>,
|
||||
}
|
||||
|
||||
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<dyn StreamTrait>, Arc<Message>)> {
|
||||
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<u8> = 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::<f32>(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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ClipboardContext>,
|
||||
}
|
||||
|
||||
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::<State, _>(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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Message>)>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref LOGIN_FAILURES: Arc::<Mutex<HashMap<String, (i32, i32, i32)>>> = 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<MessageInput>, // 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<MessageInput>, 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<MessageInput>) {
|
||||
let mut last_privacy = false;
|
||||
loop {
|
||||
|
|
@ -443,7 +471,7 @@ impl Connection {
|
|||
rx_from_cm: &mut mpsc::UnboundedReceiver<Data>,
|
||||
) -> 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<ipc::Data>,
|
||||
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
|
||||
|
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Message>);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
|
||||
name: &'static str,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
subscribes: HashMap<i32, T>,
|
||||
new_subscribes: HashMap<i32, T>,
|
||||
active: bool,
|
||||
need_snapshot: bool,
|
||||
}
|
||||
|
||||
pub trait Reset {
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
pub struct ServiceTmpl<T: Subscriber + From<ConnInner>>(Arc<RwLock<ServiceInner<T>>>);
|
||||
pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
|
||||
pub type GenericService = ServiceTmpl<ConnInner>;
|
||||
pub const HIBERNATE_TIMEOUT: u64 = 30;
|
||||
pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
|
||||
fn send_new_subscribes(&mut self, msg: Arc<Message>) {
|
||||
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<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
|
||||
#[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<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
pub fn new(name: &'static str, need_snapshot: bool) -> Self {
|
||||
Self(Arc::new(RwLock::new(ServiceInner::<T> {
|
||||
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<F>(&self, callback: F) -> ResultType<()>
|
||||
where
|
||||
F: FnMut(ServiceSwap<T>) -> ResultType<()>,
|
||||
{
|
||||
if self.0.read().unwrap().new_subscribes.len() > 0 {
|
||||
log::info!("Call snapshot of {} service", self.name());
|
||||
let mut callback = callback;
|
||||
callback(ServiceSwap::<T>(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<Message>) {
|
||||
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<i32> {
|
||||
self.send_video_frame_shared(Arc::new(msg))
|
||||
}
|
||||
|
||||
pub fn send_video_frame_shared(&self, msg: Arc<Message>) -> HashSet<i32> {
|
||||
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<S, F>(&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<F>(&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<T: Subscriber + From<ConnInner>> ServiceSwap<T> {
|
||||
#[inline]
|
||||
pub fn send(&self, msg: Message) {
|
||||
self.send_shared(Arc::new(msg));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn send_shared(&self, msg: Arc<Message>) {
|
||||
(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<T: Subscriber + From<ConnInner>> Drop for ServiceSwap<T> {
|
||||
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<Message>);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServiceInner<T: Subscriber + From<ConnInner>> {
|
||||
name: &'static str,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
subscribes: HashMap<i32, T>,
|
||||
new_subscribes: HashMap<i32, T>,
|
||||
active: bool,
|
||||
need_snapshot: bool,
|
||||
}
|
||||
|
||||
pub trait Reset {
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
pub struct ServiceTmpl<T: Subscriber + From<ConnInner>>(Arc<RwLock<ServiceInner<T>>>);
|
||||
pub struct ServiceSwap<T: Subscriber + From<ConnInner>>(ServiceTmpl<T>);
|
||||
pub type GenericService = ServiceTmpl<ConnInner>;
|
||||
pub const HIBERNATE_TIMEOUT: u64 = 30;
|
||||
pub const MAX_ERROR_TIMEOUT: u64 = 1_000;
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceInner<T> {
|
||||
fn send_new_subscribes(&mut self, msg: Arc<Message>) {
|
||||
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<T: Subscriber + From<ConnInner>> Service for ServiceTmpl<T> {
|
||||
#[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<T: Subscriber + From<ConnInner>> Clone for ServiceTmpl<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
|
||||
pub fn new(name: &'static str, need_snapshot: bool) -> Self {
|
||||
Self(Arc::new(RwLock::new(ServiceInner::<T> {
|
||||
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<F>(&self, callback: F) -> ResultType<()>
|
||||
where
|
||||
F: FnMut(ServiceSwap<T>) -> ResultType<()>,
|
||||
{
|
||||
if self.0.read().unwrap().new_subscribes.len() > 0 {
|
||||
log::info!("Call snapshot of {} service", self.name());
|
||||
let mut callback = callback;
|
||||
callback(ServiceSwap::<T>(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<Message>) {
|
||||
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<i32> {
|
||||
self.send_video_frame_shared(Arc::new(msg))
|
||||
}
|
||||
|
||||
pub fn send_video_frame_shared(&self, msg: Arc<Message>) -> HashSet<i32> {
|
||||
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<S, F>(&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<F>(&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<T: Subscriber + From<ConnInner>> ServiceSwap<T> {
|
||||
#[inline]
|
||||
pub fn send(&self, msg: Message) {
|
||||
self.send_shared(Arc::new(msg));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn send_shared(&self, msg: Arc<Message>) {
|
||||
(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<T: Subscriber + From<ConnInner>> Drop for ServiceSwap<T> {
|
||||
fn drop(&mut self) {
|
||||
(self.0).0.write().unwrap().swap_new_subscribes();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HashSet<i32>> {
|
||||
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<i32> = 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<DisplayInfo>)> {
|
|||
}
|
||||
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<Vec<Display>> {
|
||||
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;
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
77
src/tray.rs
Normal file
77
src/tray.rs
Normal file
|
|
@ -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<Mutex<HashMap<String, String>>>) {
|
||||
let event_loop = EventLoop::<Events>::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", "");
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
400
src/ui.rs
400
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<Mutex<(bool, HashMap<(String, String), Child>)>>;
|
||||
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<Vec<Arc<Vec<Value>>>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct UI(
|
||||
Childs,
|
||||
Arc<Mutex<(i32, bool)>>,
|
||||
Arc<Mutex<Status>>,
|
||||
Arc<Mutex<HashMap<String, String>>>,
|
||||
Arc<Mutex<String>>,
|
||||
mpsc::UnboundedSender<ipc::Data>,
|
||||
);
|
||||
|
||||
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!(
|
||||
"<br /> Key: {} <br /> 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<Mutex<(i32, bool)>>,
|
||||
status: Arc<Mutex<Status>>,
|
||||
options: Arc<Mutex<HashMap<String, String>>>,
|
||||
rx: mpsc::UnboundedReceiver<ipc::Data>,
|
||||
) {
|
||||
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<String> {
|
|||
|
||||
fn check_connect_status(
|
||||
reconnect: bool,
|
||||
) -> (Arc<Mutex<(i32, bool)>>, Arc<Mutex<HashMap<String, String>>>) {
|
||||
let status = Arc::new(Mutex::new((0, false)));
|
||||
) -> (
|
||||
Arc<Mutex<Status>>,
|
||||
Arc<Mutex<HashMap<String, String>>>,
|
||||
mpsc::UnboundedSender<ipc::Data>,
|
||||
) {
|
||||
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::<ipc::Data>();
|
||||
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<Mutex<&str>> = 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
|
||||
|
|
|
|||
411
src/ui/ab.tis
411
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 <div style="margin: *"><div #login-link .link style="margin: *; width: 100px; text-align: center; font-size: 1.2em;">{translate("Login")}</div></div>;
|
||||
}
|
||||
if (abLoading) {
|
||||
return <div style="margin: *"><progress style="color: #0071ff" /></div>;
|
||||
} else if (abError) {
|
||||
return <div style="margin: *; text-align: center;">{abError}
|
||||
<div #retry .link style="margin-left: 1em;">{translate("Retry")}</div>
|
||||
</div>;
|
||||
}
|
||||
var peers = this.getPeers();
|
||||
var me = this;
|
||||
return <div .app #ab style="size:*">
|
||||
<popup>
|
||||
<menu.context #ab-context>
|
||||
<li #add-id>{translate('Add ID')}</li>
|
||||
<li #add-tag>{translate('Add Tag')}</li>
|
||||
<li #unselect-tags>{translate('Unselect all tags')}</li>
|
||||
</menu>
|
||||
<menu.context #tag-context>
|
||||
<li #remove-tag>{translate('Remove')}</li>
|
||||
</menu>
|
||||
</popup>
|
||||
<div .left-pane>
|
||||
<div style="padding: 0; padding-bottom: 1em" #tags-label>{translate('Tags')}{svg_menu}</div>
|
||||
<div #tags>
|
||||
{ab.tags.map(function(t) {
|
||||
return <span class={me.selectedTags.indexOf(t) >= 0 ? "active" : "inactive"}>{t}</span>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div .right-pane>
|
||||
<div .right-content style="padding-top:0; padding-right: 0;">
|
||||
<SessionList sessions={peers} type="ab" />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
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"), <div .form>
|
||||
<div>{translate("whitelist_sep")}</div>
|
||||
<textarea .outline-focus spellcheck="false" name="text" style="overflow: scroll-indicator; width:*; height: 160px; font-size: 1.2em; padding: 0.5em;"></textarea>
|
||||
</div>, 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"), <div .form>
|
||||
<div>{translate("whitelist_sep")}</div>
|
||||
<textarea .outline-focus spellcheck="false" name="text" style="overflow: scroll-indicator; width:*; height: 160px; font-size: 1.2em; padding: 0.5em;"></textarea>
|
||||
</div>, 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 <div #tags>
|
||||
{ab.tags.map(function(t) {
|
||||
return <span class={me.tags.indexOf(t) >= 0 ? "active" : "inactive"}>{t}</span>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
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 = <svg #session-tile viewBox="0 0 158.6 158.6"><path style="stroke-width:.309756" d="M5.4 157.7c-1-.3-2-1-3.2-2.1-2.8-2.8-2.6-1-2.5-32 0-26.7 0-27 .7-28.3a9.3 9.3 0 0 1 4-4.2c1.2-.6 2.3-.6 29-.7 27.5 0 27.6 0 29.1.6.8.4 2 1.2 2.7 2 2.4 2.5 2.3.7 2.2 31.6-.1 26.5-.1 27.6-.7 28.8a9.3 9.3 0 0 1-4.2 4c-1.4.6-1.6.6-28.5.7a235 235 0 0 1-28.6-.4zm91 0a8.5 8.5 0 0 1-5.7-5.4c-.2-.7-.3-8.3-.3-28.3V96.7l.7-1.6a8.9 8.9 0 0 1 4.6-4.3c1.2-.4 3.8-.5 28.9-.4 26.6.1 27.6.1 28.8.7 1.6.8 3.2 2.5 4 4.2.7 1.4.7 1.6.7 28.3.1 31 .3 29.2-2.5 32-2.8 2.7-1 2.6-31.4 2.6-21.4 0-26.8-.1-27.9-.5zM5.3 67a8.7 8.7 0 0 1-4-3C-.5 61.6-.5 62.3-.5 33.6-.4 3.2-.5 5 2.2 2.2 5-.6 3.2-.4 34.2-.3c26.7 0 27 0 28.3.7 1.7.8 3.4 2.4 4.2 4 .6 1.2.6 2.2.7 28.8 0 25.1 0 27.7-.4 29a9 9 0 0 1-4.3 4.5l-1.6.7H33.7c-20.2 0-27.7-.1-28.4-.4Zm89.8-.3a9 9 0 0 1-4.3-4.6c-.5-1.2-.5-3.8-.5-28.9.1-26.6.2-27.6.7-28.8a9.3 9.3 0 0 1 4.2-4c1.4-.7 1.6-.7 28.3-.7 31-.1 29.2-.3 32 2.5 2.8 2.8 2.6 1 2.5 32 0 26.7 0 26.9-.7 28.3a9.3 9.3 0 0 1-4 4.2c-1.2.5-2.2.6-29 .6l-27.7.1z" transform="translate(.4 .4)"/></svg>;
|
||||
var svg_list = <svg #session-list viewBox="0 0 246.8 185.8"><path style="stroke-width:.482473" d="M-69.2 102.7A16.5 16.5 0 0 1-67 70.4c7.3-1 15 4 17.3 11 1 3 1 8 0 10.8a16.7 16.7 0 0 1-19.5 10.5zm53-3.4a12.3 12.3 0 0 1-7-16.8c1.3-3 3.1-4.7 6-6 2.2-1 2.8-1 87.2-1 92.4 0 87-.2 90.6 2.6.9.7 2.2 2.4 3 3.7 1.2 2.2 1.4 3.1 1.4 6 0 4.8-2.3 8.6-6.8 11l-1.9 1-85.2.1c-71.9 0-85.5 0-87.3-.6zm-53.5-73c-4.7-1.5-8.6-5-10.6-9.1-1.8-4-1.8-9.8 0-13.7 1.6-3.3 4.4-6.2 7.8-8 2.2-1.2 3-1.3 7.1-1.3 4 0 5 .1 7.3 1.3a16.6 16.6 0 0 1 0 29.6c-2 1-3.4 1.4-6.5 1.5-2.2 0-4.5 0-5.1-.3zm52.3-4.8c-2.4-1.1-5.3-4-6.2-6.5-1-2.4-1-7.3.1-9.7.5-1.1 1.8-2.8 2.8-3.8 3.7-3.5-4-3.2 91-3.2h85.5l2.5 1.1a12 12 0 0 1 0 21.8l-2.5 1.2H70.2c-82.5 0-85.7 0-87.6-1zm-52.1-71.6a18 18 0 0 1-10-7.7 17 17 0 0 1-.7-15c2.3-5 5.8-7.9 11.4-9.3 9-2.3 18.3 4 19.8 13.4a16.4 16.4 0 0 1-15.2 19c-2.1.1-4.1 0-5.3-.4zm52.1-5.9c-1.3-.6-3-1.7-3.7-2.5-4.7-5-4.2-13.7 1-18 3.7-3.1-1.8-3 91.5-2.8l84.9.1 2 1a12 12 0 0 1 6.7 11c0 3-.2 3.9-1.4 6-.8 1.4-2.1 3-3 3.8-3.7 2.7 1.8 2.6-90.6 2.6h-85l-2.4-1.2z" transform="translate(81.7 82.6)"/></svg>;
|
||||
var search_icon = <svg viewBox="0 0 655.278 655.024"><g transform="translate(-24.497 -195.01)"><path d="m649.96 847.92c-2.9592-1.3629-27.183-24.243-63.36-59.846-32.213-31.702-70.814-69.663-85.78-84.357l-27.21-26.717-4.7897 3.5287c-66.337 48.872-145.32 66.878-224.31 51.138-72.966-14.539-136.58-58.184-178.47-122.44-15.945-24.462-30.723-61.471-36.413-91.191-8.9404-46.696-6.2422-90.39 8.3388-135.04 13.39-41.003 34.756-75.42 66.479-107.09 74.506-74.377 183.71-99.89 284.22-66.397 62.352 20.777 117.67 65.579 150.79 122.12 38.716 66.101 46.59 147.55 21.43 221.66-9.9038 29.171-29.788 63.725-49.916 86.743l-7.0583 8.0717 3.0992 2.919c1.7046 1.6054 40.675 39.928 86.602 85.161 89.007 87.664 87.558 86.034 85.619 96.293-1.2888 6.8209-5.2313 12.041-11.321 14.989-6.7901 3.287-11.55 3.4093-17.952 0.46117zm-316.64-154.63c32.373-5.0481 61.075-15.115 86.553-30.358 47.942-28.683 83.505-72.09 100.89-123.14 35.043-102.91-6.4362-214.07-100.89-270.37-52.514-31.302-117.76-40.564-178.06-25.277-81.183 20.579-145.19 82.918-166.86 162.52-5.5757 20.478-7.445 35.423-7.445 59.52s1.8693 39.042 7.445 59.52c21.409 78.63 85.366 141.52 164.81 162.05 29.22 7.5511 66.493 9.756 93.564 5.5347z" stroke-width="1.28"/></g></svg>;
|
||||
|
|
@ -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 {
|
|||
<li #connect>{translate('Connect')}</li>
|
||||
<li #transfer>{translate('Transfer File')}</li>
|
||||
<li #tunnel>{translate('TCP Tunneling')}</li>
|
||||
{false && !handler.using_public_server() && <li #force-always-relay><span>{svg_checkmark}</span>{translate('Always connect via relay')}</li>}
|
||||
<li #rdp>RDP<EditRdpPort /></li>
|
||||
<div .separator />
|
||||
<li #rename>{translate('Rename')}</li>
|
||||
|
|
@ -114,6 +325,7 @@ class SessionList: Reactor.Component {
|
|||
<li #forget-password>{translate('Unremember Password')}</li>
|
||||
{(!this.type || this.type == "fav") && <li #add-fav>{translate('Add to Favorites')}</li>}
|
||||
{(!this.type || this.type == "fav") && <li #remove-fav>{translate('Remove from Favorites')}</li>}
|
||||
{this.type == "ab" && <li #edit-tag>{translate('Edit Tag')}</li>}
|
||||
</menu>
|
||||
</popup>
|
||||
{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", "<div .form> \
|
||||
<div><input name='name' .outline-focus style='width: *; height: 23px', value='" + old_name + "' /></div> \
|
||||
</div> \
|
||||
|
|
@ -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", <SelectTags tags={peer.tags || []} />, 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 {
|
|||
<span class={!type ? 'active' : 'inactive'}>{translate('Recent Sessions')}</span>
|
||||
<span #fav class={type == "fav" ? 'active' : 'inactive'}>{translate('Favorites')}</span>
|
||||
{handler.is_installed() && <span #lan class={type == "lan" ? 'active' : 'inactive'}>{translate('Discovered')}</span>}
|
||||
<span #ab class={type == "ab" ? 'active' : 'inactive'}>{translate('Address Book')}</span>
|
||||
</div>
|
||||
{!this.hidden && <SearchBar type={type} />}
|
||||
{!this.hidden && <SessionStyle type={type} />}
|
||||
|
|
@ -274,6 +535,7 @@ class MultipleSessions: Reactor.Component {
|
|||
{!this.hidden &&
|
||||
((type == "fav" && <Favorites />) ||
|
||||
(type == "lan" && handler.is_installed() && <LanPeers />) ||
|
||||
(type == "ab" && <AddressBook />) ||
|
||||
<SessionList sessions={handler.get_recent_sessions()} />)}
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -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 <span style="margin-left: 12px; padding: 0 6px; display: inline-block;" .link>{svg_edit}</span>;
|
||||
}
|
||||
|
||||
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 ? <input|text name='port' value={p0} /> :
|
||||
<input|text name='port' novalue={3389} />;
|
||||
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'), <div .form .set-password>
|
||||
<div><span>{translate('Port')}:</span>{port}</div>
|
||||
<div><span>{translate('Username')}:</span><input|text name="username" value={name0} /></div>
|
||||
<div><span>{translate('Password')}:</span><PasswordComponent value={pass0} /></div>
|
||||
</div>, 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
27
src/ui/cm.rs
27
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<Element>,
|
||||
senders: HashMap<i32, mpsc::UnboundedSender<Data>>,
|
||||
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<u8> = 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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(<MsgboxComponent width={width} height={height} type={type} title={title} content={content} remember={remember} callback={callback} contentStyle={contentStyle} hasRetry={hasRetry} />);
|
||||
$(#msgbox).content(<MsgboxComponent width={width} height={height} auto_login={auto_login} type={type} title={title} content={content} remember={remember} callback={callback} contentStyle={contentStyle} hasRetry={hasRetry} />);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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 <span style="margin-left: 12px; padding: 0 6px; display: inline-block;" .link>{svg_edit}</span>;
|
||||
}
|
||||
|
||||
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 <popup>
|
||||
<menu.context #action-options>
|
||||
{keyboard_enabled ? <li #os-password>{translate('OS Password')}<EditOsPassword /></li> : ""}
|
||||
<li #transfer-file>{translate('Transfer File')}</li>
|
||||
<li #tunnel>{translate('TCP Tunneling')}</li>
|
||||
{handler.get_audit_server() && <li #note>{translate('Note')}</li>}
|
||||
<div .separator />
|
||||
{keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ? <li #ctrl-alt-del>{translate('Insert')} Ctrl + Alt + Del</li> : ""}
|
||||
{keyboard_enabled ? <li #lock-screen>{translate('Insert Lock')}</li> : ""}
|
||||
|
|
@ -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", <div .form>
|
||||
<textarea .outline-focus spellcheck="false" name="text" novalue="input note here" style="overflow: scroll-indicator; width:*; height: 140px; font-size: 1.2em; padding: 0.5em;">{self.conn_note}</textarea>
|
||||
</div>, 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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
324
src/ui/index.tis
324
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 <span>{translate("Ready")}, <span .link #setup-server>{translate("setup_server_tip")}</span></span>;
|
||||
}
|
||||
|
||||
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 <li class={cls}><span>{svg_checkmark}</span>{rdp_shared_string}</li>;
|
||||
}
|
||||
|
||||
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 <div #myid>
|
||||
{this.renderPop()}
|
||||
{translate("ID")}{svg_menu}
|
||||
ID{svg_menu}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function renderPop() {
|
||||
var username = handler.get_local_option("access_token") ? getUserName() : '';
|
||||
return <popup>
|
||||
<menu.context #config-options>
|
||||
<li #enable-keyboard><span>{svg_checkmark}</span>{translate('Enable Keyboard/Mouse')}</li>
|
||||
|
|
@ -164,16 +193,24 @@ class MyIdMenu: Reactor.Component {
|
|||
<li #enable-file-transfer><span>{svg_checkmark}</span>{translate('Enable File Transfer')}</li>
|
||||
<li #enable-tunnel><span>{svg_checkmark}</span>{translate('Enable TCP Tunneling')}</li>
|
||||
<AudioInputs />
|
||||
<li #allow-remote-config-modification><span>{svg_checkmark}</span>{translate('Enable remote configuration modification')}</li>
|
||||
<div .separator />
|
||||
<li #custom-server>{translate('ID/Relay Server')}</li>
|
||||
<li #whitelist title={translate('whitelist_tip')}>{translate('IP Whitelisting')}</li>
|
||||
<li #socks5-server>{translate('Socks5 Proxy')}</li>
|
||||
{is_win ? <li #install-virtual-display>Install virtual display</li> : ""}
|
||||
<div .separator />
|
||||
<li #stop-service><span>{svg_checkmark}</span>{translate("Enable Service")}</li>
|
||||
<li #stop-service class={service_stopped ? "line-through" : "selected"}><span>{svg_checkmark}</span>{translate("Enable Service")}</li>
|
||||
{handler.is_rdp_service_open() ? <ShareRdp /> : ""}
|
||||
<DirectServer />
|
||||
{false && handler.using_public_server() && <li #allow-always-relay><span>{svg_checkmark}</span>{translate('Always connected via relay')}</li>}
|
||||
{handler.has_rendezvous_service() ? <li #stop-rendezvous-service>{translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}</li> : ""}
|
||||
{handler.is_ok_change_id() ? <div .separator /> : ""}
|
||||
{username ?
|
||||
<li #logout>{translate('Logout')} ({username})</li> :
|
||||
<li #login>{translate('Login')}</li>}
|
||||
{handler.is_ok_change_id() && key_confirmed ? <li #change-id>{translate('Change ID')}</li> : ""}
|
||||
<div .separator />
|
||||
<li #about>{translate('About')} {" "} {handler.get_app_name()}</li>
|
||||
<li #about>{translate('About')} {" "}{handler.get_app_name()}</li>
|
||||
</menu>
|
||||
</popup>;
|
||||
}
|
||||
|
|
@ -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, "<div style='line-height: 2em'> \
|
||||
<div>Version: " + handler.get_version() + " \
|
||||
<div .link .custom-event url='http://rustdesk.com/privacy'>Privacy Statement</div> \
|
||||
<div .link .custom-event url='http://rustdesk.com'>Website</div> \
|
||||
<div .link .custom-event url='https://rustdesk.com/privacy'>Privacy Statement</div> \
|
||||
<div .link .custom-event url='https://rustdesk.com'>Website</div> \
|
||||
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright © 2022 Purslane Ltd.\
|
||||
<br />" + handler.get_license() + " \
|
||||
<p style='font-weight: bold'>Made with heart in this chaotic world!</p>\
|
||||
</div>\
|
||||
</div>", 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"), "<div .form> \
|
||||
<div>" + translate("whitelist_sep") + "</div> \
|
||||
<textarea spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; width:*; height: 140px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
|
||||
<textarea .outline-focus spellcheck=\"false\" name=\"text\" novalue=\"0.0.0.0\" style=\"overflow: scroll-indicator; width:*; height: 140px; font-size: 1.2em; padding: 0.5em;\">" + old_value + "</textarea>\
|
||||
</div> \
|
||||
", 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", "<div .form .set-password> \
|
||||
<div><span>" + translate("ID Server") + ": </span><input .outline-focus name='id' value='" + old_id + "' /></div> \
|
||||
<div><span>" + translate("Relay Server") + ": </span><input name='relay' value='" + old_relay + "' /></div> \
|
||||
<div><span>" + translate("API Server") + ": </span><input name='api' value='" + old_api + "' /></div> \
|
||||
<div><span>" + translate("Key") + ": </span><input name='key' value='" + old_key + "' /></div> \
|
||||
</div> \
|
||||
", 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"), "<div .form> \
|
||||
<div>" + translate('id_change_tip') + " </div> \
|
||||
<div><span style='width: 100px; display:inline-block'>ID: </span><input .outline-focus style='width: 250px' name='id' /></div> \
|
||||
</div> \
|
||||
", 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
|
|||
</div>
|
||||
<ConnectStatus @{this.connect_status} />
|
||||
</div>
|
||||
<div #overlay style="position: absolute;size:*;background:black;opacity:0.5;display:none" />
|
||||
<div #msgbox />
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -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(<App />);
|
||||
|
||||
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'), <div .form .set-password>
|
||||
<div><span>{translate('Username')}:</span><input|text name="username" value={name0} .outline-focus /></div>
|
||||
<div><span>{translate('Password')}:</span><PasswordComponent value={pass0} /></div>
|
||||
</div>, 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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
div.content {
|
||||
size: *;
|
||||
background: white;
|
||||
padding:2em 10em;
|
||||
padding:2em 8em;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -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 <div .content>
|
||||
<div style="font-size: 2em;">{translate('Installation')}</div>
|
||||
<div style="margin: 2em 0;">{translate('Installation Path')} {": "}<input|text disabled value={view.install_path()} /></div>
|
||||
<div style="margin: 2em 0;">{translate('Installation Path')} {": "}<input|text disabled value={view.install_path()} #path_input />
|
||||
<button .button .outline #path style="margin-left: 1em">{translate('Change Path')}</button>
|
||||
</div>
|
||||
<div><button|checkbox #startmenu checked>{translate('Create start menu shortcuts')}</button></div>
|
||||
<div><button|checkbox #desktopicon checked>{translate('Create desktop icon')}</button></div>
|
||||
<div #aggrement .link style="margin-top: 2em;">{translate('End-user license agreement')}</div>
|
||||
|
|
@ -16,6 +20,9 @@ class Install: Reactor.Component {
|
|||
<progress style={"color:" + color} style="display: none" />
|
||||
<button .button id="cancel" .outline style="margin-right: 2em;">{translate('Cancel')}</button>
|
||||
<button .button id="submit">{translate('Accept and Install')}</button>
|
||||
{handler.show_run_without_install() && <button .button #run-without-install .outline style="margin-left: 2em;">
|
||||
{translate('Run without install')}
|
||||
</button>}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <div .form>
|
||||
<PasswordComponent value={this.content} />
|
||||
<div><button|checkbox(auto_login) {ts}>{translate('Auto Login')}</button></div>
|
||||
</div>;
|
||||
}
|
||||
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") {
|
||||
|
|
|
|||
226
src/ui/remote.rs
226
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<EVENT_GROUPS> {
|
||||
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<Data>,
|
||||
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::<Data>();
|
||||
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::<i32>()
|
||||
.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::<i32, mpsc::UnboundedSender<Data>>::new();
|
||||
|
|
@ -1231,6 +1210,8 @@ async fn io_loop(handler: Handler) {
|
|||
let (sender, receiver) = mpsc::unbounded_channel::<Data>();
|
||||
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::<i32>().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<FileEntry>, only_count: bool) -> Value {
|
||||
pub fn make_fd(id: i32, entries: &Vec<FileEntry>, 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<FileEntry>, 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
239
src/windows.cc
239
src/windows.cc
|
|
@ -8,6 +8,7 @@
|
|||
#include <memory>
|
||||
#include <shlobj.h> // NOLINT(build/include_order)
|
||||
#include <userenv.h>
|
||||
#include <versionhelpers.h>
|
||||
|
||||
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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue