mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-11-23 19:49:05 +08:00
100% open source
This commit is contained in:
parent
9098619162
commit
c1bad84a86
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…
Reference in New Issue
Block a user