Files
rustdesk/src/ui_interface.rs
RustDesk b50fde6910 add the base crate and repoint the moved modules at it (#16107)
* add the base crate and repoint the moved modules at it

`libs/base` (crate `base`) takes the parts of hbb_common that only this app
uses: `fs`, `platform`, `keyboard`, `message.proto`, and 145 of the 177
`config::keys` constants. hbb_common keeps what the server names, and the 32
keys it reads itself are re-exported from `base::config::keys` so call sites
still see the full set through one path.

Sources move verbatim. The only edits inside them are `crate::` prefixes that
now have to say `hbb_common::`; `keyboard.rs` and `platform/windows.rs` are
byte-identical. The crate stays on edition 2018, the edition the moved code was
written under. `log`, `lazy_static` and `anyhow` become direct dependencies so
the bare paths in that code resolve exactly as before, and its winapi features
are spelled out rather than left to feature unification.

Two call sites outside Rust and Cargo had to follow the move: the Android
protobuf source dir, which still pointed at hbb_common/protos for message.proto,
and the three AGENTS.md entries that named hbb_common for options, protos and
file transfer.

`scrap`'s `drm` feature now forwards to `base/wayland_probe`. Left pointing at
hbb_common it would still have compiled, silently dropping the Wayland
socket-probe fallback, so that forward is verified by a build with and without
the feature.

`config::keys` carries a test asserting its names stay disjoint from the ones
hbb_common kept: the glob re-export and the local constants share a namespace,
and Rust prefers the local item silently, so a name added to both sides would
otherwise let client and server disagree with no diagnostic.

Verified: macOS and Linux, debug and release, `--all-targets`; the 177 key
constants diffed name-for-name and value-for-value; the generated protobuf types
compared before and after; every `#[cfg]` gate on a moved import checked against
its original; and every file that was `rustfmt`-clean before this change still
is, compared against master file by file. Windows is checked by inspection only
-- it cannot be compiled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* one `use` per crate, and write the rule down

`fs.rs` came out of the move with two ungated `use hbb_common::` statements,
because the original single `use crate::{...}` had to give up `message_proto`
to the new crate and the rest was left in a second block. Fold it back into one.

A scan of the whole tree for the same shape finds nothing else: every other file
with more than one top-level `use base::` or `use hbb_common::` is split by a
`#[cfg]` that does not cover the whole block, or by `pub use` next to `use`.
Those are the cases that cannot merge, so AGENTS.md now states both the rule and
the exemption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:46:42 +08:00

1779 lines
54 KiB
Rust

use base::config::keys::{self, *};
#[cfg(any(target_os = "android", target_os = "ios"))]
use hbb_common::password_security;
use hbb_common::{
allow_err,
bytes::Bytes,
config::{self, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT},
directories_next,
futures::future::join_all,
log,
rendezvous_proto::*,
tokio,
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::{
sleep,
tokio::{sync::mpsc, time},
};
use serde_derive::Serialize;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::process::Child;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use crate::common::SOFTWARE_UPDATE_URL;
#[cfg(feature = "flutter")]
use crate::hbbs_http::account;
#[cfg(not(any(target_os = "ios")))]
use crate::ipc;
type Message = RendezvousMessage;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub type Children = Arc<Mutex<(bool, HashMap<(String, String), Child>)>>;
#[derive(Clone, Debug, Serialize)]
pub struct UiStatus {
pub status_num: i32,
#[cfg(not(feature = "flutter"))]
pub key_confirmed: bool,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub mouse_time: i64,
#[cfg(not(feature = "flutter"))]
pub id: String,
#[cfg(feature = "flutter")]
pub video_conn_count: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct LoginDeviceInfo {
pub os: String,
pub r#type: String,
pub name: String,
}
lazy_static::lazy_static! {
static ref UI_STATUS : Arc<Mutex<UiStatus>> = Arc::new(Mutex::new(UiStatus{
status_num: 0,
#[cfg(not(feature = "flutter"))]
key_confirmed: false,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mouse_time: 0,
#[cfg(not(feature = "flutter"))]
id: "".to_owned(),
#[cfg(feature = "flutter")]
video_conn_count: 0,
}));
static ref ASYNC_JOB_STATUS : Arc<Mutex<String>> = Default::default();
static ref ASYNC_HTTP_STATUS : Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
static ref TEMPORARY_PASSWD : Arc<Mutex<String>> = Arc::new(Mutex::new("".to_owned()));
static ref IS_REMOTE_MODIFY_ENABLED_BY_CONTROL_PERMISSIONS : Arc<Mutex<Option<bool>>> = Arc::new(Mutex::new(None));
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
lazy_static::lazy_static! {
static ref OPTION_SYNCED: Arc<Mutex<bool>> = Default::default();
static ref OPTIONS : Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(Config::get_options()));
pub static ref SENDER : Mutex<mpsc::UnboundedSender<ipc::Data>> = Mutex::new(check_connect_status(true));
static ref CHILDREN : Children = Default::default();
}
#[cfg(target_os = "windows")]
lazy_static::lazy_static! {
pub static ref IS_FILE_TRANSFER_ENABLED: Arc<Mutex<Option<bool>>> = Arc::new(Mutex::new(None));
}
const INIT_ASYNC_JOB_STATUS: &str = " ";
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn get_id() -> String {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Config::get_id();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return ipc::get_id();
}
#[inline]
pub fn goto_install() {
allow_err!(crate::run_me(vec!["--install"]));
std::process::exit(0);
}
#[inline]
pub fn install_me(_options: String, _path: String, _silent: bool, _debug: bool) {
#[cfg(windows)]
std::thread::spawn(move || {
allow_err!(crate::platform::windows::install_me(
&_options, _path, _silent, _debug
));
std::process::exit(0);
});
}
#[inline]
pub fn update_me(_path: String) {
goto_install();
}
#[inline]
pub fn run_without_install() {
crate::run_me(vec!["--noinstall"]).ok();
std::process::exit(0);
}
#[inline]
pub fn show_run_without_install() -> bool {
let mut it = std::env::args();
if let Some(tmp) = it.next() {
if crate::is_setup(&tmp) {
return it.next() == None;
}
}
false
}
#[inline]
pub fn get_license() -> String {
#[cfg(windows)]
if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() {
#[cfg(feature = "flutter")]
return format!("Key: {}\nHost: {}\nAPI: {}", lic.key, lic.host, lic.api);
// default license format is html formed (sciter)
#[cfg(not(feature = "flutter"))]
return format!(
"<br /> Key: {} <br /> Host: {} API: {}",
lic.key, lic.host, lic.api
);
}
Default::default()
}
#[inline]
pub fn refresh_options() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
*OPTIONS.lock().unwrap() = Config::get_options();
}
}
#[inline]
pub fn get_option<T: AsRef<str>>(key: T) -> String {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let map = OPTIONS.lock().unwrap();
if let Some(v) = map.get(key.as_ref()) {
v.to_owned()
} else {
"".to_owned()
}
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
Config::get_option(key.as_ref())
}
}
#[inline]
pub fn use_texture_render() -> bool {
#[cfg(target_os = "android")]
return false;
#[cfg(target_os = "ios")]
return false;
#[cfg(target_os = "macos")]
return cfg!(feature = "flutter")
&& LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) == "Y";
#[cfg(target_os = "linux")]
return cfg!(feature = "flutter")
&& LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) != "N";
#[cfg(target_os = "windows")]
{
if !cfg!(feature = "flutter") {
return false;
}
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
#[cfg(debug_assertions)]
let default_texture = true;
#[cfg(not(debug_assertions))]
let default_texture = crate::platform::is_win_10_or_greater();
if default_texture {
LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) != "N"
} else {
return LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) == "Y";
}
}
}
#[inline]
pub fn is_option_fixed(key: &str) -> bool {
config::OVERWRITE_DISPLAY_SETTINGS
.read()
.unwrap()
.contains_key(key)
|| config::OVERWRITE_LOCAL_SETTINGS
.read()
.unwrap()
.contains_key(key)
|| config::OVERWRITE_SETTINGS.read().unwrap().contains_key(key)
}
#[inline]
pub fn get_local_option(key: String) -> String {
crate::get_local_option(&key)
}
#[inline]
#[cfg(feature = "flutter")]
pub fn get_hard_option(key: String) -> String {
config::HARD_SETTINGS
.read()
.unwrap()
.get(&key)
.cloned()
.unwrap_or_default()
}
#[inline]
pub fn get_builtin_option(key: &str) -> String {
crate::get_builtin_option(key)
}
#[inline]
pub fn set_local_option(key: String, value: String) {
LocalConfig::set_option(key.clone(), value);
}
/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
/// by prepending the API server address.
pub fn resolve_avatar_url(avatar: String) -> String {
let avatar = avatar.trim().to_owned();
if avatar.starts_with('/') {
let api_server = get_api_server();
if !api_server.is_empty() {
return format!("{}{}", api_server.trim_end_matches('/'), avatar);
}
}
avatar
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn get_local_flutter_option(key: String) -> String {
LocalConfig::get_flutter_option(&key)
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn set_local_flutter_option(key: String, value: String) {
LocalConfig::set_flutter_option(key, value);
}
#[cfg(feature = "flutter")]
#[inline]
pub fn get_kb_layout_type() -> String {
LocalConfig::get_kb_layout_type()
}
#[cfg(feature = "flutter")]
#[inline]
pub fn set_kb_layout_type(kb_layout_type: String) {
LocalConfig::set_kb_layout_type(kb_layout_type);
}
#[inline]
pub fn peer_has_password(id: String) -> bool {
!PeerConfig::load(&id).password.is_empty()
}
#[inline]
pub fn forget_password(id: String) {
let mut c = PeerConfig::load(&id);
c.password.clear();
c.store(&id);
}
#[inline]
pub fn get_peer_option(id: String, name: String) -> String {
let c = PeerConfig::load(&id);
c.options.get(&name).unwrap_or(&"".to_owned()).to_owned()
}
#[inline]
#[cfg(feature = "flutter")]
pub fn get_peer_flutter_option(id: String, name: String) -> String {
let c = PeerConfig::load(&id);
c.ui_flutter.get(&name).unwrap_or(&"".to_owned()).to_owned()
}
#[inline]
#[cfg(feature = "flutter")]
pub fn set_peer_flutter_option(id: String, name: String, value: String) {
let mut c = PeerConfig::load(&id);
if value.is_empty() {
c.ui_flutter.remove(&name);
} else {
c.ui_flutter.insert(name, value);
}
c.store(&id);
}
#[inline]
pub fn set_peer_option(id: String, name: String, value: String) {
let mut c = PeerConfig::load(&id);
if value.is_empty() {
c.options.remove(&name);
} else {
c.options.insert(name, value);
}
c.store(&id);
}
#[inline]
pub fn get_options() -> String {
let options = {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
OPTIONS.lock().unwrap()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
Config::get_options()
}
};
let mut m = serde_json::Map::new();
for (k, v) in options.iter() {
m.insert(k.into(), v.to_owned().into());
}
serde_json::to_string(&m).unwrap_or_default()
}
#[inline]
pub fn test_if_valid_server(host: String, test_with_proxy: bool) -> String {
hbb_common::socket_client::test_if_valid_server(&host, test_with_proxy)
}
#[inline]
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_sound_inputs() -> Vec<String> {
let mut a = Vec::new();
#[cfg(not(target_os = "linux"))]
{
fn get_sound_inputs_() -> Vec<String> {
let mut out = Vec::new();
use cpal::traits::{DeviceTrait, HostTrait};
// Do not use `cpal::host_from_id(cpal::HostId::ScreenCaptureKit)` for feature = "screencapturekit"
// Because we explicitly handle the "System Sound" device.
let host = cpal::default_host();
if let Ok(devices) = host.devices() {
for device in devices {
if device.default_input_config().is_err() {
continue;
}
if let Ok(name) = device.name() {
out.push(name);
}
}
}
out
}
let inputs = Arc::new(Mutex::new(Vec::new()));
let cloned = inputs.clone();
// can not call below in UI thread, because conflict with sciter sound com initialization
std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs_())
.join()
.ok();
for name in inputs.lock().unwrap().drain(..) {
a.push(name);
}
}
#[cfg(target_os = "linux")]
{
let inputs: Vec<String> = crate::platform::linux::get_pa_sources()
.drain(..)
.map(|x| x.1)
.collect();
for name in inputs {
a.push(name);
}
}
a
}
#[inline]
pub fn set_options(m: HashMap<String, String>) {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
*OPTIONS.lock().unwrap() = m.clone();
ipc::set_options(m).ok();
}
#[cfg(any(target_os = "android", target_os = "ios"))]
Config::set_options(m);
}
#[inline]
pub fn set_option(key: String, value: String) {
if &key == "stop-service" {
#[cfg(target_os = "macos")]
{
let is_stop = value == "Y";
if is_stop && crate::platform::uninstall_service(true, false) {
return;
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
if crate::platform::is_installed() {
if value == "Y" {
if crate::platform::uninstall_service(true, false) {
return;
}
} else {
if crate::platform::install_service() {
return;
}
}
return;
}
}
} else if &key == "audio-input" {
#[cfg(not(target_os = "ios"))]
crate::audio_service::restart();
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let mut options = OPTIONS.lock().unwrap();
if value.is_empty() {
options.remove(&key);
} else {
options.insert(key.clone(), value.clone());
}
ipc::set_options(options.clone()).ok();
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let _nat = crate::CheckTestNatType::new();
Config::set_option(key, value);
}
}
#[inline]
pub fn install_path() -> String {
#[cfg(windows)]
return crate::platform::windows::get_install_info().1;
#[cfg(not(windows))]
return "".to_owned();
}
#[inline]
pub fn install_options() -> String {
#[cfg(windows)]
return crate::platform::windows::get_install_options();
#[cfg(not(windows))]
return "{}".to_owned();
}
#[inline]
pub fn get_socks() -> Vec<String> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let s = ipc::get_socks();
#[cfg(any(target_os = "android", target_os = "ios"))]
let s = Config::get_socks();
match s {
None => Vec::new(),
Some(s) => {
let mut v = Vec::new();
v.push(s.proxy);
v.push(s.username);
v.push(s.password);
v
}
}
}
#[inline]
pub fn set_socks(proxy: String, username: String, password: String) {
let socks = config::Socks5Server {
proxy,
username,
password,
};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
ipc::set_socks(socks).ok();
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let _nat = crate::CheckTestNatType::new();
if socks.proxy.is_empty() {
Config::set_socks(None);
} else {
Config::set_socks(Some(socks));
}
log::info!("socks updated");
}
#[cfg(target_os = "android")]
{
crate::RendezvousMediator::restart();
}
}
#[inline]
#[cfg(feature = "flutter")]
pub fn get_proxy_status() -> bool {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return ipc::get_proxy_status();
// Currently, only the desktop version has proxy settings.
#[cfg(any(target_os = "android", target_os = "ios"))]
return false;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[inline]
pub fn is_installed() -> bool {
crate::platform::is_installed()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
#[inline]
pub fn is_installed() -> bool {
false
}
#[inline]
pub fn is_share_rdp() -> bool {
#[cfg(windows)]
return crate::platform::windows::is_share_rdp();
#[cfg(not(windows))]
return false;
}
#[inline]
pub fn set_share_rdp(_enable: bool) {
#[cfg(windows)]
crate::platform::windows::set_share_rdp(_enable);
}
#[inline]
pub fn is_installed_lower_version() -> bool {
#[cfg(not(windows))]
return false;
#[cfg(windows)]
{
let b = crate::platform::windows::get_reg("BuildDate");
return crate::BUILD_DATE.cmp(&b).is_gt();
}
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_mouse_time() -> f64 {
UI_STATUS.lock().unwrap().mouse_time as f64
}
#[inline]
pub fn check_mouse_time() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let sender = SENDER.lock().unwrap();
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
}
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_connect_status() -> UiStatus {
UI_STATUS.lock().unwrap().clone()
}
#[inline]
pub fn temporary_password() -> String {
#[cfg(any(target_os = "android", target_os = "ios"))]
return password_security::temporary_password();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return TEMPORARY_PASSWD.lock().unwrap().clone();
}
#[inline]
pub fn update_temporary_password() {
#[cfg(any(target_os = "android", target_os = "ios"))]
password_security::update_temporary_password();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
allow_err!(ipc::update_temporary_password());
}
#[inline]
pub fn is_permanent_password_set() -> bool {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Config::has_permanent_password();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let daemon_is_set = ipc::is_permanent_password_set();
// `daemon_is_set` is authoritative for the return value. Local storage is only used to
// decide whether we should attempt a sync to clear stale user-side state.
let local_storage_is_empty = if daemon_is_set {
true
} else {
let (storage, _) = Config::get_local_permanent_password_storage_and_salt();
storage.is_empty()
};
if daemon_is_set || !local_storage_is_empty {
allow_err!(ipc::sync_permanent_password_storage_from_daemon());
}
daemon_is_set
}
}
#[inline]
pub fn is_local_permanent_password_set() -> bool {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Config::has_local_permanent_password();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
allow_err!(ipc::sync_permanent_password_storage_from_daemon());
Config::has_local_permanent_password()
}
}
pub fn set_permanent_password_with_result(password: String) -> bool {
if config::Config::is_disable_change_permanent_password() {
return false;
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
return config::Config::set_permanent_password(&password);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
match crate::ipc::set_permanent_password_with_ack(password) {
Ok(ok) => ok,
Err(err) => {
log::warn!("Failed to set permanent password via IPC: {err}");
false
}
}
}
}
#[inline]
pub fn get_peer(id: String) -> PeerConfig {
PeerConfig::load(&id)
}
#[inline]
pub fn get_fav() -> Vec<String> {
LocalConfig::get_fav()
}
#[inline]
pub fn store_fav(fav: Vec<String>) {
LocalConfig::set_fav(fav);
}
#[inline]
pub fn is_process_trusted(_prompt: bool) -> bool {
#[cfg(target_os = "macos")]
return crate::platform::macos::is_process_trusted(_prompt);
#[cfg(not(target_os = "macos"))]
return true;
}
#[inline]
pub fn is_can_screen_recording(_prompt: bool) -> bool {
#[cfg(target_os = "macos")]
return crate::platform::macos::is_can_screen_recording(_prompt);
#[cfg(not(target_os = "macos"))]
return true;
}
#[inline]
pub fn is_installed_daemon(_prompt: bool) -> bool {
#[cfg(target_os = "macos")]
return crate::platform::macos::is_installed_daemon(_prompt);
#[cfg(not(target_os = "macos"))]
return true;
}
#[inline]
#[cfg(feature = "flutter")]
pub fn is_can_input_monitoring(_prompt: bool) -> bool {
#[cfg(target_os = "macos")]
return crate::platform::macos::is_can_input_monitoring(_prompt);
#[cfg(not(target_os = "macos"))]
return true;
}
#[inline]
pub fn get_error() -> String {
#[cfg(target_os = "linux")]
{
let dtype = crate::platform::linux::get_display_server();
if crate::platform::linux::DISPLAY_SERVER_WAYLAND == dtype {
return crate::server::wayland::common_get_error();
}
if dtype != crate::platform::linux::DISPLAY_SERVER_X11 {
return format!(
"{} {}, {}",
crate::client::translate("Unsupported display server".to_owned()),
dtype,
crate::client::translate("x11 expected".to_owned()),
);
}
}
return "".to_owned();
}
#[inline]
pub fn is_login_wayland() -> bool {
#[cfg(target_os = "linux")]
return crate::platform::linux::is_login_wayland();
#[cfg(not(target_os = "linux"))]
return false;
}
#[inline]
pub fn current_is_wayland() -> bool {
#[cfg(target_os = "linux")]
return crate::platform::linux::current_is_wayland();
#[cfg(not(target_os = "linux"))]
return false;
}
#[inline]
pub fn get_new_version() -> String {
(*SOFTWARE_UPDATE_URL
.lock()
.unwrap()
.rsplit('/')
.next()
.unwrap_or(""))
.to_string()
}
#[inline]
pub fn get_version() -> String {
crate::VERSION.to_owned()
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn get_app_name() -> String {
crate::get_app_name()
}
#[cfg(windows)]
#[inline]
pub fn create_shortcut(_id: String) {
crate::platform::windows::create_shortcut(&_id).ok();
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn discover() {
std::thread::spawn(move || {
allow_err!(crate::lan::discover());
});
}
#[cfg(feature = "flutter")]
pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> {
use hbb_common::sodiumoxide::base64;
HashMap::<&str, String>::from_iter([
("id", id),
("username", p.info.username.clone()),
("hostname", p.info.hostname.clone()),
("platform", p.info.platform.clone()),
(
"alias",
p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(),
),
(
"hash",
base64::encode(p.password, base64::Variant::Original),
),
])
}
#[cfg(feature = "flutter")]
pub fn peer_exists(id: &str) -> bool {
PeerConfig::exists(id)
}
#[inline]
pub fn get_lan_peers() -> Vec<HashMap<&'static str, String>> {
config::LanPeers::load()
.peers
.iter()
.map(|peer| {
HashMap::<&str, String>::from_iter([
("id", peer.id.clone()),
("username", peer.username.clone()),
("hostname", peer.hostname.clone()),
("platform", peer.platform.clone()),
])
})
.collect()
}
#[inline]
pub fn remove_discovered(id: String) {
let mut peers = config::LanPeers::load().peers;
peers.retain(|x| x.id != id);
config::LanPeers::store(&peers);
}
#[inline]
pub fn get_uuid() -> String {
crate::encode64(hbb_common::get_uuid())
}
#[inline]
pub fn get_init_async_job_status() -> String {
INIT_ASYNC_JOB_STATUS.to_string()
}
#[inline]
pub fn reset_async_job_status() {
*ASYNC_JOB_STATUS.lock().unwrap() = get_init_async_job_status();
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn change_id(id: String) {
reset_async_job_status();
let old_id = get_id();
std::thread::spawn(move || {
change_id_shared(id, old_id);
});
}
#[inline]
pub fn http_request(url: String, method: String, body: Option<String>, header: String) {
// Respond to concurrent requests for resources
let current_request = ASYNC_HTTP_STATUS.clone();
current_request
.lock()
.unwrap()
.insert(url.clone(), " ".to_owned());
std::thread::spawn(move || {
let res = match crate::http_request_sync(url.clone(), method, body, header) {
Err(err) => {
log::error!("{}", err);
err.to_string()
}
Ok(text) => text,
};
current_request.lock().unwrap().insert(url, res);
});
}
#[inline]
pub fn get_async_http_status(url: String) -> Option<String> {
match ASYNC_HTTP_STATUS.lock().unwrap().get(&url) {
None => None,
Some(_str) => Some(_str.to_string()),
}
}
#[inline]
#[cfg(not(feature = "flutter"))]
pub fn post_request(url: String, body: String, header: String) {
*ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned();
std::thread::spawn(move || {
*ASYNC_JOB_STATUS.lock().unwrap() = match crate::post_request_sync(url, body, &header) {
Err(err) => err.to_string(),
Ok(text) => text,
};
});
}
#[inline]
pub fn get_async_job_status() -> String {
ASYNC_JOB_STATUS.lock().unwrap().clone()
}
#[inline]
pub fn get_langs() -> String {
use serde_json::json;
let hide_cjk = crate::lang::cjk_ui_unavailable();
let mut x: Vec<(&str, String)> = crate::lang::LANGS
.iter()
.filter(|a| !hide_cjk || !crate::lang::is_cjk_lang(a.0))
.map(|a| (a.0, format!("{} ({})", a.1, a.0)))
.collect();
x.sort_by(|a, b| a.0.cmp(b.0));
json!(x).to_string()
}
// Preserve relative paths for existing configurations and only remove accidental
// surrounding whitespace. Config values are not shell-expanded (for example, `~`).
fn trim_video_save_directory(value: &str) -> Option<&str> {
let value = value.trim();
if !value.is_empty() {
Some(value)
} else {
None
}
}
// A Windows service typically runs with System32 as its working directory, so
// require an absolute path to avoid resolving recordings there unexpectedly.
#[cfg(any(windows, test))]
fn validate_windows_service_video_save_directory(value: &str) -> Option<&str> {
let value = trim_video_save_directory(value)?;
if std::path::Path::new(value).is_absolute() {
Some(value)
} else {
None
}
}
#[inline]
pub fn video_save_directory(root: bool) -> String {
let appname = crate::get_app_name();
// ui process can show it correctly Once vidoe process created it.
let try_create = |path: &std::path::Path| {
if !path.exists() {
std::fs::create_dir_all(path).ok();
}
if path.exists() {
path.to_string_lossy().to_string()
} else {
"".to_string()
}
};
if root {
// Currently, only installed windows run as root
#[cfg(windows)]
{
let dir = Config::get_option(OPTION_WINDOWS_SERVICE_VIDEO_SAVE_DIRECTORY);
if let Some(dir) = validate_windows_service_video_save_directory(&dir) {
return dir.to_owned();
}
if !dir.trim().is_empty() {
log::warn!(
"Ignoring {OPTION_WINDOWS_SERVICE_VIDEO_SAVE_DIRECTORY}: path must be absolute"
);
}
let drive = std::env::var("SystemDrive").unwrap_or("C:".to_owned());
let dir =
std::path::PathBuf::from(format!("{drive}\\ProgramData\\{appname}\\recording",));
return dir.to_string_lossy().to_string();
}
}
// Get directory from config file otherwise --server will use the old value from global var.
#[cfg(any(target_os = "linux", target_os = "macos"))]
let dir = LocalConfig::get_option_from_file(OPTION_VIDEO_SAVE_DIRECTORY);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let dir = LocalConfig::get_option(OPTION_VIDEO_SAVE_DIRECTORY);
if let Some(dir) = trim_video_save_directory(&dir) {
return dir.to_owned();
}
#[cfg(any(target_os = "android", target_os = "ios"))]
if let Ok(home) = config::APP_HOME_DIR.read() {
let mut path = home.to_owned();
path.push_str(format!("/{appname}/ScreenRecord").as_str());
let dir = try_create(&std::path::Path::new(&path));
if !dir.is_empty() {
return dir;
}
}
if let Some(user) = directories_next::UserDirs::new() {
if let Some(video_dir) = user.video_dir() {
let dir = try_create(&video_dir.join(&appname));
if !dir.is_empty() {
return dir;
}
if video_dir.exists() {
return video_dir.to_string_lossy().to_string();
}
}
if let Some(desktop_dir) = user.desktop_dir() {
if desktop_dir.exists() {
return desktop_dir.to_string_lossy().to_string();
}
}
let home = user.home_dir();
if home.exists() {
return home.to_string_lossy().to_string();
}
}
// same order as above
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(home) = crate::platform::get_active_user_home() {
let name = if cfg!(target_os = "macos") {
"Movies"
} else {
"Videos"
};
let video_dir = home.join(name);
let dir = try_create(&video_dir.join(&appname));
if !dir.is_empty() {
return dir;
}
if video_dir.exists() {
return video_dir.to_string_lossy().to_string();
}
let desktop_dir = home.join("Desktop");
if desktop_dir.exists() {
return desktop_dir.to_string_lossy().to_string();
}
if home.exists() {
return home.to_string_lossy().to_string();
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
let dir = try_create(&parent.join("videos"));
if !dir.is_empty() {
return dir;
}
// basically exist
return parent.to_string_lossy().to_string();
}
}
Default::default()
}
#[inline]
pub fn get_api_server() -> String {
crate::get_api_server(
get_option("api-server"),
get_option("custom-rendezvous-server"),
)
}
pub enum DeployResult {
Ok,
NotEnabled,
InvalidInput,
IdTaken(String),
Error(String),
}
impl DeployResult {
pub fn message(&self) -> String {
match self {
Self::Ok => "".to_owned(),
Self::NotEnabled => "The server does not require explicit deployment.".to_owned(),
Self::InvalidInput => "Invalid input.".to_owned(),
Self::IdTaken(id) => {
format!(
"Id `{}` is already used by another machine on the server.",
id
)
}
Self::Error(err) => err.clone(),
}
}
}
pub fn deploy_device(token: String, new_id: Option<String>) -> DeployResult {
if Config::no_register_device() {
return DeployResult::Error("Cannot deploy an unregistrable device!".to_owned());
}
let token = token.trim();
if token.is_empty() {
return DeployResult::Error("token is required!".to_owned());
}
#[cfg(any(target_os = "android", target_os = "ios"))]
let local_id = Config::get_id();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let local_id = ipc::get_id();
let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone());
let uuid = crate::encode64(hbb_common::get_uuid());
let pk = crate::encode64(Config::get_key_pair().1);
let body = serde_json::json!({
"id": id_to_deploy,
"uuid": uuid,
"pk": pk,
});
let header = "Authorization: Bearer ".to_owned() + token;
let url = get_api_server() + "/api/devices/deploy";
let text = match crate::post_request_sync(url, body.to_string(), &header) {
Ok(text) => text,
Err(err) => return DeployResult::Error(format!("Request failed: {}", err)),
};
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
match parsed["result"].as_str().unwrap_or("") {
"OK" => {
if let Some(new_id) = new_id {
if new_id != local_id {
#[cfg(any(target_os = "android", target_os = "ios"))]
{
Config::set_key_confirmed(false);
Config::set_id(&new_id);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Err(err) = ipc::set_config("id", new_id) {
return DeployResult::Error(format!(
"Failed to persist deployed id locally: {}",
err
));
}
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Err(err) = ipc::notify_deployed() {
log::warn!("Failed to notify deployed state: {}", err);
}
#[cfg(target_os = "android")]
{
crate::rendezvous_mediator::NEEDS_DEPLOY
.store(false, std::sync::atomic::Ordering::SeqCst);
crate::rendezvous_mediator::reset_needs_deploy_notification();
crate::rendezvous_mediator::RendezvousMediator::restart();
}
DeployResult::Ok
}
"NOT_ENABLED" => DeployResult::NotEnabled,
"INVALID_INPUT" => DeployResult::InvalidInput,
"ID_TAKEN" => DeployResult::IdTaken(id_to_deploy),
_ => {
if text.is_empty() {
DeployResult::Error("Unknown response.".to_owned())
} else {
DeployResult::Error(text)
}
}
}
}
#[inline]
pub fn has_hwcodec() -> bool {
// Has real hardware codec using gpu
(cfg!(feature = "hwcodec") && cfg!(not(target_os = "ios"))) || cfg!(feature = "mediacodec")
}
#[inline]
pub fn has_vram() -> bool {
cfg!(feature = "vram")
}
#[cfg(feature = "flutter")]
#[inline]
pub fn supported_hwdecodings() -> (bool, bool) {
let decoding =
scrap::codec::Decoder::supported_decodings(None, use_texture_render(), None, &vec![]);
#[allow(unused_mut)]
let (mut h264, mut h265) = (decoding.ability_h264 > 0, decoding.ability_h265 > 0);
#[cfg(feature = "vram")]
{
// supported_decodings check runtime luid
let vram = scrap::vram::VRamDecoder::possible_available_without_check();
if vram.0 {
h264 = true;
}
if vram.1 {
h265 = true;
}
}
(h264, h265)
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[inline]
pub fn is_root() -> bool {
crate::platform::is_root()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
#[inline]
pub fn is_root() -> bool {
false
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
#[inline]
pub fn check_super_user_permission() -> bool {
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
return crate::platform::check_super_user_permission().unwrap_or(false);
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
return true;
}
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
pub fn check_zombie() {
let mut deads = Vec::new();
loop {
let mut lock = CHILDREN.lock().unwrap();
let mut n = 0;
for (id, c) in lock.1.iter_mut() {
if let Ok(Some(_)) = c.try_wait() {
deads.push(id.clone());
n += 1;
}
}
for ref id in deads.drain(..) {
lock.1.remove(id);
}
if n > 0 {
lock.0 = true;
}
drop(lock);
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
pub fn recent_sessions_updated() -> bool {
let mut children = CHILDREN.lock().unwrap();
if children.0 {
children.0 = false;
true
} else {
false
}
}
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
pub fn new_remote(id: String, remote_type: String, force_relay: bool) {
let mut lock = CHILDREN.lock().unwrap();
let mut args = vec![format!("--{}", remote_type), id.clone()];
if force_relay {
args.push("".to_string()); // password
args.push("--relay".to_string());
}
let key = (id.clone(), remote_type.clone());
if let Some(c) = lock.1.get_mut(&key) {
if let Ok(Some(_)) = c.try_wait() {
lock.1.remove(&key);
} else {
if remote_type == "rdp" {
allow_err!(c.kill());
std::thread::sleep(std::time::Duration::from_millis(30));
c.try_wait().ok();
lock.1.remove(&key);
} else {
return;
}
}
}
match crate::run_me(args) {
Ok(child) => {
lock.1.insert(key, child);
}
Err(err) => {
log::error!("Failed to spawn remote: {}", err);
}
}
}
// Make sure `SENDER` is inited here.
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn start_option_status_sync() {
let _sender = SENDER.lock().unwrap();
}
// not call directly
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender<ipc::Data> {
let (tx, rx) = mpsc::unbounded_channel::<ipc::Data>();
std::thread::spawn(move || check_connect_status_(reconnect, rx));
tx
}
#[cfg(feature = "flutter")]
pub fn account_auth(op: String, id: String, uuid: String, remember_me: bool) {
account::OidcSession::account_auth(get_api_server(), op, id, uuid, remember_me);
}
#[cfg(feature = "flutter")]
pub fn account_auth_cancel() {
account::OidcSession::auth_cancel();
}
#[cfg(feature = "flutter")]
pub fn account_auth_result() -> String {
serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default()
}
#[cfg(feature = "flutter")]
pub fn set_user_default_option(key: String, value: String) {
use hbb_common::config::UserDefaultConfig;
UserDefaultConfig::load().set(key, value);
}
#[cfg(feature = "flutter")]
pub fn get_user_default_option(key: String) -> String {
use hbb_common::config::UserDefaultConfig;
UserDefaultConfig::load().get(&key)
}
pub fn get_fingerprint() -> String {
#[cfg(any(target_os = "android", target_os = "ios"))]
if Config::get_key_confirmed() {
return crate::common::pk_to_fingerprint(Config::get_key_pair().1);
} else {
return "".to_owned();
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return ipc::get_fingerprint();
}
#[inline]
pub fn get_login_device_info() -> LoginDeviceInfo {
LoginDeviceInfo {
// std::env::consts::OS is better than whoami::platform() here.
os: std::env::consts::OS.to_owned(),
r#type: "client".to_owned(),
name: crate::common::hostname(),
}
}
#[inline]
pub fn get_login_device_info_json() -> String {
serde_json::to_string(&get_login_device_info()).unwrap_or("{}".to_string())
}
// notice: avoiding create ipc connection repeatedly,
// because windows named pipe has serious memory leak issue.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tokio::main(flavor = "current_thread")]
async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver<ipc::Data>) {
#[cfg(not(feature = "flutter"))]
let mut key_confirmed = false;
let mut rx = rx;
let mut mouse_time = 0;
#[cfg(feature = "flutter")]
let mut video_conn_count = 0;
#[cfg(not(feature = "flutter"))]
let mut id = "".to_owned();
let is_cm = crate::common::is_cm();
loop {
if let Ok(mut c) = ipc::connect(1000, "").await {
let mut timer = crate::rustdesk_interval(time::interval(time::Duration::from_secs(1)));
loop {
tokio::select! {
res = c.next() => {
match res {
Err(err) => {
log::error!("ipc connection closed: {}", err);
if is_cm {
crate::ui_cm_interface::quit_cm();
}
break;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Ok(Some(ipc::Data::MouseMoveTime(v))) => {
mouse_time = v;
UI_STATUS.lock().unwrap().mouse_time = v;
}
Ok(Some(ipc::Data::Options(Some(v)))) => {
*OPTIONS.lock().unwrap() = v;
*OPTION_SYNCED.lock().unwrap() = true;
}
Ok(Some(ipc::Data::Config((name, Some(value))))) => {
if name == "id" {
#[cfg(not(feature = "flutter"))]
{
id = value;
}
} else if name == "temporary-password" {
*TEMPORARY_PASSWD.lock().unwrap() = value;
}
}
#[cfg(feature = "flutter")]
Ok(Some(ipc::Data::VideoConnCount(Some(n)))) => {
video_conn_count = n;
}
Ok(Some(ipc::Data::OnlineStatus(Some((mut x, _c))))) => {
if x > 0 {
x = 1
}
#[cfg(not(feature = "flutter"))]
{
key_confirmed = _c;
}
*UI_STATUS.lock().unwrap() = UiStatus {
status_num: x as _,
#[cfg(not(feature = "flutter"))]
key_confirmed: _c,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mouse_time,
#[cfg(not(feature = "flutter"))]
id: id.clone(),
#[cfg(feature = "flutter")]
video_conn_count,
};
}
Ok(Some(ipc::Data::ControlPermissionsRemoteModify(v))) => {
*IS_REMOTE_MODIFY_ENABLED_BY_CONTROL_PERMISSIONS.lock().unwrap() = v;
}
#[cfg(target_os = "windows")]
Ok(Some(ipc::Data::FileTransferEnabledState(v))) => {
if let Some(enabled) = v {
let mut lock = IS_FILE_TRANSFER_ENABLED.lock().unwrap();
if *lock != v {
clipboard::ContextSend::enable(enabled);
*lock = v;
}
}
}
_ => {}
}
}
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();
c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok();
#[cfg(feature = "flutter")]
c.send(&ipc::Data::VideoConnCount(None)).await.ok();
c.send(&ipc::Data::ControlPermissionsRemoteModify(None)).await.ok();
#[cfg(target_os = "windows")]
c.send(&ipc::Data::FileTransferEnabledState(None)).await.ok();
}
}
}
}
if !reconnect {
OPTIONS
.lock()
.unwrap()
.insert("ipc-closed".to_owned(), "Y".to_owned());
break;
}
*UI_STATUS.lock().unwrap() = UiStatus {
status_num: -1,
#[cfg(not(feature = "flutter"))]
key_confirmed,
#[cfg(not(any(target_os = "android", target_os = "ios")))]
mouse_time,
#[cfg(not(feature = "flutter"))]
id: id.clone(),
#[cfg(feature = "flutter")]
video_conn_count,
};
sleep(1.).await;
}
}
#[allow(dead_code)]
pub fn option_synced() -> bool {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
OPTION_SYNCED.lock().unwrap().clone()
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
true
}
}
#[cfg(any(target_os = "android", feature = "flutter"))]
#[cfg(not(any(target_os = "ios")))]
#[tokio::main(flavor = "current_thread")]
pub(crate) async fn send_to_cm(data: &ipc::Data) {
if let Ok(mut c) = ipc::connect(1000, "_cm").await {
c.send(data).await.ok();
}
}
const INVALID_FORMAT: &'static str = "Invalid format";
const UNKNOWN_ERROR: &'static str = "Unknown error";
#[inline]
#[tokio::main(flavor = "current_thread")]
pub async fn change_id_shared(id: String, old_id: String) -> String {
let res = change_id_shared_(id, old_id).await.to_owned();
*ASYNC_JOB_STATUS.lock().unwrap() = res.clone();
res
}
pub async fn change_id_shared_(id: String, old_id: String) -> &'static str {
if !hbb_common::is_valid_custom_id(&id) {
log::debug!(
"debugging invalid id: \"{id}\", len: {}, base64: \"{}\"",
id.len(),
crate::encode64(&id)
);
let bom = id.trim_start_matches('\u{FEFF}');
log::debug!("bom: {}", hbb_common::is_valid_custom_id(&bom));
return INVALID_FORMAT;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let uuid = Bytes::from(
hbb_common::machine_uid::get()
.unwrap_or("".to_owned())
.as_bytes()
.to_vec(),
);
#[cfg(any(target_os = "android", target_os = "ios"))]
let uuid = Bytes::from(hbb_common::get_uuid());
if uuid.is_empty() {
log::error!("Failed to change id, uuid is_empty");
return UNKNOWN_ERROR;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await;
#[cfg(any(target_os = "android", target_os = "ios"))]
let rendezvous_servers = Config::get_rendezvous_servers();
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() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::ipc::set_config_async("id", id.to_owned()).await.ok();
#[cfg(any(target_os = "android", target_os = "ios"))]
{
Config::set_key_confirmed(false);
Config::set_id(&id);
}
}
err
}
async fn check_id(
rendezvous_server: String,
old_id: String,
id: String,
uuid: Bytes,
) -> &'static str {
if let Ok(mut socket) = hbb_common::socket_client::connect_tcp(
crate::check_port(rendezvous_server, RENDEZVOUS_PORT),
CONNECT_TIMEOUT,
)
.await
{
let mut msg_out = Message::new();
msg_out.set_register_pk(RegisterPk {
old_id,
id,
uuid,
..Default::default()
});
let mut ok = false;
if socket.send(&msg_out).await.is_ok() {
if let Some(msg_in) =
crate::common::get_next_nonkeyexchange_msg(&mut socket, None).await
{
match msg_in.union {
Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => {
match rpr.result.enum_value() {
Ok(register_pk_response::Result::OK) => {
ok = true;
}
Ok(register_pk_response::Result::ID_EXISTS) => {
return "Not available";
}
Ok(register_pk_response::Result::TOO_FREQUENT) => {
return "Too frequent";
}
Ok(register_pk_response::Result::NOT_SUPPORT) => {
return "server_not_support";
}
Ok(register_pk_response::Result::SERVER_ERROR) => {
return "Server error";
}
Ok(register_pk_response::Result::INVALID_ID_FORMAT) => {
return INVALID_FORMAT;
}
_ => {}
}
}
_ => {}
}
}
}
if !ok {
return UNKNOWN_ERROR;
}
} else {
return "Failed to connect to rendezvous server";
}
""
}
// if it's relay id, return id processed, otherwise return original id
pub fn handle_relay_id(id: &str) -> &str {
if id.ends_with(r"\r") || id.ends_with(r"/r") {
&id[0..id.len() - 2]
} else {
id
}
}
pub fn support_remove_wallpaper() -> bool {
#[cfg(any(target_os = "windows", target_os = "linux"))]
return crate::platform::WallPaperRemover::support();
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
return false;
}
pub fn has_valid_2fa() -> bool {
let raw = get_option("2fa");
crate::auth_2fa::get_2fa(Some(raw)).is_some()
}
pub fn generate2fa() -> String {
crate::auth_2fa::generate2fa()
}
pub fn verify2fa(code: String) -> bool {
let res = crate::auth_2fa::verify2fa(code);
if res {
refresh_options();
}
res
}
pub fn has_valid_bot() -> bool {
crate::auth_2fa::TelegramBot::get().map_or(false, |bot| bot.is_some())
}
pub fn verify_bot(token: String) -> String {
match crate::auth_2fa::get_chatid_telegram(&token) {
Err(err) => err.to_string(),
Ok(None) => {
"To activate the bot, simply send a message beginning with a forward slash (\"/\") like \"/hello\" to its chat.".to_owned()
}
_ => "".to_owned(),
}
}
pub fn check_hwcodec() {
#[cfg(feature = "hwcodec")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
if crate::platform::is_installed() {
ipc::notify_server_to_check_hwcodec().ok();
ipc::client_get_hwcodec_config_thread(3);
} else {
scrap::hwcodec::start_check_process();
}
})
}
}
#[cfg(feature = "flutter")]
pub fn get_unlock_pin() -> String {
#[cfg(any(target_os = "android", target_os = "ios"))]
return String::default();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return ipc::get_unlock_pin();
}
#[cfg(feature = "flutter")]
pub fn set_unlock_pin(pin: String) -> String {
#[cfg(any(target_os = "android", target_os = "ios"))]
return String::default();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
match ipc::set_unlock_pin(pin, true) {
Ok(_) => String::default(),
Err(err) => err.to_string(),
}
}
#[cfg(feature = "flutter")]
pub fn get_trusted_devices() -> String {
#[cfg(any(target_os = "android", target_os = "ios"))]
return Config::get_trusted_devices_json();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return ipc::get_trusted_devices();
}
#[cfg(feature = "flutter")]
pub fn remove_trusted_devices(json: &str) {
let hwids = serde_json::from_str::<Vec<Bytes>>(json).unwrap_or_default();
#[cfg(any(target_os = "android", target_os = "ios"))]
Config::remove_trusted_devices(&hwids);
#[cfg(not(any(target_os = "android", target_os = "ios")))]
ipc::remove_trusted_devices(hwids);
}
#[cfg(feature = "flutter")]
pub fn clear_trusted_devices() {
#[cfg(any(target_os = "android", target_os = "ios"))]
Config::clear_trusted_devices();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
ipc::clear_trusted_devices();
}
#[cfg(feature = "flutter")]
pub fn max_encrypt_len() -> usize {
hbb_common::config::ENCRYPT_MAX_LEN
}
pub fn is_remote_modify_enabled_by_control_permissions() -> Option<bool> {
*IS_REMOTE_MODIFY_ENABLED_BY_CONTROL_PERMISSIONS
.lock()
.unwrap()
}
#[cfg(test)]
mod tests {
use super::{trim_video_save_directory, validate_windows_service_video_save_directory};
#[test]
fn trim_configured_video_save_directory() {
assert_eq!(
trim_video_save_directory(" relative/recordings "),
Some("relative/recordings")
);
assert_eq!(trim_video_save_directory(" "), None);
}
#[test]
fn validate_service_video_save_directory() {
let absolute = if cfg!(windows) {
r"C:\recordings"
} else {
"/recordings"
};
let padded = format!(" {absolute} ");
assert_eq!(
validate_windows_service_video_save_directory(&padded),
Some(absolute)
);
assert_eq!(
validate_windows_service_video_save_directory("recordings"),
None
);
assert_eq!(
validate_windows_service_video_save_directory(&format!("\"{absolute}\"")),
None
);
assert_eq!(validate_windows_service_video_save_directory(" "), None);
}
}