From b50fde6910f6f5910ea96c0cade0736e7129dee3 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 8 Sep 2026 11:46:42 +0800 Subject: [PATCH] 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 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 Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Opus 5 --- AGENTS.md | 40 +- Cargo.lock | 33 +- Cargo.toml | 3 +- README.md | 3 +- flutter/android/app/build.gradle | 2 +- libs/base/Cargo.toml | 57 + libs/base/build.rs | 14 + libs/base/examples/system_message.rs | 20 + libs/base/protos/message.proto | 1023 ++++++++++ libs/base/src/config/keys.rs | 403 ++++ libs/base/src/config/mod.rs | 1 + libs/base/src/fs.rs | 1810 +++++++++++++++++ libs/base/src/keyboard.rs | 39 + libs/base/src/lib.rs | 7 + libs/base/src/platform/linux.rs | 618 ++++++ libs/base/src/platform/linux/wayland_probe.rs | 349 ++++ libs/base/src/platform/macos.rs | 55 + libs/base/src/platform/mod.rs | 82 + libs/base/src/platform/windows.rs | 198 ++ libs/base/src/protos/mod.rs | 1 + libs/clipboard/Cargo.toml | 1 + libs/clipboard/src/platform/unix/filetype.rs | 6 +- .../src/platform/unix/macos/paste_task.rs | 3 +- libs/enigo/Cargo.toml | 1 + libs/enigo/src/linux/nix_impl.rs | 2 +- libs/hbb_common | 2 +- libs/scrap/Cargo.toml | 3 +- libs/scrap/src/android/ffi.rs | 3 +- libs/scrap/src/common/aom.rs | 5 +- libs/scrap/src/common/camera.rs | 2 +- libs/scrap/src/common/codec.rs | 22 +- libs/scrap/src/common/hwcodec.rs | 2 +- libs/scrap/src/common/mod.rs | 9 +- libs/scrap/src/common/record.rs | 7 +- libs/scrap/src/common/vpxcodec.rs | 2 +- libs/scrap/src/common/vram.rs | 7 +- libs/scrap/src/wayland/display.rs | 4 +- libs/scrap/src/wayland/pipewire.rs | 3 +- src/client.rs | 13 +- src/client/file_trait.rs | 3 +- src/client/helper.rs | 6 +- src/client/io_loop.rs | 37 +- src/client/screenshot.rs | 3 +- src/clipboard.rs | 11 +- src/clipboard_file.rs | 2 +- src/common.rs | 6 +- src/core_main.rs | 7 +- src/flutter.rs | 5 +- src/flutter_ffi.rs | 44 +- src/hbbs_http/sync.rs | 3 +- src/ipc.rs | 15 +- src/keyboard.rs | 2 +- src/platform/gtk_sudo.rs | 5 +- src/platform/linux.rs | 6 +- src/platform/macos.rs | 2 +- src/platform/mod.rs | 4 +- src/platform/windows.rs | 9 +- src/port_forward.rs | 8 +- src/port_forward_mux.rs | 6 +- src/privacy_mode/win_exclude_from_capture.rs | 2 +- src/rendezvous_mediator.rs | 5 +- src/server.rs | 4 +- src/server/clipboard_service.rs | 4 +- src/server/connection.rs | 11 +- src/server/display_service.rs | 2 +- src/server/drm_capturer.rs | 11 +- src/server/input_service.rs | 9 +- src/server/port_forward_mux.rs | 4 +- src/server/portable_service.rs | 7 +- src/server/rdp_input.rs | 2 +- src/server/video_service.rs | 2 +- src/server/wayland.rs | 3 +- src/tray.rs | 7 +- src/ui/remote.rs | 5 +- src/ui_cm_interface.rs | 32 +- src/ui_interface.rs | 11 +- src/ui_session_interface.rs | 12 +- src/updater.rs | 5 +- src/virtual_display_manager.rs | 3 +- 79 files changed, 4966 insertions(+), 214 deletions(-) create mode 100644 libs/base/Cargo.toml create mode 100644 libs/base/build.rs create mode 100644 libs/base/examples/system_message.rs create mode 100644 libs/base/protos/message.proto create mode 100644 libs/base/src/config/keys.rs create mode 100644 libs/base/src/config/mod.rs create mode 100644 libs/base/src/fs.rs create mode 100644 libs/base/src/keyboard.rs create mode 100644 libs/base/src/lib.rs create mode 100644 libs/base/src/platform/linux.rs create mode 100644 libs/base/src/platform/linux/wayland_probe.rs create mode 100644 libs/base/src/platform/macos.rs create mode 100644 libs/base/src/platform/mod.rs create mode 100644 libs/base/src/platform/windows.rs create mode 100644 libs/base/src/protos/mod.rs diff --git a/AGENTS.md b/AGENTS.md index 1e4c6782e..1226c40c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,18 +8,24 @@ * `src/platform/` platform-specific code * `src/ui/` legacy Sciter UI (deprecated) * `flutter/` current UI -* `libs/hbb_common/` config / proto / shared utils +* `libs/hbb_common/` shared with the server: rendezvous proto, sockets, `Config` core +* `libs/base/` (crate `base`) client-only: option keys, message proto, file transfer, platform code * `libs/scrap/` screen capture * `libs/enigo/` input control * `libs/clipboard/` clipboard -* `libs/hbb_common/src/config.rs` all options +* `libs/base/src/config/keys.rs` the single import path for all options ### Key Components - **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server - **Screen Capture**: Platform-specific screen capture in `libs/scrap/` - **Input Handling**: Cross-platform input simulation in `libs/enigo/` - **Audio/Video Services**: Real-time audio/video streaming in `src/server/` -- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` +- **File Transfer**: Secure file transfer implementation in `libs/base/src/fs.rs` + +`hbb_common` is a git submodule shared with the server, so changing it costs a +round-trip. Put client-only code in `libs/base` instead; it is a normal +workspace member. `base::config::keys` re-exports the handful of keys +`hbb_common` still reads, so callers get the whole set from that one path. ### UI Architecture - **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` @@ -61,6 +67,34 @@ * Do not make formatting-only changes. * Keep naming/style consistent with nearby code. +### Imports + +* One `use` per crate. Everything a file takes from the same crate goes in a + single braced block, not one statement per item: + + ```rust + // no + use base::fs; + use base::message_proto::*; + + // yes + use base::{fs, message_proto::*}; + ``` + +* The only reason to split is a `#[cfg(...)]` that does not apply to the whole + block -- an attribute binds to one item, so a differently-gated import has to + stand on its own. A `pub use` re-export likewise cannot join a plain `use`. + + ```rust + #[cfg(not(feature = "flutter"))] + use base::fs; + use base::message_proto::*; + ``` + +* When splitting an existing `use` because some of its items moved to another + crate, fold each side into that crate's existing block rather than leaving a + second statement behind. + ### Comments * Avoid comments unless they explain a non-obvious reason, constraint, or workaround. diff --git a/Cargo.lock b/Cargo.lock index 902036cb3..0e9f071d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,30 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "bytes", + "filetime", + "hbb_common", + "lazy_static", + "libc", + "log", + "osascript", + "protobuf", + "protobuf-codegen", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", + "smithay-client-toolkit 0.20.0", + "tokio", + "users", + "winapi 0.3.9", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -1251,6 +1275,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" name = "clipboard" version = "0.1.0" dependencies = [ + "base", "cacao", "cc", "dashmap 5.5.3", @@ -2477,6 +2502,7 @@ dependencies = [ name = "enigo" version = "0.0.14" dependencies = [ + "base", "core-graphics 0.22.3", "hbb_common", "libxdo-sys", @@ -3664,7 +3690,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion", - "backtrace", "base64 0.22.1", "bytes", "chrono", @@ -3675,7 +3700,6 @@ dependencies = [ "dirs-next", "dlopen", "env_logger 0.11.6", - "filetime", "flexi_logger", "futures", "futures-util", @@ -3686,7 +3710,6 @@ dependencies = [ "log", "mac_address", "machine-uid", - "osascript", "percent-encoding", "protobuf", "protobuf-codegen", @@ -3699,7 +3722,6 @@ dependencies = [ "serde_derive", "serde_json 1.0.118", "sha2", - "smithay-client-toolkit 0.20.0", "socket2 0.3.19", "sodiumoxide", "sysinfo", @@ -3718,7 +3740,6 @@ dependencies = [ "webpki-roots 1.0.9", "webrtc", "whoami", - "winapi 0.3.9", "x11 2.21.0", "zstd", ] @@ -7076,6 +7097,7 @@ dependencies = [ "arboard", "async-process", "async-trait", + "base", "bytemuck", "bytes", "cc", @@ -7396,6 +7418,7 @@ name = "scrap" version = "0.5.0" dependencies = [ "android_logger", + "base", "bindgen 0.72.1", "block", "cfg-if 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 0894163b2..393619136 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ screencapturekit = ["cpal/screencapturekit"] async-trait = "0.1" scrap = { path = "libs/scrap", features = ["wayland"] } hbb_common = { path = "libs/hbb_common", features = ["webrtc"] } +base = { path = "libs/base" } serde_derive = "1.0" serde = "1.0" serde_json = "1.0" @@ -208,7 +209,7 @@ jni = "0.21" android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] +members = ["libs/scrap", "libs/hbb_common", "libs/base", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] exclude = ["vdi/host"] # Patch libxdo-sys to use a stub implementation that doesn't require libxdo diff --git a/README.md b/README.md index a593a191f..7508ce310 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,8 @@ Please ensure that you run these commands from the root of the RustDesk reposito ## File Structure -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, and some other utility functions shared with the server +- **[libs/base](https://github.com/rustdesk/rustdesk/tree/master/libs/base)**: protobuf, fs functions for file transfer, keyboard and platform code used only by this app - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control - **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: file copy and paste implementation for Windows, Linux, macOS. diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 44eb32ca0..07f029cf5 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -87,7 +87,7 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' - main.proto.srcDirs += '../../../libs/hbb_common/protos' + main.proto.srcDirs += '../../../libs/base/protos' main.proto.includes += "message.proto" } diff --git a/libs/base/Cargo.toml b/libs/base/Cargo.toml new file mode 100644 index 000000000..2890f00e3 --- /dev/null +++ b/libs/base/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "base" +version = "0.1.0" +authors = ["rustdesk "] +edition = "2018" + +# Code that only RustDesk itself uses. `hbb_common` stays the crate shared with +# the server, so anything the server never touches belongs here instead. + +[features] +default = [] +# The isolated Wayland socket-probe fallback (src/platform/linux/wayland_probe.rs). +# Off by default so the base Wayland enumeration is untouched; the DRM login-screen +# build (scrap/drm) turns it on. +wayland_probe = [] + +[dependencies] +hbb_common = { path = "../hbb_common" } +protobuf = { version = "3.7", features = ["with-bytes"] } +# the generated protobuf code refers to `::bytes::Bytes` (tokio_bytes codegen) +bytes = { version = "1.10", features = ["serde"] } +tokio = { version = "1.44", features = ["full"] } +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +filetime = "0.2" +libc = "0.2" +backtrace = "0.3" +log = "0.4" +lazy_static = "1.5" +anyhow = "1.0" + +[build-dependencies] +protobuf-codegen = { version = "3.7" } + +[target.'cfg(target_os = "windows")'.dependencies] +# Every module the moved sources name, spelled out rather than left to feature +# unification with the root crate. +winapi = { version = "0.3", features = [ + "fileapi", + "handleapi", + "minwindef", + "pdh", + "synchapi", + "sysinfoapi", + "winbase", + "winnt", +] } + +[target.'cfg(target_os = "macos")'.dependencies] +osascript = "0.3" + +[target.'cfg(target_os = "linux")'.dependencies] +sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-features = false, features = [ + "calloop", +] } +users = { version = "0.11" } diff --git a/libs/base/build.rs b/libs/base/build.rs new file mode 100644 index 000000000..96deaa15b --- /dev/null +++ b/libs/base/build.rs @@ -0,0 +1,14 @@ +fn main() { + let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); + + std::fs::create_dir_all(&out_dir).unwrap(); + + protobuf_codegen::Codegen::new() + .pure() + .out_dir(out_dir) + .inputs(["protos/message.proto"]) + .include("protos") + .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) + .run() + .expect("Codegen failed."); +} diff --git a/libs/base/examples/system_message.rs b/libs/base/examples/system_message.rs new file mode 100644 index 000000000..e4446e807 --- /dev/null +++ b/libs/base/examples/system_message.rs @@ -0,0 +1,20 @@ +extern crate base; +#[cfg(target_os = "linux")] +use base::platform::linux; +#[cfg(target_os = "macos")] +use base::platform::macos; + +fn main() { + #[cfg(target_os = "linux")] + let res = linux::system_message("test title", "test message", true); + #[cfg(target_os = "macos")] + let res = macos::alert( + "System Preferences".to_owned(), + "warning".to_owned(), + "test title".to_owned(), + "test message".to_owned(), + ["Ok".to_owned()].to_vec(), + ); + #[cfg(any(target_os = "linux", target_os = "macos"))] + println!("result {:?}", &res); +} diff --git a/libs/base/protos/message.proto b/libs/base/protos/message.proto new file mode 100644 index 000000000..1276deaa8 --- /dev/null +++ b/libs/base/protos/message.proto @@ -0,0 +1,1023 @@ +syntax = "proto3"; +package hbb; + +message EncodedVideoFrame { + bytes data = 1; + bool key = 2; + int64 pts = 3; +} + +message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } + +message RGB { bool compress = 1; } + +// planes data send directly in binary for better use arraybuffer on web +message YUV { + bool compress = 1; + int32 stride = 2; +} + +enum Chroma { + I420 = 0; + I444 = 1; +} + +message VideoFrame { + oneof union { + EncodedVideoFrames vp9s = 6; + RGB rgb = 7; + YUV yuv = 8; + EncodedVideoFrames h264s = 10; + EncodedVideoFrames h265s = 11; + EncodedVideoFrames vp8s = 12; + EncodedVideoFrames av1s = 13; + } + int32 display = 14; +} + +message DisplayInfo { + sint32 x = 1; + sint32 y = 2; + int32 width = 3; + int32 height = 4; + string name = 5; + bool online = 6; + bool cursor_embedded = 7; + Resolution original_resolution = 8; + double scale = 9; +} + +message PortForward { + string host = 1; + int32 port = 2; + bool multiplex = 3; +} + +message FileTransfer { + string dir = 1; + bool show_hidden = 2; +} + +message ViewCamera {} + +message OSLogin { + string username = 1; + string password = 2; +} + +message LoginRequest { + string username = 1; + bytes password = 2; + string my_id = 4; + string my_name = 5; + OptionMessage option = 6; + oneof union { + FileTransfer file_transfer = 7; + PortForward port_forward = 8; + ViewCamera view_camera = 15; + Terminal terminal = 16; + } + bool video_ack_required = 9; + uint64 session_id = 10; + string version = 11; + OSLogin os_login = 12; + string my_platform = 13; + bytes hwid = 14; + string avatar = 17; +} + +message Terminal { + string service_id = 1; // Service ID for reconnecting to existing session +} + +message Auth2FA { + string code = 1; + bytes hwid = 2; +} + +message ChatMessage { string text = 1; } + +message Features { + bool privacy_mode = 1; + bool terminal = 2; + bool port_forward_mux = 3; +} + +message CodecAbility { + bool vp8 = 1; + bool vp9 = 2; + bool av1 = 3; + bool h264 = 4; + bool h265 = 5; +} + +message SupportedEncoding { + bool h264 = 1; + bool h265 = 2; + bool vp8 = 3; + bool av1 = 4; + CodecAbility i444 = 5; +} + +message PeerInfo { + string username = 1; + string hostname = 2; + string platform = 3; + repeated DisplayInfo displays = 4; + int32 current_display = 5; + bool sas_enabled = 6; + string version = 7; + Features features = 9; + SupportedEncoding encoding = 10; + SupportedResolutions resolutions = 11; + // Use JSON's key-value format which is friendly for peer to handle. + // NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string. + string platform_additions = 12; + WindowsSessions windows_sessions = 13; +} + +message WindowsSession { + uint32 sid = 1; + string name = 2; +} + +message LoginResponse { + oneof union { + string error = 1; + PeerInfo peer_info = 2; + } + bool enable_trusted_devices = 3; +} + +message TouchScaleUpdate { + // The delta scale factor relative to the previous scale. + // delta * 1000 + // 0 means scale end + int32 scale = 1; +} + +message TouchPanStart { + int32 x = 1; + int32 y = 2; +} + +message TouchPanUpdate { + // The delta x position relative to the previous position. + int32 x = 1; + // The delta y position relative to the previous position. + int32 y = 2; +} + +message TouchPanEnd { + int32 x = 1; + int32 y = 2; +} + +message TouchEvent { + oneof union { + TouchScaleUpdate scale_update = 1; + TouchPanStart pan_start = 2; + TouchPanUpdate pan_update = 3; + TouchPanEnd pan_end = 4; + } +} + +message PointerDeviceEvent { + oneof union { + TouchEvent touch_event = 1; + } + repeated ControlKey modifiers = 2; +} + +message MouseEvent { + int32 mask = 1; + sint32 x = 2; + sint32 y = 3; + repeated ControlKey modifiers = 4; +} + +enum KeyboardMode{ + Legacy = 0; + Map = 1; + Translate = 2; + Auto = 3; +} + +enum ControlKey { + Unknown = 0; + Alt = 1; + Backspace = 2; + CapsLock = 3; + Control = 4; + Delete = 5; + DownArrow = 6; + End = 7; + Escape = 8; + F1 = 9; + F10 = 10; + F11 = 11; + F12 = 12; + F2 = 13; + F3 = 14; + F4 = 15; + F5 = 16; + F6 = 17; + F7 = 18; + F8 = 19; + F9 = 20; + Home = 21; + LeftArrow = 22; + /// meta key (also known as "windows"; "super"; and "command") + Meta = 23; + /// option key on macOS (alt key on Linux and Windows) + Option = 24; // deprecated, use Alt instead + PageDown = 25; + PageUp = 26; + Return = 27; + RightArrow = 28; + Shift = 29; + Space = 30; + Tab = 31; + UpArrow = 32; + Numpad0 = 33; + Numpad1 = 34; + Numpad2 = 35; + Numpad3 = 36; + Numpad4 = 37; + Numpad5 = 38; + Numpad6 = 39; + Numpad7 = 40; + Numpad8 = 41; + Numpad9 = 42; + Cancel = 43; + Clear = 44; + Menu = 45; // deprecated, use Alt instead + Pause = 46; + Kana = 47; + Hangul = 48; + Junja = 49; + Final = 50; + Hanja = 51; + Kanji = 52; + Convert = 53; + Select = 54; + Print = 55; + Execute = 56; + Snapshot = 57; + Insert = 58; + Help = 59; + Sleep = 60; + Separator = 61; + Scroll = 62; + NumLock = 63; + RWin = 64; + Apps = 65; + Multiply = 66; + Add = 67; + Subtract = 68; + Decimal = 69; + Divide = 70; + Equals = 71; + NumpadEnter = 72; + RShift = 73; + RControl = 74; + RAlt = 75; + VolumeMute = 76; // mainly used on mobile devices as controlled side + VolumeUp = 77; + VolumeDown = 78; + Power = 79; // mainly used on mobile devices as controlled side + CtrlAltDel = 100; + LockScreen = 101; +} + +message KeyEvent { + // `down` indicates the key's state(down or up). + bool down = 1; + // `press` indicates a click event(down and up). + bool press = 2; + oneof union { + ControlKey control_key = 3; + // position key code. win: scancode, linux: key code, macos: key code + uint32 chr = 4; + uint32 unicode = 5; + string seq = 6; + // high word. virtual keycode + // low word. unicode + uint32 win2win_hotkey = 7; + } + repeated ControlKey modifiers = 8; + KeyboardMode mode = 9; +} + +message CursorData { + uint64 id = 1; + sint32 hotx = 2; + sint32 hoty = 3; + int32 width = 4; + int32 height = 5; + bytes colors = 6; +} + +message CursorPosition { + sint32 x = 1; + sint32 y = 2; +} + +message Hash { + string salt = 1; + string challenge = 2; +} + +enum ClipboardFormat { + Text = 0; + Rtf = 1; + Html = 2; + ImageRgba = 21; + ImagePng = 22; + ImageSvg = 23; + Special = 31; +} + +message Clipboard { + bool compress = 1; + bytes content = 2; + int32 width = 3; + int32 height = 4; + ClipboardFormat format = 5; + // Special format name, only used when format is Special. + string special_name = 6; +} + +message MultiClipboards { repeated Clipboard clipboards = 1; } + +enum FileType { + Dir = 0; + DirLink = 2; + DirDrive = 3; + File = 4; + FileLink = 5; +} + +message FileEntry { + FileType entry_type = 1; + string name = 2; + bool is_hidden = 3; + uint64 size = 4; + uint64 modified_time = 5; +} + +message FileDirectory { + int32 id = 1; + string path = 2; + repeated FileEntry entries = 3; +} + +message ReadDir { + string path = 1; + bool include_hidden = 2; +} + +message ReadEmptyDirs { + string path = 1; + bool include_hidden = 2; +} + +message ReadEmptyDirsResponse { + string path = 1; + repeated FileDirectory empty_dirs = 2; +} + +message ReadAllFiles { + int32 id = 1; + string path = 2; + bool include_hidden = 3; +} + +message FileRename { + int32 id = 1; + string path = 2; + string new_name = 3; +} + +message FileAction { + oneof union { + ReadDir read_dir = 1; + FileTransferSendRequest send = 2; + FileTransferReceiveRequest receive = 3; + FileDirCreate create = 4; + FileRemoveDir remove_dir = 5; + FileRemoveFile remove_file = 6; + ReadAllFiles all_files = 7; + FileTransferCancel cancel = 8; + FileTransferSendConfirmRequest send_confirm = 9; + FileRename rename = 10; + ReadEmptyDirs read_empty_dirs = 11; + } +} + +message FileTransferCancel { int32 id = 1; } + +message FileResponse { + oneof union { + FileDirectory dir = 1; + FileTransferBlock block = 2; + FileTransferError error = 3; + FileTransferDone done = 4; + FileTransferDigest digest = 5; + ReadEmptyDirsResponse empty_dirs = 6; + } +} + +message FileTransferDigest { + int32 id = 1; + sint32 file_num = 2; + uint64 last_modified = 3; + uint64 file_size = 4; + bool is_upload = 5; + bool is_identical = 6; + uint64 transferred_size = 7; // For resume. Indicates the size of the file already transferred + bool is_resume = 8; // For resume. Indicates if the transfer is a resume. + // `is_resume` can let the controlled side know whether to check the `.digest` file. + // When `is_resume` is false, `.digest` exists, the same file does not exist, + // the controlled side should not check `.digest`, it should confirm with a new transfer request. +} + +message FileTransferBlock { + int32 id = 1; + sint32 file_num = 2; + bytes data = 3; + bool compressed = 4; + uint32 blk_id = 5; +} + +message FileTransferError { + int32 id = 1; + string error = 2; + sint32 file_num = 3; +} + +message FileTransferSendRequest { + int32 id = 1; + string path = 2; + bool include_hidden = 3; + int32 file_num = 4; + + enum FileType { + Generic = 0; + Printer = 1; + } + FileType file_type = 5; +} + +message FileTransferSendConfirmRequest { + int32 id = 1; + sint32 file_num = 2; + oneof union { + bool skip = 3; + uint32 offset_blk = 4; + } +} + +message FileTransferDone { + int32 id = 1; + sint32 file_num = 2; +} + +message FileTransferReceiveRequest { + int32 id = 1; + string path = 2; // path written to + repeated FileEntry files = 3; + int32 file_num = 4; + uint64 total_size = 5; +} + +message FileRemoveDir { + int32 id = 1; + string path = 2; + bool recursive = 3; +} + +message FileRemoveFile { + int32 id = 1; + string path = 2; + sint32 file_num = 3; +} + +message FileDirCreate { + int32 id = 1; + string path = 2; +} + +// main logic from freeRDP +message CliprdrMonitorReady { +} + +message CliprdrFormat { + int32 id = 2; + string format = 3; +} + +message CliprdrServerFormatList { + repeated CliprdrFormat formats = 2; +} + +message CliprdrServerFormatListResponse { + int32 msg_flags = 2; +} + +message CliprdrServerFormatDataRequest { + int32 requested_format_id = 2; +} + +message CliprdrServerFormatDataResponse { + int32 msg_flags = 2; + bytes format_data = 3; +} + +message CliprdrFileContentsRequest { + int32 stream_id = 2; + int32 list_index = 3; + int32 dw_flags = 4; + int32 n_position_low = 5; + int32 n_position_high = 6; + int32 cb_requested = 7; + bool have_clip_data_id = 8; + int32 clip_data_id = 9; +} + +message CliprdrFileContentsResponse { + int32 msg_flags = 3; + int32 stream_id = 4; + bytes requested_data = 5; +} + +// Try empty clipboard in the following case(Windows only): +// 1. `A`(Windows) -> `B`, `C` +// 2. Copy in `A, file clipboards on `B` and `C` are updated. +// 3. Copy in `B`. +// `A` should tell `C` to empty the file clipboard. +message CliprdrTryEmpty { +} + +// Clipobard file message for audit. +message CliprdrFile { + string name = 1; + uint64 size = 2; +} + +message CliprdrFiles { + repeated CliprdrFile files = 1; +} + +message Cliprdr { + oneof union { + CliprdrMonitorReady ready = 1; + CliprdrServerFormatList format_list = 2; + CliprdrServerFormatListResponse format_list_response = 3; + CliprdrServerFormatDataRequest format_data_request = 4; + CliprdrServerFormatDataResponse format_data_response = 5; + CliprdrFileContentsRequest file_contents_request = 6; + CliprdrFileContentsResponse file_contents_response = 7; + CliprdrTryEmpty try_empty = 8; + CliprdrFiles files = 9; + } +} + +message Resolution { + int32 width = 1; + int32 height = 2; +} + +message DisplayResolution { + int32 display = 1; + Resolution resolution = 2; +} + +message SupportedResolutions { repeated Resolution resolutions = 1; } + +message SwitchDisplay { + int32 display = 1; + sint32 x = 2; + sint32 y = 3; + int32 width = 4; + int32 height = 5; + bool cursor_embedded = 6; + SupportedResolutions resolutions = 7; + // Do not care about the origin point for now. + Resolution original_resolution = 8; +} + +message CaptureDisplays { + repeated int32 add = 1; + repeated int32 sub = 2; + repeated int32 set = 3; +} + +message ToggleVirtualDisplay { + int32 display = 1; + bool on = 2; +} + +message TogglePrivacyMode { + string impl_key = 1; + bool on = 2; +} + +message PermissionInfo { + enum Permission { + Keyboard = 0; + Clipboard = 2; + Audio = 3; + File = 4; + Restart = 5; + Recording = 6; + BlockInput = 7; + PrivacyMode = 8; + } + + Permission permission = 1; + bool enabled = 2; +} + +enum ImageQuality { + NotSet = 0; + Low = 2; + Balanced = 3; + Best = 4; +} + +message SupportedDecoding { + enum PreferCodec { + Auto = 0; + VP9 = 1; + H264 = 2; + H265 = 3; + VP8 = 4; + AV1 = 5; + } + + int32 ability_vp9 = 1; + int32 ability_h264 = 2; + int32 ability_h265 = 3; + PreferCodec prefer = 4; + int32 ability_vp8 = 5; + int32 ability_av1 = 6; + CodecAbility i444 = 7; + Chroma prefer_chroma = 8; +} + +message OptionMessage { + enum BoolOption { + NotSet = 0; + No = 1; + Yes = 2; + } + ImageQuality image_quality = 1; + BoolOption lock_after_session_end = 2; + BoolOption show_remote_cursor = 3; + BoolOption privacy_mode = 4; + BoolOption block_input = 5; + int32 custom_image_quality = 6; + BoolOption disable_audio = 7; + BoolOption disable_clipboard = 8; + BoolOption enable_file_transfer = 9; + SupportedDecoding supported_decoding = 10; + int32 custom_fps = 11; + BoolOption disable_keyboard = 12; +// Position 13 is used for Resolution. Remove later. +// Resolution custom_resolution = 13; +// BoolOption support_windows_specific_session = 14; + // starting from 15 please, do not use removed fields + BoolOption follow_remote_cursor = 15; + BoolOption follow_remote_window = 16; + BoolOption disable_camera = 17; + BoolOption terminal_persistent = 18; + BoolOption show_my_cursor = 19; +} + +message TestDelay { + int64 time = 1; + bool from_client = 2; + uint32 last_delay = 3; + uint32 target_bitrate = 4; +} + +message PublicKey { + bytes asymmetric_value = 1; + bytes symmetric_value = 2; +} + +message SignedId { bytes id = 1; } + +message AudioFormat { + uint32 sample_rate = 1; + uint32 channels = 2; +} + +message AudioFrame { + bytes data = 1; +} + +// Notify peer to show message box. +message MessageBox { + // Message type. Refer to flutter/lib/common.dart/msgBox(). + string msgtype = 1; + string title = 2; + // English + string text = 3; + // If not empty, msgbox provides a button to following the link. + // The link here can't be directly http url. + // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). + string link = 4; +} + +message BackNotification { + // no need to consider block input by someone else + enum BlockInputState { + BlkStateUnknown = 0; + BlkOnSucceeded = 2; + BlkOnFailed = 3; + BlkOffSucceeded = 4; + BlkOffFailed = 5; + } + enum PrivacyModeState { + PrvStateUnknown = 0; + // Privacy mode on by someone else + PrvOnByOther = 2; + // Privacy mode is not supported on the remote side + PrvNotSupported = 3; + // Privacy mode on by self + PrvOnSucceeded = 4; + // Privacy mode on by self, but denied + PrvOnFailedDenied = 5; + // Some plugins are not found + PrvOnFailedPlugin = 6; + // Privacy mode on by self, but failed + PrvOnFailed = 7; + // Privacy mode off by self + PrvOffSucceeded = 8; + // Ctrl + P + PrvOffByPeer = 9; + // Privacy mode off by self, but failed + PrvOffFailed = 10; + PrvOffUnknown = 11; + } + + oneof union { + PrivacyModeState privacy_mode_state = 1; + BlockInputState block_input_state = 2; + } + // Supplementary message, for "PrvOnFailed" and "PrvOffFailed" + string details = 3; + // The key of the implementation + string impl_key = 4; +} + +message ElevationRequestWithLogon { + string username = 1; + string password = 2; +} + +message ElevationRequest { + oneof union { + bool direct = 1; + ElevationRequestWithLogon logon = 2; + } +} + +message SwitchSidesRequest { + bytes uuid = 1; +} + +message SwitchSidesResponse { + bytes uuid = 1; + LoginRequest lr = 2; +} + +message SwitchBack {} + +message PluginRequest { + string id = 1; + bytes content = 2; +} + +message PluginFailure { + string id = 1; + string name = 2; + string msg = 3; +} + +message WindowsSessions { + repeated WindowsSession sessions = 1; + uint32 current_sid = 2; +} + +// Query messages from peer. +message MessageQuery { + // The SwitchDisplay message of the target display. + // If the target display is not found, the message will be ignored. + int32 switch_display = 1; +} + +message Misc { + oneof union { + ChatMessage chat_message = 4; + SwitchDisplay switch_display = 5; + PermissionInfo permission_info = 6; + OptionMessage option = 7; + AudioFormat audio_format = 8; + string close_reason = 9; + bool refresh_video = 10; + bool video_received = 12; + BackNotification back_notification = 13; + bool restart_remote_device = 14; + bool uac = 15; + bool foreground_window_elevated = 16; + bool stop_service = 17; + ElevationRequest elevation_request = 18; + string elevation_response = 19; + bool portable_service_running = 20; + SwitchSidesRequest switch_sides_request = 21; + SwitchBack switch_back = 22; + // Deprecated since 1.2.4, use `change_display_resolution` (36) instead. + // But we must keep it for compatibility when peer version < 1.2.4. + Resolution change_resolution = 24; + PluginRequest plugin_request = 25; + PluginFailure plugin_failure = 26; + uint32 full_speed_fps = 27; // deprecated + uint32 auto_adjust_fps = 28; + bool client_record_status = 29; + CaptureDisplays capture_displays = 30; + int32 refresh_video_display = 31; + ToggleVirtualDisplay toggle_virtual_display = 32; + TogglePrivacyMode toggle_privacy_mode = 33; + SupportedEncoding supported_encoding = 34; + uint32 selected_sid = 35; + DisplayResolution change_display_resolution = 36; + MessageQuery message_query = 37; + int32 follow_current_display = 38; + } +} + +message VoiceCallRequest { + int64 req_timestamp = 1; + // Indicates whether the request is a connect action or a disconnect action. + bool is_connect = 2; +} + +message VoiceCallResponse { + bool accepted = 1; + int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. + int64 ack_timestamp = 3; +} + +message ScreenshotRequest { + int32 display = 1; + // sid is the session id on the controlling side + // It is used to forward the message to the correct remote (session) window. + string sid = 2; +} + +message ScreenshotResponse { + string sid = 1; + // empty if success + string msg = 2; + bytes data = 3; +} + +// Terminal messages - standalone feature like FileAction +message OpenTerminal { + int32 terminal_id = 1; // 0 for default terminal + uint32 rows = 2; + uint32 cols = 3; +} + +message ResizeTerminal { + int32 terminal_id = 1; + uint32 rows = 2; + uint32 cols = 3; +} + +message TerminalData { + int32 terminal_id = 1; + bytes data = 2; + bool compressed = 3; +} + +message CloseTerminal { + int32 terminal_id = 1; +} + +message TerminalAction { + oneof union { + OpenTerminal open = 1; + TerminalData data = 2; + ResizeTerminal resize = 3; + CloseTerminal close = 4; + } +} + +message TerminalOpened { + int32 terminal_id = 1; + bool success = 2; + string message = 3; + uint32 pid = 4; + string service_id = 5; // Service ID for persistent sessions + repeated int32 persistent_sessions = 6; // Used to restore the persistent sessions. + bool replay_terminal_output = 7; // Whether the next data response replays buffered terminal output. +} + +message TerminalClosed { + int32 terminal_id = 1; + int32 exit_code = 2; +} + +message TerminalError { + int32 terminal_id = 1; + string message = 2; +} + +message TerminalResponse { + oneof union { + TerminalOpened opened = 1; + TerminalData data = 2; + TerminalClosed closed = 3; + TerminalError error = 4; + } +} + +message PortForwardOpen { + int32 channel_id = 1; + string host = 2; + int32 port = 3; + uint32 window = 4; +} + +message PortForwardOpened { + int32 channel_id = 1; + bool success = 2; + string message = 3; + uint32 window = 4; +} + +message PortForwardData { + int32 channel_id = 1; + bytes data = 2; +} + +message PortForwardClose { + int32 channel_id = 1; +} + +message PortForwardWindowUpdate { + int32 channel_id = 1; + uint32 add = 2; +} + +// One symmetric message for both directions. `open` only travels controller -> controlled, +// `opened` only controlled -> controller; a frame in the wrong direction is ignored. +message PortForwardChannel { + oneof union { + PortForwardOpen open = 1; + PortForwardOpened opened = 2; + PortForwardData data = 3; + PortForwardClose close = 4; + PortForwardWindowUpdate window_update = 5; + } +} + +message Message { + oneof union { + SignedId signed_id = 3; + PublicKey public_key = 4; + TestDelay test_delay = 5; + VideoFrame video_frame = 6; + LoginRequest login_request = 7; + LoginResponse login_response = 8; + Hash hash = 9; + MouseEvent mouse_event = 10; + AudioFrame audio_frame = 11; + CursorData cursor_data = 12; + CursorPosition cursor_position = 13; + uint64 cursor_id = 14; + KeyEvent key_event = 15; + Clipboard clipboard = 16; + FileAction file_action = 17; + FileResponse file_response = 18; + Misc misc = 19; + Cliprdr cliprdr = 20; + MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; + VoiceCallRequest voice_call_request = 23; + VoiceCallResponse voice_call_response = 24; + PeerInfo peer_info = 25; + PointerDeviceEvent pointer_device_event = 26; + Auth2FA auth_2fa = 27; + MultiClipboards multi_clipboards = 28; + ScreenshotRequest screenshot_request = 29; + ScreenshotResponse screenshot_response= 30; + TerminalAction terminal_action = 31; + TerminalResponse terminal_response = 32; + PortForwardChannel port_forward_channel = 33; + } +} diff --git a/libs/base/src/config/keys.rs b/libs/base/src/config/keys.rs new file mode 100644 index 000000000..de2a25879 --- /dev/null +++ b/libs/base/src/config/keys.rs @@ -0,0 +1,403 @@ +//! Option keys shared across the app. +//! +//! The handful that `hbb_common` itself reads stay defined there and are +//! re-exported here, so callers always use this one path. + +pub use hbb_common::config::keys::*; + +pub const OPTION_VIEW_ONLY: &str = "view_only"; +pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar"; +pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor"; +pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor"; +pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window"; +pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor"; +pub const OPTION_DISABLE_AUDIO: &str = "disable_audio"; +pub const OPTION_ENABLE_REMOTE_PRINTER: &str = "enable-remote-printer"; +pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard"; +pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end"; +pub const OPTION_PRIVACY_MODE: &str = "privacy_mode"; +pub const OPTION_TOUCH_MODE: &str = "touch-mode"; +pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; +pub const OPTION_THEME: &str = "theme"; +pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; +pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right"; +pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel"; +pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs"; +pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str = "enable-open-new-connections-in-tabs"; +pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render"; +// Internal health record written by the texture-render watchdog/probe; +// "failed-*" flips the texture-render default to opt-in on this machine. +pub const OPTION_TEXTURE_RENDER_HEALTH: &str = "texture-render-health"; +pub const OPTION_ALLOW_D3D_RENDER: &str = "allow-d3d-render"; +pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update"; +pub const OPTION_ALLOW_AUTO_UPDATE: &str = "allow-auto-update"; +pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions"; +pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags"; +pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection"; +pub const OPTION_ACCESS_MODE: &str = "access-mode"; +pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard"; +pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard"; +pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer"; +pub const OPTION_ENABLE_CAMERA: &str = "enable-camera"; +pub const OPTION_ENABLE_TERMINAL: &str = "enable-terminal"; +pub const OPTION_TERMINAL_PERSISTENT: &str = "terminal-persistent"; +pub const OPTION_ENABLE_AUDIO: &str = "enable-audio"; +pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel"; +pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart"; +pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session"; +pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input"; +pub const OPTION_ENABLE_PRIVACY_MODE: &str = "enable-privacy-mode"; +pub const OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW: &str = "enable-perm-change-in-accept-window"; +pub const OPTION_ALLOW_SCOPE_VIOLATION_CLOSE: &str = "allow-scope-violation-close"; +pub const OPTION_ALLOW_SCOPE_VIOLATION_ALARM: &str = "allow-scope-violation-alarm"; +pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification"; +pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery"; +pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port"; +pub const OPTION_WHITELIST: &str = "whitelist"; +pub const OPTION_ID_WHITELIST: &str = "id-whitelist"; +pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect"; +pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout"; +pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open"; +pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming"; +pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing"; +pub const OPTION_HIDE_RECORDING_BUTTON: &str = "hide-recording-button"; +pub const OPTION_WINDOWS_SERVICE_VIDEO_SAVE_DIRECTORY: &str = + "windows-service-video-save-directory"; +pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory"; +pub const OPTION_ENABLE_ABR: &str = "enable-abr"; +pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper"; +pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render"; +pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec"; +pub const OPTION_APPROVE_MODE: &str = "approve-mode"; +pub const OPTION_VERIFICATION_METHOD: &str = "verification-method"; +pub const OPTION_TEMPORARY_PASSWORD_LENGTH: &str = "temporary-password-length"; +pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server"; +pub const OPTION_API_SERVER: &str = "api-server"; +pub const OPTION_KEY: &str = "key"; +pub const OPTION_PRESET_ADDRESS_BOOK_NAME: &str = "preset-address-book-name"; +pub const OPTION_PRESET_ADDRESS_BOOK_TAG: &str = "preset-address-book-tag"; +pub const OPTION_PRESET_ADDRESS_BOOK_ALIAS: &str = "preset-address-book-alias"; +pub const OPTION_PRESET_ADDRESS_BOOK_PASSWORD: &str = "preset-address-book-password"; +pub const OPTION_PRESET_ADDRESS_BOOK_NOTE: &str = "preset-address-book-note"; +pub const OPTION_PRESET_DEVICE_USERNAME: &str = "preset-device-username"; +pub const OPTION_PRESET_DEVICE_NAME: &str = "preset-device-name"; +pub const OPTION_PRESET_NOTE: &str = "preset-note"; +pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture"; +pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = + "enable-android-software-encoding-half-scale"; +pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices"; +pub const OPTION_AV1_TEST: &str = "av1-test"; +/// Maximum number of files allowed during a single file transfer request. +/// +/// Key: `file-transfer-max-files`. +/// Unit: number of files (not bytes). +/// +/// Behaviour: +/// - If set to a positive integer N, at most N files are allowed. +/// - If set to 0, a safe built-in default is used (see DEFAULT_MAX_VALIDATED_FILES). +/// - If unset, negative, or non-integer, no explicit limit is enforced for backward compatibility. +pub const OPTION_FILE_TRANSFER_MAX_FILES: &str = "file-transfer-max-files"; +pub const OPTION_DISABLE_UDP: &str = "disable-udp"; +pub const OPTION_SHOW_VIRTUAL_MOUSE: &str = "show-virtual-mouse"; +// joystick is the virtual mouse. +// So `OPTION_SHOW_VIRTUAL_MOUSE` should also be set if `OPTION_SHOW_VIRTUAL_JOYSTICK` is set. +pub const OPTION_SHOW_VIRTUAL_JOYSTICK: &str = "show-virtual-joystick"; +pub const OPTION_ENABLE_FLUTTER_HTTP_ON_RUST: &str = "enable-flutter-http-on-rust"; +pub const OPTION_ALLOW_ASK_FOR_NOTE: &str = "allow-ask-for-note"; + +// built-in options +pub const OPTION_DISPLAY_NAME: &str = "display-name"; +pub const OPTION_AVATAR: &str = "avatar"; +pub const OPTION_PRESET_DEVICE_GROUP_NAME: &str = "preset-device-group-name"; +pub const OPTION_PRESET_USERNAME: &str = "preset-user-name"; +pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name"; +pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning"; +pub const OPTION_HIDE_GENERAL_SETTINGS: &str = "hide-general-settings"; +pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings"; +pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings"; +pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings"; +pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings"; +pub const OPTION_HIDE_REMOTE_PRINTER_SETTINGS: &str = "hide-remote-printer-settings"; +pub const OPTION_HIDE_WEBSOCKET_SETTINGS: &str = "hide-websocket-settings"; +pub const OPTION_HIDE_STOP_SERVICE: &str = "hide-stop-service"; +pub const OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED: &str = + "allow-command-line-settings-when-settings-disabled"; + +// Connection punch-through / port-forward options +pub const OPTION_ENABLE_TCP_PUNCH: &str = "enable-tcp-punch"; +pub const OPTION_ENABLE_UDP_PUNCH: &str = "enable-udp-punch"; +pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-punch"; +pub const OPTION_ENABLE_PORT_FORWARD_MUX: &str = "enable-port-forward-mux"; +pub const OPTION_ENABLE_WEBRTC: &str = "enable-webrtc"; +pub const OPTION_ALLOW_KCP_CC: &str = "allow-kcp-congestion-control"; +pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card"; +pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; +pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; +pub const OPTION_HIDE_TRAY: &str = "hide-tray"; +pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; +pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; +pub const OPTION_ALLOW_DEEP_LINK_PASSWORD: &str = "allow-deep-link-password"; +pub const OPTION_ALLOW_DEEP_LINK_SERVER_SETTINGS: &str = "allow-deep-link-server-settings"; +pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; +pub const OPTION_ALLOW_HTTPS_21114: &str = "allow-https-21114"; +pub const OPTION_USE_RAW_TCP_FOR_API: &str = "use-raw-tcp-for-api"; +pub const OPTION_HIDE_POWERED_BY_ME: &str = "hide-powered-by-me"; +pub const OPTION_MAIN_WINDOW_ALWAYS_ON_TOP: &str = "main-window-always-on-top"; + +// flutter local options +pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; +pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting"; +pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index"; +pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order"; +pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible"; +pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type"; +pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name"; +pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification"; +pub const OPTION_ALLOW_SYNC_CLIPBOARD_BETWEEN_SESSIONS: &str = + "allow-sync-clipboard-between-sessions"; + +pub const OPTION_PRINTER_INCOMING_JOB_ACTION: &str = "printer-incomming-job-action"; +pub const OPTION_PRINTER_ALLOW_AUTO_PRINT: &str = "allow-printer-auto-print"; +pub const OPTION_PRINTER_SELECTED_NAME: &str = "printer-selected-name"; + +// android floating window options +pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window"; +pub const OPTION_FLOATING_WINDOW_SIZE: &str = "floating-window-size"; +pub const OPTION_FLOATING_WINDOW_UNTOUCHABLE: &str = "floating-window-untouchable"; +pub const OPTION_FLOATING_WINDOW_TRANSPARENCY: &str = "floating-window-transparency"; +pub const OPTION_FLOATING_WINDOW_SVG: &str = "floating-window-svg"; + +// android keep screen on +pub const OPTION_KEEP_SCREEN_ON: &str = "keep-screen-on"; + +// Server-side: keep host system awake during incoming sessions (Security setting) +pub const OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS: &str = "keep-awake-during-incoming-sessions"; + +// Client-side: keep client system awake during outgoing sessions (General setting) +pub const OPTION_KEEP_AWAKE_DURING_OUTGOING_SESSIONS: &str = "keep-awake-during-outgoing-sessions"; + +pub const OPTION_DISABLE_GROUP_PANEL: &str = "disable-group-panel"; +pub const OPTION_DISABLE_DISCOVERY_PANEL: &str = "disable-discovery-panel"; +pub const OPTION_PRE_ELEVATE_SERVICE: &str = "pre-elevate-service"; + +// DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS +pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[ + OPTION_VIEW_ONLY, + OPTION_SHOW_MONITORS_TOOLBAR, + OPTION_COLLAPSE_TOOLBAR, + OPTION_SHOW_REMOTE_CURSOR, + OPTION_FOLLOW_REMOTE_CURSOR, + OPTION_FOLLOW_REMOTE_WINDOW, + OPTION_ZOOM_CURSOR, + OPTION_SHOW_QUALITY_MONITOR, + OPTION_DISABLE_AUDIO, + OPTION_ENABLE_FILE_COPY_PASTE, + OPTION_DISABLE_CLIPBOARD, + OPTION_LOCK_AFTER_SESSION_END, + OPTION_PRIVACY_MODE, + OPTION_TOUCH_MODE, + OPTION_I444, + OPTION_REVERSE_MOUSE_WHEEL, + OPTION_SWAP_LEFT_RIGHT_MOUSE, + OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS, + OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION, + OPTION_VIEW_STYLE, + OPTION_TERMINAL_PERSISTENT, + OPTION_SCROLL_STYLE, + OPTION_EDGE_SCROLL_EDGE_THICKNESS, + OPTION_IMAGE_QUALITY, + OPTION_CUSTOM_IMAGE_QUALITY, + OPTION_CUSTOM_FPS, + OPTION_CODEC_PREFERENCE, + OPTION_SYNC_INIT_CLIPBOARD, + OPTION_TRACKPAD_SPEED, +]; +// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS +pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ + OPTION_THEME, + OPTION_LANGUAGE, + OPTION_ENABLE_CONFIRM_CLOSING_TABS, + OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS, + OPTION_TEXTURE_RENDER, + OPTION_ALLOW_D3D_RENDER, + OPTION_SYNC_AB_WITH_RECENT_SESSIONS, + OPTION_SYNC_AB_TAGS, + OPTION_FILTER_AB_BY_INTERSECTION, + OPTION_REMOTE_MENUBAR_DRAG_LEFT, + OPTION_REMOTE_MENUBAR_DRAG_RIGHT, + OPTION_HIDE_AB_TAGS_PANEL, + OPTION_FLUTTER_REMOTE_MENUBAR_STATE, + OPTION_FLUTTER_PEER_SORTING, + OPTION_FLUTTER_PEER_TAB_INDEX, + OPTION_FLUTTER_PEER_TAB_ORDER, + OPTION_FLUTTER_PEER_TAB_VISIBLE, + OPTION_FLUTTER_PEER_CARD_UI_TYLE, + OPTION_FLUTTER_CURRENT_AB_NAME, + OPTION_DISABLE_FLOATING_WINDOW, + OPTION_FLOATING_WINDOW_SIZE, + OPTION_FLOATING_WINDOW_UNTOUCHABLE, + OPTION_FLOATING_WINDOW_TRANSPARENCY, + OPTION_FLOATING_WINDOW_SVG, + OPTION_KEEP_SCREEN_ON, + // Client-side: keep client system awake during outgoing sessions (General setting) + OPTION_KEEP_AWAKE_DURING_OUTGOING_SESSIONS, + OPTION_DISABLE_GROUP_PANEL, + OPTION_DISABLE_DISCOVERY_PANEL, + OPTION_PRE_ELEVATE_SERVICE, + OPTION_ALLOW_REMOTE_CM_MODIFICATION, + OPTION_ALLOW_SYNC_CLIPBOARD_BETWEEN_SESSIONS, + OPTION_ENABLE_CHECK_UPDATE, + OPTION_PRINTER_INCOMING_JOB_ACTION, + OPTION_PRINTER_ALLOW_AUTO_PRINT, + OPTION_PRINTER_SELECTED_NAME, + OPTION_ALLOW_AUTO_RECORD_OUTGOING, + OPTION_HIDE_RECORDING_BUTTON, + OPTION_VIDEO_SAVE_DIRECTORY, + OPTION_ENABLE_TCP_PUNCH, + OPTION_ENABLE_UDP_PUNCH, + OPTION_ENABLE_IPV6_PUNCH, + OPTION_ENABLE_PORT_FORWARD_MUX, + OPTION_ENABLE_WEBRTC, + OPTION_TOUCH_MODE, + OPTION_SHOW_VIRTUAL_MOUSE, + OPTION_SHOW_VIRTUAL_JOYSTICK, + OPTION_ENABLE_FLUTTER_HTTP_ON_RUST, + OPTION_ALLOW_ASK_FOR_NOTE, +]; +// DEFAULT_SETTINGS, OVERWRITE_SETTINGS +pub const KEYS_SETTINGS: &[&str] = &[ + OPTION_ACCESS_MODE, + OPTION_ENABLE_KEYBOARD, + OPTION_ENABLE_CLIPBOARD, + OPTION_ENABLE_FILE_TRANSFER, + OPTION_ENABLE_CAMERA, + OPTION_ENABLE_TERMINAL, + OPTION_ENABLE_REMOTE_PRINTER, + OPTION_ENABLE_AUDIO, + OPTION_ENABLE_TUNNEL, + OPTION_ENABLE_REMOTE_RESTART, + OPTION_ENABLE_RECORD_SESSION, + OPTION_ENABLE_BLOCK_INPUT, + OPTION_ENABLE_PRIVACY_MODE, + OPTION_ALLOW_SCOPE_VIOLATION_CLOSE, + OPTION_ALLOW_SCOPE_VIOLATION_ALARM, + OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION, + OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD, + OPTION_ENABLE_LAN_DISCOVERY, + OPTION_DIRECT_SERVER, + OPTION_DIRECT_ACCESS_PORT, + OPTION_WHITELIST, + OPTION_ID_WHITELIST, + OPTION_ALLOW_AUTO_DISCONNECT, + OPTION_AUTO_DISCONNECT_TIMEOUT, + OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, + OPTION_ALLOW_AUTO_RECORD_INCOMING, + OPTION_WINDOWS_SERVICE_VIDEO_SAVE_DIRECTORY, + OPTION_ENABLE_ABR, + OPTION_ALLOW_REMOVE_WALLPAPER, + OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, + OPTION_ENABLE_HWCODEC, + OPTION_APPROVE_MODE, + OPTION_VERIFICATION_METHOD, + OPTION_TEMPORARY_PASSWORD_LENGTH, + OPTION_PROXY_URL, + OPTION_PROXY_USERNAME, + OPTION_PROXY_PASSWORD, + OPTION_CUSTOM_RENDEZVOUS_SERVER, + OPTION_API_SERVER, + OPTION_KEY, + OPTION_ALLOW_WEBSOCKET, + OPTION_PRESET_ADDRESS_BOOK_NAME, + OPTION_PRESET_ADDRESS_BOOK_TAG, + OPTION_PRESET_ADDRESS_BOOK_ALIAS, + OPTION_PRESET_ADDRESS_BOOK_PASSWORD, + OPTION_PRESET_ADDRESS_BOOK_NOTE, + OPTION_PRESET_DEVICE_USERNAME, + OPTION_PRESET_DEVICE_NAME, + OPTION_PRESET_NOTE, + OPTION_ENABLE_DIRECTX_CAPTURE, + OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE, + OPTION_ENABLE_TRUSTED_DEVICES, + OPTION_RELAY_SERVER, + OPTION_ICE_SERVERS, + OPTION_DISABLE_UDP, + OPTION_ALLOW_INSECURE_TLS_FALLBACK, + OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, + OPTION_ALLOW_AUTO_UPDATE, + OPTION_ALLOW_KCP_CC, + OPTION_ALLOW_WEBRTC_CC, +]; + +// BUILDIN_SETTINGS +pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[ + OPTION_DISPLAY_NAME, + OPTION_AVATAR, + OPTION_PRESET_DEVICE_GROUP_NAME, + OPTION_PRESET_USERNAME, + OPTION_PRESET_STRATEGY_NAME, + OPTION_REMOVE_PRESET_PASSWORD_WARNING, + OPTION_HIDE_GENERAL_SETTINGS, + OPTION_HIDE_SECURITY_SETTINGS, + OPTION_HIDE_NETWORK_SETTINGS, + OPTION_HIDE_SERVER_SETTINGS, + OPTION_HIDE_PROXY_SETTINGS, + OPTION_HIDE_REMOTE_PRINTER_SETTINGS, + OPTION_HIDE_WEBSOCKET_SETTINGS, + OPTION_HIDE_STOP_SERVICE, + OPTION_HIDE_USERNAME_ON_CARD, + OPTION_HIDE_HELP_CARDS, + OPTION_DEFAULT_CONNECT_PASSWORD, + OPTION_HIDE_TRAY, + OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, + OPTION_ALLOW_LOGON_SCREEN_PASSWORD, + OPTION_ALLOW_DEEP_LINK_PASSWORD, + OPTION_ALLOW_DEEP_LINK_SERVER_SETTINGS, + OPTION_ONE_WAY_FILE_TRANSFER, + OPTION_ALLOW_HTTPS_21114, + OPTION_ALLOW_HOSTNAME_AS_ID, + OPTION_REGISTER_DEVICE, + OPTION_HIDE_POWERED_BY_ME, + OPTION_MAIN_WINDOW_ALWAYS_ON_TOP, + OPTION_FILE_TRANSFER_MAX_FILES, + OPTION_DISABLE_CHANGE_PERMANENT_PASSWORD, + OPTION_DISABLE_CHANGE_ID, + OPTION_DISABLE_UNLOCK_PIN, + OPTION_USE_RAW_TCP_FOR_API, + OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED, +]; + +#[cfg(test)] +mod tests { + /// The glob above and the constants below share one namespace, and Rust + /// silently prefers the explicit item over a glob import. A key defined on + /// both sides would therefore compile, with the client and the server + /// disagreeing about its string value and nothing to signal it. Keep the + /// two sets apart. + #[test] + fn key_names_do_not_collide_with_hbb_common() { + fn names(src: &str) -> Vec<&str> { + src.lines() + .filter_map(|l| l.trim().strip_prefix("pub const ")) + .filter_map(|l| l.split(':').next()) + .map(str::trim) + .filter(|n| n.starts_with("OPTION_") || n.starts_with("KEYS_")) + .collect() + } + + let here = names(include_str!("keys.rs")); + let there = names(include_str!("../../../hbb_common/src/config.rs")); + assert!( + !here.is_empty() && !there.is_empty(), + "key parsing found nothing" + ); + + let both: Vec<_> = here.iter().filter(|n| there.contains(n)).collect(); + assert!( + both.is_empty(), + "defined in both crates, so the local one shadows hbb_common's \ + with no diagnostic: {:?}", + both + ); + } +} diff --git a/libs/base/src/config/mod.rs b/libs/base/src/config/mod.rs new file mode 100644 index 000000000..703bc0872 --- /dev/null +++ b/libs/base/src/config/mod.rs @@ -0,0 +1 @@ +pub mod keys; diff --git a/libs/base/src/fs.rs b/libs/base/src/fs.rs new file mode 100644 index 000000000..96f377200 --- /dev/null +++ b/libs/base/src/fs.rs @@ -0,0 +1,1810 @@ +#[cfg(windows)] +use std::os::windows::prelude::*; +use std::{ + fmt::{Debug, Display}, + io::Cursor, + path::{Path, PathBuf}, + sync::atomic::{AtomicI32, Ordering}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use serde_derive::{Deserialize, Serialize}; +use serde_json::json; +use tokio::{ + fs::{File, OpenOptions}, + io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufStream as TokioBufStream}, +}; + +use crate::message_proto::*; +// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html +use hbb_common::{ + anyhow::anyhow, + bail, + compress::{compress, decompress}, + config::Config, + get_version_number, ResultType, Stream, +}; + +static NEXT_JOB_ID: AtomicI32 = AtomicI32::new(1); + +pub fn get_next_job_id() -> i32 { + NEXT_JOB_ID.fetch_add(1, Ordering::SeqCst) +} + +pub fn update_next_job_id(id: i32) { + NEXT_JOB_ID.store(id, Ordering::SeqCst); +} + +pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { + let mut dir = FileDirectory { + path: get_string(path), + ..Default::default() + }; + #[cfg(windows)] + if "/" == &get_string(path) { + let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; + for i in 0..32 { + if drives & (1 << i) != 0 { + let name = format!( + "{}:", + std::char::from_u32('A' as u32 + i as u32).unwrap_or('A') + ); + dir.entries.push(FileEntry { + name, + entry_type: FileType::DirDrive.into(), + ..Default::default() + }); + } + } + return Ok(dir); + } + for entry in path.read_dir()?.flatten() { + let p = entry.path(); + let name = p + .file_name() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + if name.is_empty() { + continue; + } + let mut is_hidden = false; + let meta; + if let Ok(tmp) = std::fs::symlink_metadata(&p) { + meta = tmp; + } else { + continue; + } + // docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + #[cfg(windows)] + if meta.file_attributes() & 0x2 != 0 { + is_hidden = true; + } + #[cfg(not(windows))] + if name.find('.').unwrap_or(usize::MAX) == 0 { + is_hidden = true; + } + if is_hidden && !include_hidden { + continue; + } + let (entry_type, size) = { + if p.is_dir() { + if meta.file_type().is_symlink() { + (FileType::DirLink.into(), 0) + } else { + (FileType::Dir.into(), 0) + } + } else if meta.file_type().is_symlink() { + (FileType::FileLink.into(), 0) + } else { + (FileType::File.into(), meta.len()) + } + }; + let modified_time = meta + .modified() + .map(|x| { + x.duration_since(std::time::SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0) + }) + .unwrap_or(0); + dir.entries.push(FileEntry { + name: get_file_name(&p), + entry_type, + is_hidden, + size, + modified_time, + ..Default::default() + }); + } + Ok(dir) +} + +#[inline] +pub fn get_file_name(p: &Path) -> String { + p.file_name() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned() +} + +#[inline] +pub fn get_string(path: &Path) -> String { + path.to_str().unwrap_or("").to_owned() +} + +#[inline] +pub fn get_path(path: &str) -> PathBuf { + Path::new(path).to_path_buf() +} + +#[inline] +pub fn get_home_as_string() -> String { + get_string(&Config::get_home()) +} + +fn read_dir_recursive( + path: &Path, + prefix: &Path, + include_hidden: bool, +) -> ResultType> { + let mut files = Vec::new(); + if path.is_dir() { + // to-do: symbol link handling, cp the link rather than the content + // to-do: file mode, for unix + let fd = read_dir(path, include_hidden)?; + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::File) => { + let mut entry = entry.clone(); + entry.name = get_string(&prefix.join(entry.name)); + files.push(entry); + } + Ok(FileType::Dir) => { + if let Ok(mut tmp) = read_dir_recursive( + &path.join(&entry.name), + &prefix.join(&entry.name), + include_hidden, + ) { + for entry in tmp.drain(0..) { + files.push(entry); + } + } + } + _ => {} + } + } + Ok(files) + } else if path.is_file() { + let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { + ( + meta.len(), + meta.modified() + .map(|x| { + x.duration_since(std::time::SystemTime::UNIX_EPOCH) + .map(|x| x.as_secs()) + .unwrap_or(0) + }) + .unwrap_or(0), + ) + } else { + (0, 0) + }; + files.push(FileEntry { + entry_type: FileType::File.into(), + size, + modified_time, + ..Default::default() + }); + Ok(files) + } else { + bail!("Not exists"); + } +} + +pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType> { + read_dir_recursive(&get_path(path), &get_path(""), include_hidden) +} + +fn read_empty_dirs_recursive( + path: &Path, + prefix: &Path, + include_hidden: bool, +) -> ResultType> { + let mut dirs = Vec::new(); + if path.is_dir() { + // to-do: symbol link handling, cp the link rather than the content + // to-do: file mode, for unix + let fd = read_dir(path, include_hidden)?; + if fd.entries.is_empty() { + dirs.push(fd); + } else { + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::Dir) => { + if let Ok(mut tmp) = read_empty_dirs_recursive( + &path.join(&entry.name), + &prefix.join(&entry.name), + include_hidden, + ) { + for entry in tmp.drain(0..) { + dirs.push(entry); + } + } + } + _ => {} + } + } + } + Ok(dirs) + } else if path.is_file() { + Ok(dirs) + } else { + bail!("Not exists"); + } +} + +pub fn get_empty_dirs_recursive( + path: &str, + include_hidden: bool, +) -> ResultType> { + read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden) +} + +#[inline] +pub fn is_file_exists(file_path: &str) -> bool { + return Path::new(file_path).exists(); +} + +#[inline] +pub fn can_enable_overwrite_detection(version: i64) -> bool { + version >= get_version_number("1.1.10") +} + +#[repr(i32)] +#[derive(Copy, Clone, Serialize, Debug, PartialEq)] +pub enum JobType { + Generic = 0, + Printer = 1, +} + +impl Default for JobType { + fn default() -> Self { + JobType::Generic + } +} + +impl From for file_transfer_send_request::FileType { + fn from(t: JobType) -> Self { + match t { + JobType::Generic => file_transfer_send_request::FileType::Generic, + JobType::Printer => file_transfer_send_request::FileType::Printer, + } + } +} + +impl From for JobType { + fn from(value: i32) -> Self { + match value { + 0 => JobType::Generic, + 1 => JobType::Printer, + _ => JobType::Generic, + } + } +} + +impl Into for JobType { + fn into(self) -> i32 { + self as i32 + } +} + +impl JobType { + pub fn from_proto(t: ::protobuf::EnumOrUnknown) -> Self { + match t.enum_value() { + Ok(file_transfer_send_request::FileType::Generic) => JobType::Generic, + Ok(file_transfer_send_request::FileType::Printer) => JobType::Printer, + _ => JobType::Generic, + } + } +} + +#[derive(Debug)] +pub enum DataSource { + FilePath(PathBuf), + MemoryCursor(Cursor>), +} + +impl Default for DataSource { + fn default() -> Self { + DataSource::FilePath(PathBuf::new()) + } +} + +impl serde::Serialize for DataSource { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + match self { + DataSource::FilePath(p) => serializer.serialize_str(p.to_str().unwrap_or("")), + DataSource::MemoryCursor(_) => serializer.serialize_str(""), + } + } +} + +impl Display for DataSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DataSource::FilePath(p) => write!(f, "File: {}", p.to_string_lossy().to_string()), + DataSource::MemoryCursor(_) => write!(f, "Bytes"), + } + } +} + +impl DataSource { + fn to_meta(&self) -> String { + match self { + DataSource::FilePath(p) => p.to_string_lossy().to_string(), + DataSource::MemoryCursor(_) => "".to_string(), + } + } +} + +enum DataStream { + FileStream(File), + BufStream(TokioBufStream>>), +} + +impl Debug for DataStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DataStream::FileStream(fs) => write!(f, "{:?}", fs), + DataStream::BufStream(_) => write!(f, "BufStream"), + } + } +} + +impl DataStream { + async fn write_all(&mut self, buf: &[u8]) -> ResultType<()> { + match self { + DataStream::FileStream(fs) => fs.write_all(buf).await?, + DataStream::BufStream(bs) => bs.write_all(buf).await?, + } + Ok(()) + } + + async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + DataStream::FileStream(fs) => fs.read(buf).await, + DataStream::BufStream(bs) => bs.read(buf).await, + } + } +} + +#[derive(Default, Serialize, Deserialize, Debug)] +pub struct FileDigest { + pub size: u64, + pub modified: u64, +} + +#[derive(Default, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TransferJob { + pub id: i32, + pub r#type: JobType, + pub remote: String, + pub data_source: DataSource, + pub show_hidden: bool, + pub is_remote: bool, + pub is_last_job: bool, + pub is_resume: bool, + pub file_num: i32, + #[serde(skip_serializing)] + files: Vec, + pub conn_id: i32, // server only + + #[serde(skip_serializing)] + data_stream: Option, + pub total_size: u64, + finished_size: u64, + transferred: u64, + enable_overwrite_detection: bool, + file_confirmed: bool, + // indicating the last file is skipped + file_skipped: bool, + file_is_waiting: bool, + default_overwrite_strategy: Option, + #[serde(skip_serializing)] + digest: FileDigest, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct TransferJobMeta { + #[serde(default)] + pub id: i32, + #[serde(default)] + pub remote: String, + #[serde(default)] + pub to: String, + #[serde(default)] + pub show_hidden: bool, + #[serde(default)] + pub file_num: i32, + #[serde(default)] + pub is_remote: bool, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct RemoveJobMeta { + #[serde(default)] + pub path: String, + #[serde(default)] + pub is_remote: bool, + #[serde(default)] + pub no_confirm: bool, +} + +#[inline] +fn get_ext(name: &str) -> &str { + if let Some(i) = name.rfind('.') { + return &name[i + 1..]; + } + "" +} + +#[inline] +fn is_compressed_file(name: &str) -> bool { + let compressed_exts = ["xz", "gz", "zip", "7z", "rar", "bz2", "tgz", "png", "jpg"]; + let ext = get_ext(name); + compressed_exts.contains(&ext) +} + +pub fn validate_file_name_no_traversal(name: &str) -> ResultType<()> { + if name.bytes().any(|b| b == 0) { + bail!("file name contains null bytes"); + } + let has_traversal = name + .split(|c: char| c == '/' || (cfg!(windows) && c == '\\')) + .filter(|s| !s.is_empty()) + .any(|s| s == ".."); + if has_traversal { + bail!("path traversal detected in file name"); + } + #[cfg(windows)] + { + if name.len() >= 2 { + let bytes = name.as_bytes(); + if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + bail!("absolute path detected in file name"); + } + } + if name.starts_with('/') || name.starts_with('\\') { + bail!("absolute path detected in file name"); + } + } + #[cfg(not(windows))] + if name.starts_with('/') { + bail!("absolute path detected in file name"); + } + Ok(()) +} + +fn validate_transfer_file_names(files: &[FileEntry]) -> ResultType<()> { + // Single-file transfer may use an empty relative name, because + // the destination file path is carried by transfer metadata. + if files.len() == 1 && files.first().map_or(false, |f| f.name.is_empty()) { + return Ok(()); + } + for file in files { + if file.name.is_empty() { + bail!("empty file name in multi-file transfer"); + } + validate_file_name_no_traversal(&file.name)?; + } + Ok(()) +} + +#[inline] +fn validate_fs_path_argument(path: &str, arg_name: &str) -> ResultType<()> { + if path.is_empty() { + bail!("{arg_name} cannot be empty"); + } + if path.bytes().any(|b| b == 0) { + bail!("{arg_name} contains null bytes"); + } + Ok(()) +} + +fn validate_no_symlink_components(base: &PathBuf, name: &str) -> ResultType<()> { + if name.is_empty() { + return Ok(()); + } + let mut current = base.clone(); + for component in Path::new(name).components() { + match component { + std::path::Component::Normal(seg) => { + current.push(seg); + // Best-effort guard: path-based checks are inherently TOCTOU-prone + // if local filesystem state changes between validation and write. + match std::fs::symlink_metadata(¤t) { + Ok(meta) => { + // This is inherent to filesystem-based checks and acknowledged as a limitation. + // For true protection, you'd need openat(2) / O_NOFOLLOW at write time. + if meta.file_type().is_symlink() { + bail!("symlink path component is not allowed"); + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // Component does not exist yet, continue best-effort validation. + } + Err(err) => { + bail!( + "failed to validate path component '{}': {}", + current.display(), + err + ); + } + } + } + std::path::Component::CurDir => {} + _ => { + bail!("invalid file name component"); + } + } + } + Ok(()) +} + +/// Validate an untrusted relative file name and existing path components before joining it. +pub fn join_validated_path(base: &PathBuf, name: &str) -> ResultType { + validate_file_name_no_traversal(name)?; + validate_no_symlink_components(base, name)?; + Ok(TransferJob::join(base, name)) +} + +impl TransferJob { + #[allow(clippy::too_many_arguments)] + pub fn new_write( + id: i32, + r#type: JobType, + remote: String, + data_source: DataSource, + file_num: i32, + show_hidden: bool, + is_remote: bool, + enable_overwrite_detection: bool, + ) -> Self { + log::info!("new write {}", data_source); + Self { + id, + r#type, + remote, + data_source, + file_num, + show_hidden, + is_remote, + files: Vec::new(), + total_size: 0, + enable_overwrite_detection, + ..Default::default() + } + } + + pub fn with_files(mut self, files: Vec) -> ResultType { + self.set_files(files)?; + Ok(self) + } + + pub fn new_read( + id: i32, + r#type: JobType, + remote: String, + data_source: DataSource, + file_num: i32, + show_hidden: bool, + is_remote: bool, + enable_overwrite_detection: bool, + ) -> ResultType { + log::info!("new read {}", data_source); + let (files, total_size) = match &data_source { + DataSource::FilePath(p) => { + let p = p.to_str().ok_or(anyhow!("Invalid path"))?; + let files = get_recursive_files(p, show_hidden)?; + let total_size = files.iter().map(|x| x.size).sum(); + (files, total_size) + } + DataSource::MemoryCursor(c) => (Vec::new(), c.get_ref().len() as u64), + }; + Ok(Self { + id, + r#type, + remote, + data_source, + file_num, + show_hidden, + is_remote, + files, + total_size, + enable_overwrite_detection, + ..Default::default() + }) + } + + pub async fn get_buf_data(self) -> ResultType>> { + match self.data_stream { + Some(DataStream::BufStream(mut bs)) => { + bs.flush().await?; + Ok(Some(bs.into_inner().into_inner())) + } + _ => Ok(None), + } + } + + #[inline] + pub fn files(&self) -> &Vec { + &self.files + } + + #[inline] + pub fn set_files(&mut self, files: Vec) -> ResultType<()> { + validate_transfer_file_names(&files)?; + if let DataSource::FilePath(base) = &self.data_source { + for file in &files { + validate_no_symlink_components(base, &file.name)?; + } + } + self.total_size = files.iter().map(|x| x.size).sum(); + self.files = files; + Ok(()) + } + + #[inline] + pub fn set_digest(&mut self, size: u64, modified: u64) { + self.digest.size = size; + self.digest.modified = modified; + } + + #[inline] + pub fn id(&self) -> i32 { + self.id + } + + #[inline] + pub fn total_size(&self) -> u64 { + self.total_size + } + + #[inline] + pub fn finished_size(&self) -> u64 { + self.finished_size + } + + #[inline] + pub fn transferred(&self) -> u64 { + self.transferred + } + + #[inline] + pub fn file_num(&self) -> i32 { + self.file_num + } + + fn resolve_entry_path(&self, base: &PathBuf, name: &str) -> Option { + if self.r#type == JobType::Generic { + match join_validated_path(base, name) { + Ok(path) => Some(path), + Err(err) => { + log::error!("Invalid file name in transfer job {}: {}", self.id, err); + None + } + } + } else { + Some(Self::join(base, name)) + } + } + + pub fn modify_time(&self) { + if self.r#type == JobType::Printer { + return; + } + if let DataSource::FilePath(p) = &self.data_source { + let file_num = self.file_num as usize; + if file_num < self.files.len() { + let entry = &self.files[file_num]; + let Some(path) = self.resolve_entry_path(p, &entry.name) else { + return; + }; + let download_path = format!("{}.download", get_string(&path)); + let digest_path = format!("{}.digest", get_string(&path)); + std::fs::remove_file(digest_path).ok(); + std::fs::rename(download_path, &path).ok(); + filetime::set_file_mtime( + &path, + filetime::FileTime::from_unix_time(entry.modified_time as _, 0), + ) + .ok(); + } + } + } + + pub fn remove_download_file(&self) { + if self.r#type == JobType::Printer { + return; + } + if let DataSource::FilePath(p) = &self.data_source { + let file_num = self.file_num as usize; + if file_num < self.files.len() { + let entry = &self.files[file_num]; + let Some(path) = self.resolve_entry_path(p, &entry.name) else { + return; + }; + let download_path = format!("{}.download", get_string(&path)); + let digest_path = format!("{}.digest", get_string(&path)); + std::fs::remove_file(download_path).ok(); + std::fs::remove_file(digest_path).ok(); + } + } + } + + #[inline] + pub fn set_finished_size_on_resume(&mut self) { + if self.is_resume && self.file_num > 0 { + let finished_size: u64 = self + .files + .iter() + .take(self.file_num as usize) + .map(|file| file.size) + .sum(); + self.finished_size = finished_size; + } + } + + pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { + if block.id != self.id { + bail!("Wrong id"); + } + match &self.data_source { + DataSource::FilePath(p) => { + let file_num = block.file_num as usize; + if file_num >= self.files.len() { + bail!("Wrong file number"); + } + if file_num != self.file_num as usize || self.data_stream.is_none() { + self.modify_time(); + if let Some(DataStream::FileStream(file)) = self.data_stream.as_mut() { + file.sync_all().await?; + } + self.file_num = block.file_num; + let entry = &self.files[file_num]; + let (path, digest_path) = if self.r#type == JobType::Printer { + (p.to_string_lossy().to_string(), None) + } else { + let path = join_validated_path(p, &entry.name)?; + // NOTE: We intentionally keep path-based validation + regular file open here. + // This still has a known TOCTOU window for symlink races, but avoids a large + // cross-platform rewrite for now. + // Revisit with descriptor/handle-based no-follow open in future hardening. + if let Some(pp) = path.parent() { + std::fs::create_dir_all(pp).ok(); + } + let file_path = get_string(&path); + ( + format!("{}.download", &file_path), + Some(format!("{}.digest", &file_path)), + ) + }; + if let Some(dp) = digest_path.as_ref() { + if Path::new(dp).exists() { + std::fs::remove_file(dp)?; + } + } + self.data_stream = Some(DataStream::FileStream(File::create(&path).await?)); + if let Some(dp) = digest_path.as_ref() { + std::fs::write(dp, json!(self.digest).to_string()).ok(); + } + } + } + DataSource::MemoryCursor(c) => { + if self.data_stream.is_none() { + self.data_stream = Some(DataStream::BufStream(TokioBufStream::new(c.clone()))); + } + } + } + if block.compressed { + let tmp = decompress(&block.data); + self.data_stream + .as_mut() + .ok_or(anyhow!("data stream is None"))? + .write_all(&tmp) + .await?; + self.finished_size += tmp.len() as u64; + } else { + self.data_stream + .as_mut() + .ok_or(anyhow!("file is None"))? + .write_all(&block.data) + .await?; + self.finished_size += block.data.len() as u64; + } + self.transferred += block.data.len() as u64; + Ok(()) + } + + #[inline] + pub fn join(p: &PathBuf, name: &str) -> PathBuf { + if name.is_empty() { + p.clone() + } else { + p.join(name) + } + } + + /// Open the data stream for the current file. + /// Returns Ok(true) if job is done, Ok(false) otherwise. + async fn open_data_stream(&mut self) -> ResultType { + let file_num = self.file_num as usize; + match &mut self.data_source { + DataSource::FilePath(p) => { + if file_num >= self.files.len() { + // job done + self.data_stream.take(); + return Ok(true); + }; + if self.data_stream.is_none() { + match File::open(Self::join(p, &self.files[file_num].name)).await { + Ok(file) => { + self.data_stream = Some(DataStream::FileStream(file)); + self.file_confirmed = false; + self.file_is_waiting = false; + } + // On open error, behave the same as validation failure: advance + // to next file and return the error. + Err(err) => { + self.file_num += 1; + self.file_confirmed = false; + self.file_is_waiting = false; + return Err(err.into()); + } + } + } + } + DataSource::MemoryCursor(c) => { + if self.data_stream.is_none() { + let mut t = std::io::Cursor::new(Vec::new()); + std::mem::swap(&mut t, c); + self.data_stream = Some(DataStream::BufStream(TokioBufStream::new(t))); + } + } + } + Ok(false) + } + + /// Get current file's digest (last_modified, file_size) for overwrite detection. + async fn get_current_digest(&self) -> ResultType<(u64, u64)> { + let meta = match self.data_stream.as_ref().ok_or(anyhow!("file is None"))? { + DataStream::FileStream(file) => file.metadata().await?, + DataStream::BufStream(_) => bail!("No digest for buf stream"), + }; + let last_modified = meta + .modified()? + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(); + Ok((last_modified, meta.len())) + } + + async fn init_data_stream(&mut self, stream: &mut hbb_common::Stream) -> ResultType<()> { + if self.open_data_stream().await? { + return Ok(()); + } + if self.r#type == JobType::Generic + && self.enable_overwrite_detection + && !self.file_confirmed() + && !self.file_is_waiting() + { + self.send_current_digest(stream).await?; + self.set_file_is_waiting(true); + } + Ok(()) + } + + /// Initialize data stream for CM (Connection Manager) scenario. + /// Returns digest info (last_modified, file_size) if overwrite detection is enabled, + /// so caller can send it via IPC instead of network stream. + /// Returns Ok(None) if job is done or already initialized. + pub async fn init_data_stream_for_cm(&mut self) -> ResultType> { + if self.open_data_stream().await? { + return Ok(None); + } + // For overwrite detection, return digest info instead of sending via stream + if self.r#type == JobType::Generic + && self.enable_overwrite_detection + && !self.file_confirmed() + && !self.file_is_waiting() + { + let digest = self.get_current_digest().await?; + self.set_file_is_waiting(true); + return Ok(Some(digest)); + } + Ok(None) + } + + pub async fn read(&mut self) -> ResultType> { + if self.r#type == JobType::Generic { + if self.enable_overwrite_detection && !self.file_confirmed() { + return Ok(None); + } + } + + let file_num = self.file_num as usize; + let name = match &self.data_source { + DataSource::FilePath(p) => { + if file_num >= self.files.len() { + self.data_stream.take(); + return Ok(None); + }; + if self.files.len() == 1 && self.files[file_num].name.is_empty() { + p.file_name() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + } else { + &self.files[file_num].name + } + } + DataSource::MemoryCursor(..) => "", + }; + const BUF_SIZE: usize = 128 * 1024; + let mut buf: Vec = vec![0; BUF_SIZE]; + let mut compressed = false; + let mut offset: usize = 0; + loop { + match self + .data_stream + .as_mut() + .ok_or(anyhow!("data stream is None"))? + .read(&mut buf[offset..]) + .await + { + Err(err) => { + self.file_num += 1; + self.data_stream = None; + self.file_confirmed = false; + self.file_is_waiting = false; + return Err(err.into()); + } + Ok(n) => { + offset += n; + if n == 0 || offset == BUF_SIZE { + break; + } + } + } + } + unsafe { buf.set_len(offset) }; + if offset == 0 { + if matches!(self.data_source, DataSource::MemoryCursor(_)) { + self.data_stream.take(); + return Ok(None); + } + self.file_num += 1; + self.data_stream = None; + self.file_confirmed = false; + self.file_is_waiting = false; + } else { + self.finished_size += offset as u64; + if matches!(self.data_source, DataSource::FilePath(_)) && !is_compressed_file(name) { + let tmp = compress(&buf); + if tmp.len() < buf.len() { + buf = tmp; + compressed = true; + } + } + self.transferred += buf.len() as u64; + } + Ok(Some(FileTransferBlock { + id: self.id, + file_num: file_num as _, + data: buf.into(), + compressed, + ..Default::default() + })) + } + + // Only for generic job and file stream + async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> { + let (last_modified, file_size) = self.get_current_digest().await?; + let mut msg = Message::new(); + let mut resp = FileResponse::new(); + resp.set_digest(FileTransferDigest { + id: self.id, + file_num: self.file_num, + last_modified, + file_size, + is_resume: self.is_resume, + ..Default::default() + }); + msg.set_file_response(resp); + stream.send(&msg).await?; + log::info!( + "id: {}, file_num: {}, digest message is sent. waiting for confirm. msg: {:?}", + self.id, + self.file_num, + msg + ); + Ok(()) + } + + pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option) { + self.default_overwrite_strategy = overwrite_strategy; + } + + pub fn default_overwrite_strategy(&self) -> Option { + self.default_overwrite_strategy + } + + pub fn set_file_confirmed(&mut self, file_confirmed: bool) { + log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed); + self.file_confirmed = file_confirmed; + self.file_skipped = false; + } + + pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) { + self.file_is_waiting = file_is_waiting; + } + + #[inline] + pub fn file_is_waiting(&self) -> bool { + self.file_is_waiting + } + + #[inline] + pub fn file_confirmed(&self) -> bool { + self.file_confirmed + } + + /// Indicating whether the last file is skipped + #[inline] + pub fn file_skipped(&self) -> bool { + self.file_skipped + } + + /// Indicating whether the whole task is skipped + #[inline] + pub fn job_skipped(&self) -> bool { + self.file_skipped() && self.files.len() == 1 + } + + /// Check whether the job is completed after `read` returns `None` + /// This is a helper function which gives additional lifecycle when the job reads `None`. + /// If returns `true`, it means we can delete the job automatically. `False` otherwise. + /// + /// [`Note`] + /// Conditions: + /// 1. Files are not waiting for confirmation by peers. + #[inline] + pub fn job_completed(&self) -> bool { + // has no error, Condition 2 + !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) + } + + /// Get job error message, useful for getting status when job had finished + pub fn job_error(&self) -> Option { + if self.job_skipped() { + return Some("skipped".to_string()); + } + None + } + + pub fn set_file_skipped(&mut self) -> bool { + log::debug!("skip file {} in job {}", self.file_num, self.id); + self.data_stream.take(); + self.set_file_confirmed(false); + self.set_file_is_waiting(false); + self.file_num += 1; + self.file_skipped = true; + true + } + + async fn set_stream_offset(&mut self, file_num: usize, offset: u64) { + if let DataSource::FilePath(p) = &self.data_source { + let entry = &self.files[file_num]; + let Some(path) = self.resolve_entry_path(p, &entry.name) else { + return; + }; + let file_path = get_string(&path); + let download_path = format!("{}.download", &file_path); + let digest_path = format!("{}.digest", &file_path); + + let mut f = if Path::new(&download_path).exists() && Path::new(&digest_path).exists() { + // If both download and digest files exist, seek (writer) to the offset + // NOTE: same as write path: best-effort symlink validation happened earlier, + // but this reopen remains TOCTOU-prone by design for now. + match OpenOptions::new() + .create(true) + .write(true) + .open(&download_path) + .await + { + Ok(f) => f, + Err(e) => { + log::warn!("Failed to open file {}: {}", download_path, e); + return; + } + } + } else if Path::new(&file_path).exists() { + // If `file_path` exists, seek (reader) to the offset + match File::open(&file_path).await { + Ok(f) => f, + Err(e) => { + log::warn!("Failed to open file {}: {}", file_path, e); + return; + } + } + } else { + log::warn!( + "File {} not found, cannot seek to offset {}", + file_path, + offset + ); + return; + }; + if f.seek(std::io::SeekFrom::Start(offset)).await.is_ok() { + self.data_stream = Some(DataStream::FileStream(f)); + self.transferred += offset; + self.finished_size += offset; + } + } + } + + pub async fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool { + if self.file_num() != r.file_num { + // This branch will always be hit if: + // 1. `confirm()` is called in `ui_cm_interface.rs` + // 2. Not resuming + // + // It is ok. Because `confirm()` in `ui_cm_interface.rs` is only used for resuming. + log::info!("file num truncated, ignoring"); + } else { + match r.union { + Some(file_transfer_send_confirm_request::Union::Skip(s)) => { + if s { + self.set_file_skipped(); + } else { + self.set_file_confirmed(true); + } + } + Some(file_transfer_send_confirm_request::Union::OffsetBlk(offset)) => { + self.set_file_confirmed(true); + // If offset is greater than 0, we need to seek to the offset + if offset > 0 { + self.set_stream_offset(r.file_num as usize, offset as u64) + .await; + } + } + _ => {} + } + } + true + } + + #[inline] + pub fn gen_meta(&self) -> TransferJobMeta { + TransferJobMeta { + id: self.id, + remote: self.remote.to_string(), + to: self.data_source.to_meta(), + file_num: self.file_num, + show_hidden: self.show_hidden, + is_remote: self.is_remote, + } + } +} + +#[inline] +pub fn new_error(id: i32, err: T, file_num: i32) -> Message { + let mut resp = FileResponse::new(); + resp.set_error(FileTransferError { + id, + error: err.to_string(), + file_num, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn new_dir(id: i32, path: String, files: Vec) -> Message { + let mut resp = FileResponse::new(); + resp.set_dir(FileDirectory { + id, + path, + entries: files, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn new_block(block: FileTransferBlock) -> Message { + let mut resp = FileResponse::new(); + resp.set_block(block); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message { + let mut msg_out = Message::new(); + let mut action = FileAction::new(); + action.set_send_confirm(r); + msg_out.set_file_action(action); + msg_out +} + +#[inline] +pub fn new_receive( + id: i32, + path: String, + file_num: i32, + files: Vec, + total_size: u64, +) -> Message { + let mut action = FileAction::new(); + action.set_receive(FileTransferReceiveRequest { + id, + path, + files, + file_num, + total_size, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_action(action); + msg_out +} + +#[inline] +pub fn new_send( + id: i32, + r#type: JobType, + path: String, + file_num: i32, + include_hidden: bool, +) -> Message { + log::info!("new send: {}, id: {}", path, id); + let mut action = FileAction::new(); + let t: file_transfer_send_request::FileType = r#type.into(); + action.set_send(FileTransferSendRequest { + id, + path, + include_hidden, + file_num, + file_type: t.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_action(action); + msg_out +} + +#[inline] +pub fn new_done(id: i32, file_num: i32) -> Message { + let mut resp = FileResponse::new(); + resp.set_done(FileTransferDone { + id, + file_num, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_file_response(resp); + msg_out +} + +#[inline] +pub fn remove_job(id: i32, jobs: &mut Vec) -> Option { + jobs.iter() + .position(|x| x.id() == id) + .map(|index| jobs.remove(index)) +} + +#[inline] +pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { + jobs.iter_mut().find(|x| x.id() == id) +} + +#[inline] +pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> { + jobs.iter().find(|x| x.id() == id) +} + +async fn init_jobs(jobs: &mut Vec, stream: &mut hbb_common::Stream) -> ResultType<()> { + for job in jobs.iter_mut() { + if job.is_last_job { + continue; + } + if let Err(err) = job.init_data_stream(stream).await { + stream + .send(&new_error(job.id(), err, job.file_num())) + .await?; + } + } + Ok(()) +} + +pub async fn handle_read_jobs( + jobs: &mut Vec, + stream: &mut hbb_common::Stream, +) -> ResultType { + init_jobs(jobs, stream).await?; + + let mut job_log = Default::default(); + let mut finished = Vec::new(); + for job in jobs.iter_mut() { + if job.is_last_job { + continue; + } + match job.read().await { + Err(err) => { + stream + .send(&new_error(job.id(), err, job.file_num())) + .await?; + } + Ok(Some(block)) => { + stream.send(&new_block(block)).await?; + } + Ok(None) => { + if job.job_completed() { + job_log = serialize_transfer_job(job, true, false, ""); + finished.push(job.id()); + match job.job_error() { + Some(err) => { + job_log = serialize_transfer_job(job, false, false, &err); + stream + .send(&new_error(job.id(), err, job.file_num())) + .await? + } + None => stream.send(&new_done(job.id(), job.file_num())).await?, + } + } else { + // waiting confirmation. + } + } + } + // Break to handle jobs one by one. + break; + } + for id in finished { + let _ = remove_job(id, jobs); + } + Ok(job_log) +} + +pub fn remove_all_empty_dir(path: &Path) -> ResultType<()> { + let fd = read_dir(path, true)?; + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::Dir) => { + remove_all_empty_dir(&path.join(&entry.name)).ok(); + } + Ok(FileType::DirLink) | Ok(FileType::FileLink) => { + std::fs::remove_file(path.join(&entry.name)).ok(); + } + _ => {} + } + } + std::fs::remove_dir(path).ok(); + Ok(()) +} + +#[inline] +pub fn remove_file(file: &str) -> ResultType<()> { + validate_fs_path_argument(file, "file path")?; + std::fs::remove_file(get_path(file))?; + Ok(()) +} + +#[inline] +pub fn create_dir(dir: &str) -> ResultType<()> { + validate_fs_path_argument(dir, "directory path")?; + std::fs::create_dir_all(get_path(dir))?; + Ok(()) +} + +#[inline] +pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> { + validate_fs_path_argument(path, "path")?; + if new_name.is_empty() { + bail!("new file name cannot be empty"); + } + validate_file_name_no_traversal(new_name)?; + let path = std::path::Path::new(&path); + if path.exists() { + let dir = path + .parent() + .ok_or(anyhow!("Parent directoy of {path:?} not exists"))?; + let new_path = dir.join(&new_name); + std::fs::rename(&path, &new_path)?; + Ok(()) + } else { + bail!("{path:?} not exists"); + } +} + +#[inline] +pub fn transform_windows_path(entries: &mut Vec) { + for entry in entries { + entry.name = entry.name.replace('\\', "/"); + } +} + +pub enum DigestCheckResult { + IsSame, + NeedConfirm(FileTransferDigest), + NoSuchFile, +} + +#[inline] +pub fn is_write_need_confirmation( + is_resume: bool, + file_path: &str, + digest: &FileTransferDigest, +) -> ResultType { + let path = Path::new(file_path); + let digest_file = format!("{}.digest", file_path); + let download_file = format!("{}.download", file_path); + if is_resume && Path::new(&digest_file).exists() && Path::new(&download_file).exists() { + // If the digest file exists, it means the file was transferred before. + // We can use the digest file to check whether the file is the same. + if let Ok(content) = std::fs::read_to_string(digest_file) { + if let Ok(local_digest) = serde_json::from_str::(&content) { + let is_identical = local_digest.modified == digest.last_modified + && local_digest.size == digest.file_size; + if is_identical { + if let Ok(download_metadata) = std::fs::metadata(download_file) { + // Get the file size of the local file + // Only send confirmation if the file is not empty. + let transferred_size = download_metadata.len(); + if transferred_size > 0 { + return Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { + id: digest.id, + file_num: digest.file_num, + last_modified: digest.last_modified, + file_size: digest.file_size, + is_identical, + transferred_size, + ..Default::default() + })); + } + } + } + } + } + } + + if path.exists() && path.is_file() { + let metadata = std::fs::metadata(path)?; + let modified_time = metadata.modified()?; + let remote_mt = Duration::from_secs(digest.last_modified); + let local_mt = modified_time.duration_since(UNIX_EPOCH)?; + // [Note] + // We decide to give the decision whether to override the existing file to users, + // which obey the behavior of the file manager in our system. + let mut is_identical = false; + if remote_mt == local_mt && digest.file_size == metadata.len() { + is_identical = true; + } + Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { + id: digest.id, + file_num: digest.file_num, + last_modified: local_mt.as_secs(), + file_size: metadata.len(), + is_identical, + ..Default::default() + })) + } else { + // If the file does not exist, or the digest file and download file do not exist, we return NoSuchFile. + Ok(DigestCheckResult::NoSuchFile) + } +} + +pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String { + let mut v = vec![]; + for job in jobs { + let value = serde_json::to_value(job).unwrap_or_default(); + v.push(value); + } + serde_json::to_string(&v).unwrap_or_default() +} + +pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String { + let mut value = serde_json::to_value(job).unwrap_or_default(); + value["done"] = json!(done); + value["cancel"] = json!(cancel); + value["error"] = json!(error); + serde_json::to_string(&value).unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestTempDir { + path: PathBuf, + } + + impl TestTempDir { + fn new(prefix: &str) -> Self { + Self { + path: unique_temp_dir(prefix), + } + } + + fn join(&self, path: &str) -> PathBuf { + self.path.join(path) + } + } + + impl Drop for TestTempDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.path); + } + } + + fn unique_temp_dir(prefix: &str) -> PathBuf { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + std::env::temp_dir().join(format!("{}_{}_{}", prefix, std::process::id(), timestamp)) + } + + fn new_file_entry(name: &str) -> FileEntry { + let mut entry = FileEntry::new(); + entry.name = name.to_string(); + entry + } + + fn new_validation_job(id: i32) -> TransferJob { + TransferJob::new_write( + id, + JobType::Generic, + "/fake/remote".to_string(), + DataSource::FilePath(std::env::temp_dir().join(format!("rustdesk_validation_{id}"))), + 0, + false, + true, + false, + ) + } + + fn new_write_job(id: i32, download_dir: PathBuf, name: &str) -> ResultType { + let job = TransferJob::new_write( + id, + JobType::Generic, + "/fake/remote".to_string(), + DataSource::FilePath(download_dir), + 0, + false, + true, + false, + ) + .with_files(vec![new_file_entry(name)])?; + Ok(job) + } + + fn assert_err_contains(err: anyhow::Error, expected: &str) { + assert!( + err.to_string().contains(expected), + "expected error containing '{}', got: {}", + expected, + err + ); + } + + #[test] + fn path_traversal_e2e_write_rejects_relative_escape() { + let tmp_root = TestTempDir::new("rustdesk_e2e_relative"); + let downloads = tmp_root.join("downloads"); + std::fs::create_dir_all(&downloads).expect("create downloads dir"); + + let err = new_write_job(1, downloads, "../traversal_proof.txt") + .expect_err("relative path traversal must be rejected"); + assert_err_contains(err, "path traversal"); + assert!(!tmp_root.join("traversal_proof.txt").exists()); + } + + #[test] + fn path_traversal_e2e_write_rejects_absolute_path() { + let tmp_root = TestTempDir::new("rustdesk_e2e_absolute"); + let downloads = tmp_root.join("downloads"); + let absolute_target = tmp_root.join("fake_ssh").join("authorized_keys"); + std::fs::create_dir_all(&downloads).expect("create downloads dir"); + + let err = new_write_job(2, downloads, &absolute_target.to_string_lossy()) + .expect_err("absolute path must be rejected"); + assert_err_contains(err, "absolute path"); + assert!(!absolute_target.exists()); + } + + #[test] + #[cfg_attr(windows, ignore = "requires symlink privilege to create test symlink")] + fn path_traversal_e2e_write_rejects_symlink_escape() { + let tmp_root = TestTempDir::new("rustdesk_e2e_symlink"); + let downloads = tmp_root.join("downloads"); + let outside = tmp_root.join("outside"); + let escaped_target = outside.join("escape.txt"); + std::fs::create_dir_all(&downloads).expect("create downloads dir"); + std::fs::create_dir_all(&outside).expect("create outside dir"); + + let symlink_path = downloads.join("link"); + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(&outside, &symlink_path).expect("create symlink for test"); + } + #[cfg(windows)] + { + use std::os::windows::fs::symlink_dir; + symlink_dir(&outside, &symlink_path).expect("create directory symlink for test"); + } + + let err = new_write_job(3, downloads, "link/escape.txt") + .expect_err("symlink traversal must be rejected"); + assert_err_contains(err, "symlink"); + assert!(!escaped_target.exists()); + } + + #[test] + fn set_files_allows_single_empty_name_for_single_file_transfer() { + let mut job = new_validation_job(101); + assert!(job.set_files(vec![new_file_entry("")]).is_ok()); + } + + #[test] + fn set_files_rejects_empty_name_in_multi_file_transfer() { + let mut job = new_validation_job(102); + let err = job + .set_files(vec![new_file_entry(""), new_file_entry("ok.txt")]) + .expect_err("empty name in multi-file transfer must be rejected"); + assert_err_contains(err, "empty file name"); + } + + #[test] + fn set_files_rejects_null_byte_name() { + let mut job = new_validation_job(103); + let err = job + .set_files(vec![new_file_entry("bad\0name.txt")]) + .expect_err("null byte in file name must be rejected"); + assert_err_contains(err, "null bytes"); + } + + #[test] + fn set_files_rejects_mixed_entries_when_one_is_traversal() { + let mut job = new_validation_job(104); + let err = job + .set_files(vec![ + new_file_entry("safe/file.txt"), + new_file_entry("../../escape.txt"), + ]) + .expect_err("any traversal entry must reject the full file list"); + assert_err_contains(err, "path traversal"); + } + + #[cfg(windows)] + #[test] + fn set_files_rejects_unc_absolute_path() { + let mut job = new_validation_job(105); + let err = job + .set_files(vec![new_file_entry("\\\\server\\share\\payload.txt")]) + .expect_err("UNC absolute path must be rejected"); + assert_err_contains(err, "absolute path"); + } + + #[cfg(not(windows))] + #[test] + fn set_files_allows_backslash_prefixed_name_on_unix() { + let mut job = new_validation_job(105); + assert!(job + .set_files(vec![new_file_entry("\\\\server\\share\\payload.txt")]) + .is_ok()); + } + + #[test] + fn remove_file_rejects_empty_path() { + let err = remove_file("").expect_err("empty file path must be rejected"); + assert_err_contains(err, "cannot be empty"); + } + + #[test] + fn remove_file_rejects_null_byte_path() { + let err = remove_file("bad\0path").expect_err("null byte path must be rejected"); + assert_err_contains(err, "null bytes"); + } + + #[test] + fn create_dir_rejects_empty_path() { + let err = create_dir("").expect_err("empty directory path must be rejected"); + assert_err_contains(err, "cannot be empty"); + } + + #[test] + fn create_dir_rejects_null_byte_path() { + let err = create_dir("bad\0path").expect_err("null byte path must be rejected"); + assert_err_contains(err, "null bytes"); + } + + #[test] + fn rename_file_rejects_invalid_new_name() { + let tmp_root = TestTempDir::new("rustdesk_rename_invalid"); + let src = tmp_root.join("source.txt"); + std::fs::create_dir_all(&tmp_root.path).expect("create temp dir"); + std::fs::write(&src, b"content").expect("create source file"); + + let src_str = src.to_string_lossy().to_string(); + + let err_empty = + rename_file(&src_str, "").expect_err("empty new file name must be rejected"); + assert_err_contains(err_empty, "cannot be empty"); + + let err_traversal = rename_file(&src_str, "../escape.txt") + .expect_err("traversal new file name must be rejected"); + assert_err_contains(err_traversal, "path traversal"); + + let err_null = rename_file(&src_str, "bad\0name.txt") + .expect_err("null byte in new file name must be rejected"); + assert_err_contains(err_null, "null bytes"); + + #[cfg(windows)] + { + let err_abs = rename_file(&src_str, "C:\\Windows\\Temp\\payload.txt") + .expect_err("absolute new file name must be rejected"); + assert_err_contains(err_abs, "absolute path"); + } + #[cfg(not(windows))] + { + let err_abs = rename_file(&src_str, "/tmp/payload.txt") + .expect_err("absolute new file name must be rejected"); + assert_err_contains(err_abs, "absolute path"); + } + } + + #[test] + fn rename_file_accepts_valid_new_name() { + let tmp_root = TestTempDir::new("rustdesk_rename_ok"); + let src = tmp_root.join("rename_src.txt"); + let dst = tmp_root.join("renamed.txt"); + std::fs::create_dir_all(&tmp_root.path).expect("create temp dir"); + std::fs::write(&src, b"content").expect("create source file"); + + let src_str = src.to_string_lossy().to_string(); + rename_file(&src_str, "renamed.txt").expect("rename should succeed"); + + assert!(!src.exists()); + assert!(dst.exists()); + } + + #[cfg(windows)] + #[test] + fn set_files_rejects_windows_drive_absolute_path() { + let mut job = new_validation_job(106); + let err = job + .set_files(vec![new_file_entry("C:\\Windows\\Temp\\payload.txt")]) + .expect_err("drive-letter absolute path must be rejected"); + assert_err_contains(err, "absolute path"); + } + + #[cfg(windows)] + #[test] + fn set_files_rejects_windows_verbatim_drive_absolute_path() { + let mut job = new_validation_job(1061); + let err = job + .set_files(vec![new_file_entry(r"\\?\C:\Windows\Temp\x.txt")]) + .expect_err("verbatim drive absolute path must be rejected"); + assert_err_contains(err, "absolute path"); + } +} diff --git a/libs/base/src/keyboard.rs b/libs/base/src/keyboard.rs new file mode 100644 index 000000000..10979f520 --- /dev/null +++ b/libs/base/src/keyboard.rs @@ -0,0 +1,39 @@ +use std::{fmt, slice::Iter, str::FromStr}; + +use crate::protos::message::KeyboardMode; + +impl fmt::Display for KeyboardMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyboardMode::Legacy => write!(f, "legacy"), + KeyboardMode::Map => write!(f, "map"), + KeyboardMode::Translate => write!(f, "translate"), + KeyboardMode::Auto => write!(f, "auto"), + } + } +} + +impl FromStr for KeyboardMode { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "legacy" => Ok(KeyboardMode::Legacy), + "map" => Ok(KeyboardMode::Map), + "translate" => Ok(KeyboardMode::Translate), + "auto" => Ok(KeyboardMode::Auto), + _ => Err(()), + } + } +} + +impl KeyboardMode { + pub fn iter() -> Iter<'static, KeyboardMode> { + static KEYBOARD_MODES: [KeyboardMode; 4] = [ + KeyboardMode::Legacy, + KeyboardMode::Map, + KeyboardMode::Translate, + KeyboardMode::Auto, + ]; + KEYBOARD_MODES.iter() + } +} diff --git a/libs/base/src/lib.rs b/libs/base/src/lib.rs new file mode 100644 index 000000000..4389428b4 --- /dev/null +++ b/libs/base/src/lib.rs @@ -0,0 +1,7 @@ +pub mod config; +pub mod fs; +pub mod keyboard; +pub mod platform; +pub mod protos; + +pub use protos::message as message_proto; diff --git a/libs/base/src/platform/linux.rs b/libs/base/src/platform/linux.rs new file mode 100644 index 000000000..aa1228ca6 --- /dev/null +++ b/libs/base/src/platform/linux.rs @@ -0,0 +1,618 @@ +use hbb_common::ResultType; +// Kept in hbb_common because `config::patch()` needs the shell lookup; re-exported +// here so the long-standing `platform::linux::CMD_SH` paths are unchanged. +pub use hbb_common::sh::{run_cmds_trim_newline, CMD_LOGINCTL, CMD_PS, CMD_SH}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + process::Command, +}; +use users::{get_current_uid, get_user_by_uid, os::unix::UserExt}; + +use sctk::{ + output::OutputData, + output::{OutputHandler, OutputState}, + reexports::client::protocol::wl_output::WlOutput, + reexports::client::{globals, Proxy}, + reexports::client::{Connection, QueueHandle}, + registry::{ProvidesRegistryState, RegistryState}, +}; + +lazy_static::lazy_static! { + pub static ref DISTRO: Distro = Distro::new(); +} + +pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; +pub const DISPLAY_SERVER_X11: &str = "x11"; +pub const DISPLAY_DESKTOP_KDE: &str = "KDE"; + +pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; + +pub struct Distro { + pub name: String, + pub version_id: String, +} + +impl Distro { + fn new() -> Self { + let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + Self { name, version_id } + } +} + +// Deprecated. Use `base::platform::linux::is_kde_session()` instead for now. +// Or we need to set the correct environment variable in the server process. +#[inline] +pub fn is_kde() -> bool { + if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) { + env == DISPLAY_DESKTOP_KDE + } else { + false + } +} + +// Don't use `base::platform::linux::is_kde()` here. +// It's not correct in the server process. +pub fn is_kde_session() -> bool { + std::process::Command::new(CMD_SH.as_str()) + .arg("-c") + .arg("pgrep -f kded[0-9]+") + .stdout(std::process::Stdio::piped()) + .output() + .map(|o| !o.stdout.is_empty()) + .unwrap_or(false) +} + +#[inline] +pub fn is_gdm_user(username: &str) -> bool { + username == "gdm" || username == "sddm" + // || username == "lightgdm" +} + +#[inline] +pub fn is_desktop_wayland() -> bool { + get_display_server() == DISPLAY_SERVER_WAYLAND +} + +#[inline] +pub fn is_x11_or_headless() -> bool { + !is_desktop_wayland() +} + +// -1 +const INVALID_SESSION: &str = "4294967295"; + +pub fn get_display_server() -> String { + // Check for forced display server environment variable first + if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") { + return forced_display; + } + + // Check if `loginctl` can be called successfully + if run_loginctl(None).is_err() { + return DISPLAY_SERVER_X11.to_owned(); + } + + let mut session = get_values_of_seat0(&[0])[0].clone(); + if session.is_empty() { + // loginctl has not given the expected output. try something else. + if let Ok(sid) = std::env::var("XDG_SESSION_ID") { + // could also execute "cat /proc/self/sessionid" + session = sid; + } + if session.is_empty() { + session = run_cmds("cat /proc/self/sessionid").unwrap_or_default(); + if session == INVALID_SESSION { + session = "".to_owned(); + } + } + } + if session.is_empty() { + std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned()) + } else { + get_display_server_of_session(&session) + } +} + +pub fn get_display_server_of_session(session: &str) -> String { + let mut display_server = if let Ok(output) = + run_loginctl(Some(vec!["show-session", "-p", "Type", session])) + // Check session type of the session + { + String::from_utf8_lossy(&output.stdout) + .replace("Type=", "") + .trim_end() + .into() + } else { + "".to_owned() + }; + if display_server.is_empty() || display_server == "tty" || display_server == "unspecified" { + if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { + if !sestype.is_empty() { + return sestype.to_lowercase(); + } + } + display_server = "x11".to_owned(); + } + display_server.to_lowercase() +} + +#[inline] +fn line_values(indices: &[usize], line: &str) -> Vec { + indices + .into_iter() + .map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned()) + .collect::>() +} + +#[inline] +pub fn get_values_of_seat0(indices: &[usize]) -> Vec { + _get_values_of_seat0(indices, true) +} + +#[inline] +pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec { + _get_values_of_seat0(indices, false) +} + +// Ignore "3 sessions listed." +fn ignore_loginctl_line(line: &str) -> bool { + line.contains("sessions") || line.split(" ").count() < 4 +} + +fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec { + if let Ok(output) = run_loginctl(None) { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if ignore_loginctl_line(line) { + continue; + } + if line.contains("seat0") { + if let Some(sid) = line.split_whitespace().next() { + if is_active(sid) { + if ignore_gdm_wayland { + if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) + && get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND + { + continue; + } + } + return line_values(indices, line); + } + } + } + } + + // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 + for line in String::from_utf8_lossy(&output.stdout).lines() { + if ignore_loginctl_line(line) { + continue; + } + if let Some(sid) = line.split_whitespace().next() { + if is_active(sid) { + let d = get_display_server_of_session(sid); + if ignore_gdm_wayland { + if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) + && d == DISPLAY_SERVER_WAYLAND + { + continue; + } + } + if d == "tty" || d == "unspecified" { + continue; + } + return line_values(indices, line); + } + } + } + } + + line_values(indices, "") +} + +pub fn is_active(sid: &str) -> bool { + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { + String::from_utf8_lossy(&output.stdout).contains("active") + } else { + false + } +} + +pub fn is_active_and_seat0(sid: &str) -> bool { + if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) { + String::from_utf8_lossy(&output.stdout).contains("State=active") + && String::from_utf8_lossy(&output.stdout).contains("Seat=seat0") + } else { + false + } +} + +// Check both "Lock" and "Switch user" +pub fn is_session_locked(sid: &str) -> bool { + if let Ok(output) = run_loginctl(Some(vec!["show-session", sid, "--property=LockedHint"])) { + String::from_utf8_lossy(&output.stdout).contains("LockedHint=yes") + } else { + false + } +} + +// **Note** that the return value here, the last character is '\n'. +// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end. +pub fn run_cmds(cmds: &str) -> ResultType { + let output = std::process::Command::new(CMD_SH.as_str()) + .args(vec!["-c", cmds]) + .output()?; + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +fn run_loginctl(args: Option>) -> std::io::Result { + if std::env::var("FLATPAK_ID").is_ok() { + let mut l_args = CMD_LOGINCTL.to_string(); + if let Some(a) = args.as_ref() { + l_args = format!("{} {}", l_args, a.join(" ")); + } + let res = std::process::Command::new("flatpak-spawn") + .args(vec![String::from("--host"), l_args]) + .output(); + if res.is_ok() { + return res; + } + } + let mut cmd = std::process::Command::new(CMD_LOGINCTL.as_str()); + if let Some(a) = args { + return cmd.args(a).output(); + } + cmd.output() +} + +/// forever: may not work +#[cfg(target_os = "linux")] +pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { + let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ + ("notify-send", [title, msg].to_vec()), + ( + "zenity", + [ + "--info", + "--timeout", + if forever { "0" } else { "3" }, + "--title", + title, + "--text", + msg, + ] + .to_vec(), + ), + ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), + ( + "xmessage", + [ + "-center", + "-timeout", + if forever { "0" } else { "3" }, + title, + msg, + ] + .to_vec(), + ), + ]); + for (k, v) in cmds { + if Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + hbb_common::bail!("failed to post system message"); +} + +#[derive(Debug, Clone, serde_derive::Serialize, serde_derive::Deserialize)] +pub struct WaylandDisplayInfo { + pub name: String, + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub logical_size: Option<(i32, i32)>, + pub refresh_rate: i32, + /// Output rotation in degrees (0/90/180/270), from `wl_output.geometry`. The mode keeps its + /// unrotated dimensions and `logical_size` arrives already swapped, so without this field a + /// rotated output is indistinguishable from a scaled one. Flipped variants map to their + /// rotation. Defaulted so a serialized snapshot from an older probe child still deserializes. + #[serde(default)] + pub transform: i32, +} + +/// The isolated socket-probe fallback, in its own file and behind the `wayland_probe` feature so +/// the base Wayland path never compiles it. The DRM login-screen build turns it on. +#[cfg(feature = "wayland_probe")] +pub mod wayland_probe; +#[cfg(feature = "wayland_probe")] +pub use wayland_probe::{wayland_display_probe_child_main, WAYLAND_DISPLAY_PROBE_ARG}; + +// Retrieves information about all connected displays via the Wayland protocol. +pub fn get_wayland_displays() -> ResultType> { + // Read before connecting: `connect_to_env` consumes `WAYLAND_SOCKET`. Only the probe fallback + // needs this, so it is computed only when that feature is compiled in. + #[cfg(feature = "wayland_probe")] + let named_endpoint = wayland_probe::env_names_wayland_endpoint(); + match Connection::connect_to_env() { + Ok(conn) => collect_wayland_displays(&conn), + // Without the feature, the connect error is final, exactly as before this fallback existed. + #[cfg(not(feature = "wayland_probe"))] + Err(err) => Err(err.into()), + #[cfg(feature = "wayland_probe")] + Err(err) => wayland_probe::wayland_displays_from_runtime_dir(named_endpoint) + .map_err(|fallback_err| anyhow::anyhow!("{err}; {fallback_err}")), + } +} + +/// `wl_output::Transform` as degrees. Flipped variants report their rotation ONLY: wayland +/// defines them as a vertical-axis mirror followed by the rotation, and the mirror half is +/// dropped here - a consumer correcting frames by this value serves a flipped output mirrored. +/// Said once in the log rather than silently, because no compositor of ours produces a flipped +/// output to measure the mirror half against; carrying it must wait for a measured producer. +fn transform_degrees(t: sctk::reexports::client::protocol::wl_output::Transform) -> i32 { + use sctk::reexports::client::protocol::wl_output::Transform; + match t { + Transform::Normal => 0, + Transform::_90 => 90, + Transform::_180 => 180, + Transform::_270 => 270, + Transform::Flipped | Transform::Flipped90 | Transform::Flipped180 + | Transform::Flipped270 => { + static FLIPPED_WARNED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + if !FLIPPED_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log::warn!( + "an output reports a flipped transform ({t:?}); only its rotation is \ + corrected, the mirror is not" + ); + } + match t { + Transform::Flipped90 => 90, + Transform::Flipped180 => 180, + Transform::Flipped270 => 270, + _ => 0, + } + } + _ => 0, + } +} + +fn collect_wayland_displays(conn: &Connection) -> ResultType> { + struct WaylandEnv { + registry_state: RegistryState, + output_state: OutputState, + } + + impl OutputHandler for WaylandEnv { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + } + + impl ProvidesRegistryState for WaylandEnv { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers![OutputState]; + } + + sctk::delegate_output!(WaylandEnv); + sctk::delegate_registry!(WaylandEnv); + + let (globals, mut event_queue) = globals::registry_queue_init(conn)?; + let queue_handle = event_queue.handle(); + + let registry_state = RegistryState::new(&globals); + let output_state = OutputState::new(&globals, &queue_handle); + + let mut environment = WaylandEnv { + registry_state, + output_state, + }; + + event_queue.roundtrip(&mut environment)?; + + let outputs: Vec<_> = environment.output_state.outputs().collect(); + let mut display_infos = Vec::new(); + + for output in outputs { + if let Some(output_data) = output.data::() { + output_data.with_output_info(|info| { + if let Some(mode) = info.modes.iter().find(|m| m.current) { + // wlroots compositors leave wl_output.geometry at (0, 0) for every output and + // publish the real layout only through xdg-output, so taking `location` there + // stacks the whole desktop on the origin. Mutter fills both, so this stays a + // no-op on GNOME. + let (x, y) = info.logical_position.unwrap_or(info.location); + let (width, height) = mode.dimensions; + let refresh_rate = mode.refresh_rate; + let name = info.name.clone().unwrap_or_default(); + let logical_size = info.logical_size; + let transform = transform_degrees(info.transform); + display_infos.push(WaylandDisplayInfo { + name, + x, + y, + width, + height, + logical_size, + refresh_rate, + transform, + }); + } + }); + } + } + + Ok(display_infos) +} + +/// Escape a string for safe use in shell commands by wrapping in single quotes. +/// +/// This function handles the edge case of single quotes within the string by: +/// 1. Ending the current single-quoted section +/// 2. Adding an escaped single quote +/// 3. Starting a new single-quoted section +/// +/// Example: "it's here" -> "'it'\''s here'" +#[inline] +pub fn shell_quote(s: &str) -> String { + format!("'{}'", s.replace("'", "'\\''")) +} + +/// Get the current user's home directory via getpwuid (trusted source). +/// +/// This function uses the system's password database (via `getpwuid`) to retrieve +/// the home directory, avoiding the security risk of relying on the `HOME` +/// environment variable which can be manipulated by untrusted input. +/// +/// # Returns +/// - `Some(PathBuf)` if the home directory was found and exists +/// - `None` if the user lookup failed or the directory doesn't exist +/// +/// # Security +/// This function is designed to be safe against confused-deputy attacks where +/// an attacker might manipulate environment variables to influence privileged +/// operations. +pub fn get_home_dir_trusted() -> Option { + let uid = get_current_uid(); + match get_user_by_uid(uid) { + Some(user) => { + let home = user.home_dir(); + if Path::is_dir(home) { + Some(PathBuf::from(home)) + } else { + log::warn!( + "Home directory for uid {} does not exist or is not a directory: {:?}", + uid, + home + ); + None + } + } + None => { + log::warn!("Failed to get user info for uid {}", uid); + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transform_degrees_maps_all_eight_variants() { + use sctk::reexports::client::protocol::wl_output::Transform; + // Flipped variants report their rotation: the frame still needs that turn to read + // upright, and the mirror half has no producer among desktop compositors to test. + for (t, deg) in [ + (Transform::Normal, 0), + (Transform::_90, 90), + (Transform::_180, 180), + (Transform::_270, 270), + (Transform::Flipped, 0), + (Transform::Flipped90, 90), + (Transform::Flipped180, 180), + (Transform::Flipped270, 270), + ] { + assert_eq!(transform_degrees(t), deg, "{t:?}"); + } + } + + #[test] + fn test_display_info_without_transform_defaults_to_zero() { + // A snapshot serialized by an older probe child carries no transform field; it must + // deserialize with 0 rather than fail, or a greeter-side child update becomes a + // lockstep upgrade. + let old = r#"{"name":"HDMI-1","x":0,"y":0,"width":1920,"height":1080,"logical_size":null,"refresh_rate":60}"#; + let info: WaylandDisplayInfo = serde_json::from_str(old).unwrap(); + assert_eq!(info.transform, 0); + let roundtrip: WaylandDisplayInfo = + serde_json::from_str(&serde_json::to_string(&info).unwrap()).unwrap(); + assert_eq!(roundtrip.transform, 0); + } + + #[test] + fn test_run_cmds_trim_newline() { + assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123"); + assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123"); + assert_eq!( + run_cmds_trim_newline("whoami").unwrap() + "\n", + run_cmds("whoami").unwrap() + ); + } + + /// Test get_home_dir_trusted: returns valid path and ignores HOME env var + #[test] + fn test_get_home_dir_trusted() { + let original_home = std::env::var("HOME").ok(); + + // Set HOME to a fake/malicious path + std::env::set_var("HOME", "/tmp/fake_malicious_home"); + let result = get_home_dir_trusted(); + + // Restore original HOME + match original_home { + Some(home) => std::env::set_var("HOME", home), + None => std::env::remove_var("HOME"), + } + + // Verify: returns valid path that is NOT the fake HOME + if let Some(path) = result { + assert!(path.is_absolute(), "Path should be absolute: {:?}", path); + assert!(path.is_dir(), "Path should be a directory: {:?}", path); + assert_ne!( + path.to_string_lossy(), + "/tmp/fake_malicious_home", + "Should not use HOME env var" + ); + } + } + + /// Test shell_quote with normal strings + #[test] + fn test_shell_quote_normal() { + assert_eq!(shell_quote("hello"), "'hello'"); + assert_eq!(shell_quote("/home/user"), "'/home/user'"); + } + + /// Test shell_quote with spaces + #[test] + fn test_shell_quote_spaces() { + assert_eq!(shell_quote("/home/my user/file"), "'/home/my user/file'"); + assert_eq!(shell_quote("path with spaces"), "'path with spaces'"); + } + + /// Test shell_quote with single quotes (the tricky case) + #[test] + fn test_shell_quote_single_quotes() { + assert_eq!(shell_quote("it's"), "'it'\\''s'"); + assert_eq!(shell_quote("don't stop"), "'don'\\''t stop'"); + } + + /// Test shell_quote with shell metacharacters + #[test] + fn test_shell_quote_metacharacters() { + // These should all be safely quoted + assert_eq!(shell_quote("test;rm -rf /"), "'test;rm -rf /'"); + assert_eq!(shell_quote("$(whoami)"), "'$(whoami)'"); + assert_eq!(shell_quote("`id`"), "'`id`'"); + assert_eq!(shell_quote("a && b"), "'a && b'"); + assert_eq!(shell_quote("a | b"), "'a | b'"); + } +} diff --git a/libs/base/src/platform/linux/wayland_probe.rs b/libs/base/src/platform/linux/wayland_probe.rs new file mode 100644 index 000000000..9a9370224 --- /dev/null +++ b/libs/base/src/platform/linux/wayland_probe.rs @@ -0,0 +1,349 @@ +//! Isolated Wayland display probe: enumerates a compositor over a runtime-directory socket when +//! the environment names no endpoint (a greeter's `--server` and the root service are given no +//! compositor variables). Gated behind the `wayland_probe` feature so the base Wayland path is +//! untouched — a consumer that does not build the DRM login-screen backend never compiles this, +//! and `get_wayland_displays` keeps its original behavior of returning the connect error. + +use super::{collect_wayland_displays, get_values_of_seat0_with_gdm_wayland, WaylandDisplayInfo}; +use hbb_common::{bail, ResultType}; +use sctk::reexports::client::Connection; +use std::path::{Path, PathBuf}; + +const RUNTIME_DIR_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2); + +/// The argument the consumer binary must dispatch to `wayland_display_probe_child_main` before +/// any other startup work; see that function for why the probe is its own process. +pub const WAYLAND_DISPLAY_PROBE_ARG: &str = "--wayland-display-probe"; + +/// First stdout line of a probe child. A binary that does not dispatch the arg never prints it. +const WAYLAND_PROBE_MAGIC: &str = "wayland-display-probe-v1"; + +/// Latched on a failed handshake: a consumer that does not dispatch the probe arg runs its NORMAL +/// startup instead, and this path re-enters every enumeration cycle. +static PROBE_UNSUPPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +static RUNTIME_DIR_PROBE_BUSY: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +/// Clears the in-flight flag on every exit path of the parent, error arms included. +struct ProbeBusyGuard; + +impl Drop for ProbeBusyGuard { + fn drop(&mut self) { + RUNTIME_DIR_PROBE_BUSY.store(false, std::sync::atomic::Ordering::Release); + } +} + +/// Entry point of the isolated probe process. The consumer binary dispatches +/// `WAYLAND_DISPLAY_PROBE_ARG` here first, before config, logging or any other startup work. +/// +/// Its own process because the release profile builds with panic=abort: sctk panics on malformed +/// protocol bytes, and in-process that abort takes the whole server down. Here it takes down only +/// this child, which the parent reports as a failed probe. The seat0 lookup also runs in here, so +/// the parent's single deadline bounds the loginctl reads too. +pub fn wayland_display_probe_child_main() -> ! { + use std::io::Write; + // The handshake first, so the parent can tell this entry point ran and not a consumer binary + // that fell through to its normal startup. + println!("{WAYLAND_PROBE_MAGIC}"); + let _ = std::io::stdout().flush(); + let code = match seat0_runtime_dir() + .and_then(|dir| { + drop_to_dir_owner(&dir)?; + probe_runtime_dir(&dir) + }) + .and_then(|displays| serde_json::to_string(&displays).map_err(anyhow::Error::from)) + { + Ok(json) => { + println!("{json}"); + 0 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }; + let _ = std::io::stdout().flush(); + std::process::exit(code) +} + +static ENDPOINT_WAS_NAMED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +/// Whether the environment ever named a wayland endpoint in this process. Empty is not a name. +/// +/// Read before `connect_to_env`, which removes `WAYLAND_SOCKET` from the environment on both its +/// success and its bad-fd path; and latched, so a consumed variable cannot turn a process that WAS +/// pointed at a compositor into one that is free to go looking for another. +pub(super) fn env_names_wayland_endpoint() -> bool { + use std::sync::atomic::Ordering; + let named = ["WAYLAND_DISPLAY", "WAYLAND_SOCKET"] + .iter() + .any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty())); + if named { + ENDPOINT_WAS_NAMED.store(true, Ordering::Release); + } + ENDPOINT_WAS_NAMED.load(Ordering::Acquire) +} + +/// The probe parses compositor-controlled protocol data; a root service must not do that as +/// root. Before touching the socket, become the runtime directory's owner — and refuse to probe +/// at all if the drop fails, since staying root is the one unacceptable outcome. +fn drop_to_dir_owner(dir: &Path) -> ResultType<()> { + if unsafe { libc::geteuid() } != 0 { + return Ok(()); + } + use std::os::unix::fs::MetadataExt; + let meta = std::fs::metadata(dir)?; + let (uid, gid) = (meta.uid(), meta.gid()); + if uid == 0 { + // Root's own session: there is no boundary to cross and nothing to drop to. + return Ok(()); + } + unsafe { + if libc::setgroups(0, std::ptr::null()) != 0 + || libc::setgid(gid) != 0 + || libc::setuid(uid) != 0 + || libc::setuid(0) == 0 + { + bail!("could not drop privileges for the socket probe"); + } + } + Ok(()) +} + +/// `/run/user/` of the active seat0 session, a greeter included. +/// +/// Derived from the uid rather than read from `XDG_RUNTIME_DIR`: the root service is given no such +/// variable, and `get_home_dir_trusted` refuses to trust the environment for the same reason. +fn seat0_runtime_dir() -> ResultType { + let uid = get_values_of_seat0_with_gdm_wayland(&[1]).remove(0); + if uid.is_empty() || !uid.bytes().all(|b| b.is_ascii_digit()) { + bail!("no active seat0 session to take a runtime directory from"); + } + Ok(PathBuf::from(format!("/run/user/{uid}"))) +} + +/// The wayland sockets present in `dir`, lowest display number first. +/// +/// Scanned rather than guessed: `wl_display_add_socket_auto` takes the first FREE name up to +/// `wayland-32`, and a greeter is where leftovers accumulate across compositor restarts. Only that +/// name pattern, because the same directory holds pipewire and dbus sockets. +fn wayland_sockets_in(dir: &Path) -> Vec { + use std::os::unix::fs::FileTypeExt; + let mut paths: Vec = match std::fs::read_dir(dir) { + Ok(entries) => entries + .flatten() + .filter(|entry| { + let name = entry.file_name(); + let name = name.to_string_lossy(); + name.starts_with("wayland-") + && !name.ends_with(".lock") + && entry.file_type().map(|t| t.is_socket()).unwrap_or(false) + }) + .map(|entry| entry.path()) + .collect(), + Err(_) => Vec::new(), + }; + paths.sort_by_key(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .and_then(|name| name.strip_prefix("wayland-")) + .and_then(|number| number.parse::().ok()) + .unwrap_or(u32::MAX) + }); + paths +} + +/// Enumerate through a socket in the seat0 runtime directory, for the case where nothing named an +/// endpoint: a greeter's `--server` and the root service are given no compositor variables, so +/// nothing tells the enumerator where a compositor that IS running lives. An endpoint that WAS +/// named and failed must not silently reattach to a different compositor. +/// +/// In a subprocess and bounded, because the caller holds a process-wide lock across the call while +/// `connect(2)` parks on a full backlog and sctk's roundtrip polls without a deadline; and because +/// sctk panics on malformed output events, which the release profile's panic=abort turns into an +/// abort of the whole server. A child dies alone, and on the deadline it is killed instead of +/// leaking a thread. The seat0 lookup runs inside the child, under the same deadline. +pub(super) fn wayland_displays_from_runtime_dir( + named_endpoint: bool, +) -> ResultType> { + use std::sync::atomic::Ordering; + if named_endpoint { + bail!("an explicit wayland endpoint is set and did not connect"); + } + if PROBE_UNSUPPORTED.load(Ordering::Acquire) { + bail!("this binary does not dispatch {WAYLAND_DISPLAY_PROBE_ARG}"); + } + if RUNTIME_DIR_PROBE_BUSY.swap(true, Ordering::AcqRel) { + bail!("an earlier probe has not returned"); + } + let _busy = ProbeBusyGuard; + let exe = std::env::current_exe()?; + // Its own process group, so the deadline can kill loginctl descendants along with the child, + // and so no surviving descendant can hold the pipes open past the reads below. + use std::os::unix::process::CommandExt; + let mut child = std::process::Command::new(exe) + .arg(WAYLAND_DISPLAY_PROBE_ARG) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .process_group(0) + .spawn()?; + let probe_pgid = child.id() as libc::pid_t; + let kill_probe_group = || unsafe { + let _ = libc::kill(-probe_pgid, libc::SIGKILL); + }; + let deadline = std::time::Instant::now() + RUNTIME_DIR_PROBE_TIMEOUT; + let status = loop { + match child.try_wait()? { + Some(status) => { + kill_probe_group(); + break status; + } + None if std::time::Instant::now() >= deadline => { + kill_probe_group(); + // The direct pid too, not only its group: if the child left the group its own + // kill would miss it, and the wait below would then block on a live child. A + // pid-targeted SIGKILL is uncatchable, so wait() is bounded either way. + let _ = child.kill(); + let _ = child.wait(); + // An unwired binary runs its normal startup, and a long-running one (the + // server itself) lands HERE rather than at the handshake check below — latch + // on this path too, or every enumeration cycle spawns a full consumer + // process. Judged by what the child already wrote: a real probe prints the + // magic line first and flushes, so its absence after a whole deadline means + // this is not a probe. Only buffered bytes are read — a blocking read could + // hang on a grandchild that inherited the write end. + match first_buffered_line(child.stdout.take()) { + // The pipe could not be inspected at all: no evidence, no latch. + None => { + bail!("the wayland socket probe timed out and its output was uninspectable") + } + Some(head) if head.as_deref() == Some(WAYLAND_PROBE_MAGIC) => { + bail!("the wayland socket probe did not answer and was killed"); + } + Some(_) => { + PROBE_UNSUPPORTED.store(true, Ordering::Release); + bail!("the wayland socket probe timed out without the handshake; probe disabled"); + } + } + } + None => std::thread::sleep(std::time::Duration::from_millis(25)), + } + }; + // Drained non-blocking, not read_to_string: the child exited so its output is already + // buffered, but a descendant that escaped the process group could still hold a write end open + // and an EOF-seeking read would then hang here forever. + let stdout = drain_nonblocking(child.stdout.take()).unwrap_or_default(); + let stderr = drain_nonblocking(child.stderr.take()).unwrap_or_default(); + let mut lines = stdout.lines(); + if lines.next() != Some(WAYLAND_PROBE_MAGIC) { + // Not a probe: the binary ran its normal startup. Latch, or this path would spawn one + // full consumer process per enumeration cycle. + PROBE_UNSUPPORTED.store(true, Ordering::Release); + bail!("this binary does not dispatch {WAYLAND_DISPLAY_PROBE_ARG}; probe disabled"); + } + if !status.success() { + let detail = stderr.trim(); + if detail.is_empty() { + // panic=abort or a signal leaves stderr empty; the status is then the only cause. + bail!("wayland socket probe failed: {status}"); + } + bail!("wayland socket probe failed ({status}): {detail}"); + } + let displays: Vec = + match serde_json::from_str(lines.next().unwrap_or_default()) { + Ok(displays) => displays, + Err(err) => bail!("wayland socket probe answered a malformed list: {err}"), + }; + // The child already refuses an empty list; refuse it here too, so a truncated pipe cannot + // become a cached-for-life empty enumeration. + if displays.is_empty() { + bail!("wayland socket probe returned no outputs"); + } + log::debug!( + "wayland: {} output(s) via the probe subprocess", + displays.len() + ); + Ok(displays) +} + +/// Everything already buffered in the pipe, read strictly non-blocking and capped: a descendant +/// that escaped the probe's process group can hold a write end open, so a blocking read (even +/// after the child exits) could hang the enumeration forever. `None` means the pipe could not be +/// INSPECTED (missing handle or fcntl failure) and must not be read as evidence of anything; +/// `Some` is whatever bytes were buffered, whether or not EOF arrived. +fn drain_nonblocking(pipe: Option) -> Option { + let mut pipe = pipe?; + let fd = pipe.as_raw_fd(); + unsafe { + let flags = libc::fcntl(fd, libc::F_GETFL); + if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 { + return None; + } + } + // Capped so a descendant that keeps writing cannot spin this read forever. + const CAP: usize = 64 * 1024; + let mut out = Vec::new(); + let mut buf = [0u8; 4096]; + loop { + match pipe.read(&mut buf) { + Ok(0) => break, // EOF: the write end is fully closed + Ok(n) => { + out.extend_from_slice(&buf[..n]); + if out.len() >= CAP { + break; + } + } + Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue, + // WouldBlock: what is buffered is drained (a descendant may still hold the writer). + // Any other error: stop with what we have. + Err(_) => break, + } + } + Some(String::from_utf8_lossy(&out).into_owned()) +} + +/// The first line the child buffered, for the timeout latch decision. `Some(None)` is an +/// inspected-but-empty buffer (genuine absence of the handshake); outer `None` is uninspectable. +fn first_buffered_line(pipe: Option) -> Option> { + drain_nonblocking(pipe).map(|s| s.lines().next().map(str::to_owned)) +} + +fn probe_runtime_dir(dir: &Path) -> ResultType> { + use std::os::unix::net::UnixStream; + let mut errs = Vec::new(); + for path in wayland_sockets_in(dir) { + match UnixStream::connect(&path) + .map_err(anyhow::Error::from) + .and_then(|s| Connection::from_socket(s).map_err(anyhow::Error::from)) + .and_then(|conn| collect_wayland_displays(&conn)) + { + // The caller caches an empty list as ground truth for the process lifetime, and a + // compositor still probing its monitors is exactly what this path connects to. + Ok(displays) if displays.is_empty() => { + errs.push(format!("{}: no outputs yet", path.display())) + } + Ok(displays) => { + // Which socket answered, when nothing in the environment named one. + log::debug!( + "wayland: {} output(s) from {}, found by scanning", + displays.len(), + path.display() + ); + return Ok(displays); + } + Err(err) => errs.push(format!("{}: {err}", path.display())), + } + } + bail!( + "no usable wayland socket in {} ({})", + dir.display(), + if errs.is_empty() { + "none present".to_owned() + } else { + errs.join("; ") + } + ) +} diff --git a/libs/base/src/platform/macos.rs b/libs/base/src/platform/macos.rs new file mode 100644 index 000000000..2df8b528b --- /dev/null +++ b/libs/base/src/platform/macos.rs @@ -0,0 +1,55 @@ +use hbb_common::ResultType; +use osascript; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct AlertParams { + title: String, + message: String, + alert_type: String, + buttons: Vec, +} + +#[derive(Deserialize)] +struct AlertResult { + #[serde(rename = "buttonReturned")] + button: String, +} + +/// Firstly run the specified app, then alert a dialog. Return the clicked button value. +/// +/// # Arguments +/// +/// * `app` - The app to execute the script. +/// * `alert_type` - Alert type. . informational, warning, critical +/// * `title` - The alert title. +/// * `message` - The alert message. +/// * `buttons` - The buttons to show. +pub fn alert( + app: String, + alert_type: String, + title: String, + message: String, + buttons: Vec, +) -> ResultType { + let script = osascript::JavaScript::new(&format!( + " + var App = Application('{}'); + App.includeStandardAdditions = true; + return App.displayAlert($params.title, {{ + message: $params.message, + 'as': $params.alert_type, + buttons: $params.buttons, + }}); + ", + app + )); + + let result: AlertResult = script.execute_with_params(AlertParams { + title, + message, + alert_type, + buttons, + })?; + Ok(result.button) +} diff --git a/libs/base/src/platform/mod.rs b/libs/base/src/platform/mod.rs new file mode 100644 index 000000000..728f6c8c5 --- /dev/null +++ b/libs/base/src/platform/mod.rs @@ -0,0 +1,82 @@ +#[cfg(target_os = "linux")] +pub mod linux; + +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(target_os = "windows")] +pub mod windows; + +#[cfg(not(debug_assertions))] +use hbb_common::{config::Config, log}; +#[cfg(not(debug_assertions))] +use std::process::exit; + +#[cfg(not(debug_assertions))] +static mut GLOBAL_CALLBACK: Option> = None; + +#[cfg(not(debug_assertions))] +extern "C" fn breakdown_signal_handler(sig: i32) { + let mut stack = vec![]; + backtrace::trace(|frame| { + backtrace::resolve_frame(frame, |symbol| { + if let Some(name) = symbol.name() { + stack.push(name.to_string()); + } + }); + true // keep going to the next frame + }); + let mut info = String::default(); + if stack.iter().any(|s| { + s.contains(&"nouveau_pushbuf_kick") + || s.to_lowercase().contains("nvidia") + || s.contains("gdk_window_end_draw_frame") + || s.contains("glGetString") + }) { + Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); + info = "Always use software rendering will be set.".to_string(); + log::info!("{}", info); + } + if stack.iter().any(|s| { + s.to_lowercase().contains("nvidia") + || s.to_lowercase().contains("amf") + || s.to_lowercase().contains("mfx") + || s.contains("cuProfilerStop") + }) { + Config::set_option("enable-hwcodec".to_string(), "N".to_string()); + info = "Perhaps hwcodec causing the crash, disable it first".to_string(); + log::info!("{}", info); + } + log::error!( + "Got signal {} and exit. stack:\n{}", + sig, + stack.join("\n").to_string() + ); + if !info.is_empty() { + #[cfg(target_os = "linux")] + linux::system_message( + "RustDesk", + &format!("Got signal {} and exit.{}", sig, info), + true, + ) + .ok(); + } + unsafe { + #[allow(static_mut_refs)] + if let Some(callback) = &GLOBAL_CALLBACK { + callback() + } + } + exit(0); +} + +#[cfg(not(debug_assertions))] +pub fn register_breakdown_handler(callback: T) +where + T: Fn() + 'static, +{ + unsafe { + GLOBAL_CALLBACK = Some(Box::new(callback)); + libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); + } +} diff --git a/libs/base/src/platform/windows.rs b/libs/base/src/platform/windows.rs new file mode 100644 index 000000000..7481631ac --- /dev/null +++ b/libs/base/src/platform/windows.rs @@ -0,0 +1,198 @@ +use std::{ + collections::VecDeque, + sync::{Arc, Mutex}, + time::Instant, +}; +use winapi::{ + shared::minwindef::{DWORD, FALSE, TRUE}, + um::{ + handleapi::CloseHandle, + pdh::{ + PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx, + PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE, + PDH_HCOUNTER, PDH_HQUERY, + }, + synchapi::{CreateEventA, WaitForSingleObject}, + sysinfoapi::VerSetConditionMask, + winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0}, + winnt::{ + HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION, + VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR, + }, + }, +}; + +lazy_static::lazy_static! { + static ref CPU_USAGE_ONE_MINUTE: Arc>> = Arc::new(Mutex::new(None)); +} + +// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs +#[repr(transparent)] +pub struct RAIIHandle(pub HANDLE); + +impl Drop for RAIIHandle { + fn drop(&mut self) { + // This never gives problem except when running under a debugger. + unsafe { CloseHandle(self.0) }; + } +} + +#[repr(transparent)] +pub(self) struct RAIIPDHQuery(pub PDH_HQUERY); + +impl Drop for RAIIPDHQuery { + fn drop(&mut self) { + unsafe { PdhCloseQuery(self.0) }; + } +} + +pub fn start_cpu_performance_monitor() { + // Code from: + // https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data + // https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex + // Why value lower than taskManager: + // https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43 + // Therefore we should compare with Precess Explorer rather than taskManager + + let f = || unsafe { + // load avg or cpu usage, test with prime95. + // Prefer cpu usage because we can get accurate value from Precess Explorer. + // const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0"; + const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0"; + const SAMPLE_INTERVAL: DWORD = 2; // 2 second + + let mut ret; + let mut query: PDH_HQUERY = std::mem::zeroed(); + ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query); + if ret != 0 { + log::error!("PdhOpenQueryA failed: 0x{:X}", ret); + return; + } + let _query = RAIIPDHQuery(query); + let mut counter: PDH_HCOUNTER = std::mem::zeroed(); + ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter); + if ret != 0 { + log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret); + return; + } + ret = PdhCollectQueryData(query); + if ret != 0 { + log::error!("PdhCollectQueryData failed: 0x{:X}", ret); + return; + } + let mut _counter_type: DWORD = 0; + let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed(); + let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _); + if event.is_null() { + log::error!("CreateEventA failed"); + return; + } + let _event: RAIIHandle = RAIIHandle(event); + ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event); + if ret != 0 { + log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret); + return; + } + + let mut queue: VecDeque = VecDeque::new(); + let mut recent_valid: VecDeque = VecDeque::new(); + loop { + // latest one minute + if queue.len() == 31 { + queue.pop_front(); + } + if recent_valid.len() == 31 { + recent_valid.pop_front(); + } + // allow get value within one minute + if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 { + let sum: f64 = queue.iter().map(|f| f.to_owned()).sum(); + let avg = sum / (queue.len() as f64); + *CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now())); + } else { + *CPU_USAGE_ONE_MINUTE.lock().unwrap() = None; + } + if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) { + recent_valid.push_back(false); + continue; + } + if PdhGetFormattedCounterValue( + counter, + PDH_FMT_DOUBLE, + &mut _counter_type, + &mut counter_value, + ) != 0 + || counter_value.CStatus != 0 + { + recent_valid.push_back(false); + continue; + } + queue.push_back(counter_value.u.doubleValue().clone()); + recent_valid.push_back(true); + } + }; + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + std::thread::spawn(f); + }); +} + +pub fn cpu_uage_one_minute() -> Option { + let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone(); + if let Some((v, instant)) = v { + if instant.elapsed().as_secs() < 30 { + return Some(v); + } + } + None +} + +pub fn sync_cpu_usage(cpu_usage: Option) { + let v = match cpu_usage { + Some(cpu_usage) => Some((cpu_usage, Instant::now())), + None => None, + }; + *CPU_USAGE_ONE_MINUTE.lock().unwrap() = v; + log::info!("cpu usage synced: {:?}", cpu_usage); +} + +// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 +// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780 +pub fn is_windows_version_or_greater( + os_major: u32, + os_minor: u32, + build_number: u32, + service_pack_major: u32, + service_pack_minor: u32, +) -> bool { + let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() }; + osvi.dwOSVersionInfoSize = std::mem::size_of::() as DWORD; + osvi.dwMajorVersion = os_major as _; + osvi.dwMinorVersion = os_minor as _; + osvi.dwBuildNumber = build_number as _; + osvi.wServicePackMajor = service_pack_major as _; + osvi.wServicePackMinor = service_pack_minor as _; + + let result = unsafe { + let mut condition_mask = 0; + let op = VER_GREATER_EQUAL; + condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op); + condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op); + condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op); + condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op); + condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op); + + VerifyVersionInfoW( + &mut osvi as *mut OSVERSIONINFOEXW, + VER_MAJORVERSION + | VER_MINORVERSION + | VER_BUILDNUMBER + | VER_SERVICEPACKMAJOR + | VER_SERVICEPACKMINOR, + condition_mask, + ) + }; + + result == TRUE +} diff --git a/libs/base/src/protos/mod.rs b/libs/base/src/protos/mod.rs new file mode 100644 index 000000000..57d9b68fe --- /dev/null +++ b/libs/base/src/protos/mod.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index 7e15791e9..5ca2a4d53 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -30,6 +30,7 @@ lazy_static = "1.4" serde = "1.0" serde_derive = "1.0" hbb_common = { path = "../hbb_common" } +base = { path = "../base" } parking_lot = {version = "0.12"} [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] diff --git a/libs/clipboard/src/platform/unix/filetype.rs b/libs/clipboard/src/platform/unix/filetype.rs index ca5cc0a5e..52d637bbe 100644 --- a/libs/clipboard/src/platform/unix/filetype.rs +++ b/libs/clipboard/src/platform/unix/filetype.rs @@ -60,10 +60,8 @@ pub(super) fn validate_file_name(name: &str) -> Result<(), CliprdrError> { description: "clipboard file name is not a normalized relative path".to_string(), }); } - hbb_common::fs::validate_file_name_no_traversal(name).map_err(|error| { - CliprdrError::InvalidRequest { - description: error.to_string(), - } + base::fs::validate_file_name_no_traversal(name).map_err(|error| CliprdrError::InvalidRequest { + description: error.to_string(), }) } diff --git a/libs/clipboard/src/platform/unix/macos/paste_task.rs b/libs/clipboard/src/platform/unix/macos/paste_task.rs index 5885ea655..76e054643 100644 --- a/libs/clipboard/src/platform/unix/macos/paste_task.rs +++ b/libs/clipboard/src/platform/unix/macos/paste_task.rs @@ -2,7 +2,8 @@ use crate::{ platform::unix::{FileDescription, FileType, BLOCK_SIZE}, send_data, ClipboardFile, CliprdrError, ProgressPercent, }; -use hbb_common::{allow_err, fs::join_validated_path, log, tokio::time::Instant}; +use base::fs::join_validated_path; +use hbb_common::{allow_err, log, tokio::time::Instant}; use std::{ cmp::min, fs::{File, FileTimes, OpenOptions}, diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 6468eeedd..c47409d1e 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -25,6 +25,7 @@ log = "0.4" rdev = { git = "https://github.com/rustdesk-org/rdev" } tfc = { git = "https://github.com/rustdesk-org/The-Fat-Controller", branch = "history/rebase_upstream_20240722" } hbb_common = { path = "../hbb_common" } +base = { path = "../base" } [features] with_serde = ["serde", "serde_derive"] diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 4e379407f..ffb07ea9a 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -122,7 +122,7 @@ impl Enigo { impl Default for Enigo { fn default() -> Self { - let is_x11 = hbb_common::platform::linux::is_x11_or_headless(); + let is_x11 = base::platform::linux::is_x11_or_headless(); Self { is_x11, tfc: if is_x11 { diff --git a/libs/hbb_common b/libs/hbb_common index 3d6fb2c39..55395c6fc 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 3d6fb2c397f2a9a717440f7e29afed5ab5f5dc03 +Subproject commit 55395c6fcbcb8dd4bc8d4e7ab4d7d7c8d1789b43 diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index bab2b4e9f..1cc1ff619 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -20,7 +20,7 @@ wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", " # Depends on `wayland`: the three drm modules live inside the `#[cfg(feature = "wayland")]` arm of # common/mod.rs, so `scrap/drm` on its own would compile nothing. The root crate happens to always # enable `scrap/wayland`, which is what hid this. -drm = ["wayland", "hbb_common/wayland_probe"] +drm = ["wayland", "base/wayland_probe"] mediacodec = ["ndk"] linux-pkg-config = ["dep:pkg-config"] hwcodec = ["dep:hwcodec"] @@ -31,6 +31,7 @@ cfg-if = "1.0" num_cpus = "1.15" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } +base = { path = "../base" } webm = { git = "https://github.com/rustdesk-org/rust-webm" } serde = {version="1.0", features=["derive"]} diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index 2c891a932..26f6a50f4 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -9,7 +9,8 @@ use jni::{ JavaVM, }; -use hbb_common::{message_proto::MultiClipboards, protobuf::Message}; +use base::message_proto::MultiClipboards; +use hbb_common::protobuf::Message; use jni::errors::{Error as JniError, Result as JniResult}; use lazy_static::lazy_static; use serde::Deserialize; diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs index 1ecd6059b..c33a0a387 100644 --- a/libs/scrap/src/common/aom.rs +++ b/libs/scrap/src/common/aom.rs @@ -13,10 +13,9 @@ use crate::{EncodeInput, EncodeYuvFormat, Pixfmt}; use hbb_common::{ anyhow::{anyhow, Context}, bytes::Bytes, - log, - message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, - ResultType, + log, ResultType, }; +use base::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use std::{ptr, slice}; generate_call_macro!(call_aom, false); diff --git a/libs/scrap/src/common/camera.rs b/libs/scrap/src/common/camera.rs index ea259bdc1..e31a37a27 100644 --- a/libs/scrap/src/common/camera.rs +++ b/libs/scrap/src/common/camera.rs @@ -11,7 +11,7 @@ use nokhwa::{ Camera, }; -use hbb_common::message_proto::{DisplayInfo, Resolution}; +use base::message_proto::{DisplayInfo, Resolution}; #[cfg(feature = "vram")] use crate::AdapterDevice; diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9b072e1bd..606a4be4b 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -18,6 +18,10 @@ use crate::{ CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture, }; +use base::message_proto::{ + supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames, + SupportedDecoding, SupportedEncoding, VideoFrame, +}; #[cfg(any( feature = "hwcodec", feature = "mediacodec", @@ -30,10 +34,6 @@ use hbb_common::{ bail, config::{Config, PeerConfig}, lazy_static, log, - message_proto::{ - supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames, - SupportedDecoding, SupportedEncoding, VideoFrame, - }, sysinfo::System, ResultType, }; @@ -269,7 +269,7 @@ impl Encoder { let preference = most_frequent.enum_value_or(PreferCodec::Auto); // auto: h265 > h264 > av1/vp9/vp8 - let av1_test = Config::get_option(hbb_common::config::keys::OPTION_AV1_TEST) != "N"; + let av1_test = Config::get_option(base::config::keys::OPTION_AV1_TEST) != "N"; let mut auto_codec = if av1_useable && av1_test { CodecFormat::AV1 } else { @@ -849,7 +849,7 @@ impl Decoder { #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] pub fn enable_hwcodec_option() -> bool { - use hbb_common::config::keys::OPTION_ENABLE_HWCODEC; + use base::config::keys::OPTION_ENABLE_HWCODEC; if !cfg!(target_os = "ios") { return option2bool( @@ -861,7 +861,7 @@ pub fn enable_hwcodec_option() -> bool { } #[cfg(feature = "vram")] pub fn enable_vram_option(encode: bool) -> bool { - use hbb_common::config::keys::OPTION_ENABLE_HWCODEC; + use base::config::keys::OPTION_ENABLE_HWCODEC; if cfg!(windows) { let enable = option2bool( @@ -880,13 +880,13 @@ pub fn enable_vram_option(encode: bool) -> bool { #[cfg(windows)] pub fn enable_directx_capture() -> bool { - use hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE as OPTION; + use base::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE as OPTION; option2bool(OPTION, &Config::get_option(OPTION)) } #[cfg(windows)] pub fn allow_d3d_render() -> bool { - use hbb_common::config::keys::OPTION_ALLOW_D3D_RENDER as OPTION; + use base::config::keys::OPTION_ALLOW_D3D_RENDER as OPTION; option2bool(OPTION, &hbb_common::config::LocalConfig::get_option(OPTION)) } @@ -980,7 +980,7 @@ pub fn codec_thread_num(limit: usize) -> usize { #[cfg(windows)] { res = 0; - let percent = hbb_common::platform::windows::cpu_uage_one_minute(); + let percent = base::platform::windows::cpu_uage_one_minute(); info = format!("cpu usage: {:?}", percent); if let Some(pecent) = percent { if pecent < 100.0 { @@ -1038,7 +1038,7 @@ fn disable_av1() -> bool { #[cfg(not(target_os = "ios"))] pub fn test_av1() { - use hbb_common::config::keys::OPTION_AV1_TEST; + use base::config::keys::OPTION_AV1_TEST; use hbb_common::rand::Rng; use std::{sync::Once, time::Duration}; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 17eda7f3c..9cf3367ba 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -3,11 +3,11 @@ use crate::{ convert::*, CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN, }; +use base::message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use hbb_common::{ anyhow::{anyhow, bail, Context}, bytes::Bytes, log, - message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, serde_derive::{Deserialize, Serialize}, serde_json, ResultType, }; diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1efed1176..8c14db13b 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -1,9 +1,6 @@ pub use self::vpxcodec::*; -use hbb_common::{ - bail, log, - message_proto::{video_frame, Chroma, VideoFrame}, - ResultType, -}; +use base::message_proto::{video_frame, Chroma, VideoFrame}; +use hbb_common::{bail, log, ResultType}; use std::{ffi::c_void, slice}; cfg_if! { @@ -268,7 +265,7 @@ pub struct EncodeYuvFormat { #[cfg(x11)] #[inline] pub fn is_x11() -> bool { - hbb_common::platform::linux::is_x11_or_headless() + base::platform::linux::is_x11_or_headless() } #[cfg(x11)] diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index ffeb25791..08a086ecd 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -1,11 +1,8 @@ use crate::CodecFormat; +use base::message_proto::{message, video_frame, EncodedVideoFrame, Message}; #[cfg(feature = "hwcodec")] use hbb_common::anyhow::anyhow; -use hbb_common::{ - bail, chrono, log, - message_proto::{message, video_frame, EncodedVideoFrame, Message}, - ResultType, -}; +use hbb_common::{bail, chrono, log, ResultType}; #[cfg(feature = "hwcodec")] use hwcodec::mux::{MuxContext, Muxer}; use std::{ diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index f41dfb134..0478f7fa3 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -5,8 +5,8 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::log; -use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use hbb_common::ResultType; +use base::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use crate::codec::{base_bitrate, codec_thread_num, EncoderApi}; use crate::{EncodeInput, EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN}; diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index 22645d92b..3c140abb3 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -9,12 +9,11 @@ use crate::{ hwcodec::HwCodecConfig, AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt, }; +use base::message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use hbb_common::{ anyhow::{anyhow, bail, Context}, bytes::Bytes, - log, - message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, - ResultType, + log, ResultType, }; use hwcodec::{ common::{DataFormat, Driver, MAX_GOP}, @@ -98,7 +97,7 @@ impl EncoderApi for VRamEncoder { &mut self, frame: EncodeInput, ms: i64, - ) -> ResultType { + ) -> ResultType { let (texture, rotation) = frame.texture()?; if rotation != 0 { // to-do: support rotation diff --git a/libs/scrap/src/wayland/display.rs b/libs/scrap/src/wayland/display.rs index fdd296b32..6ff4177cc 100644 --- a/libs/scrap/src/wayland/display.rs +++ b/libs/scrap/src/wayland/display.rs @@ -8,7 +8,7 @@ use std::{ }; use tracing::warn; -use hbb_common::platform::linux::{get_wayland_displays, WaylandDisplayInfo}; +use base::platform::linux::{get_wayland_displays, WaylandDisplayInfo}; lazy_static! { static ref DISPLAYS: Mutex>> = Mutex::new(None); @@ -105,7 +105,7 @@ fn try_xrandr_primary() -> Option { } fn try_kscreen_primary() -> Option { - if !hbb_common::platform::linux::is_kde_session() { + if !base::platform::linux::is_kde_session() { return None; } diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index f0852e564..34e89788d 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -23,7 +23,8 @@ use gstreamer_app::AppSink; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use hbb_common::{bail, config, platform::linux::CMD_SH, serde_json, tokio, ResultType}; +use base::platform::linux::CMD_SH; +use hbb_common::{bail, config, serde_json, tokio, ResultType}; use super::capturable::PixelProvider; use super::capturable::{Capturable, Recorder}; diff --git a/src/client.rs b/src/client.rs index ce5ab152e..10330cfd6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -47,13 +47,11 @@ use hbb_common::{ anyhow::{anyhow, Context}, bail, config::{ - self, keys, use_ws, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, + self, use_ws, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, }, - fs::JobType, futures::future::{select_ok, BoxFuture, FutureExt}, get_version_number, log, - message_proto::{option_message::BoolOption, *}, protobuf::{Message as _, MessageField}, rand, rendezvous_proto::*, @@ -73,6 +71,11 @@ use hbb_common::{ webrtc::WebRTCStream, AddrMangle, ResultType, Stream, }; +use base::{ + config::keys, + fs::JobType, + message_proto::{option_message::BoolOption, *}, +}; pub use helper::*; use scrap::{ codec::Decoder, @@ -4020,7 +4023,7 @@ async fn do_sync_cpu_usage() { if let Ok(Some(data)) = conn.next_timeout(50).await { match data { Data::SyncWinCpuUsage(cpu_usage) => { - hbb_common::platform::windows::sync_cpu_usage(cpu_usage); + base::platform::windows::sync_cpu_usage(cpu_usage); } _ => {} } @@ -4680,7 +4683,7 @@ pub trait Interface: Send + Clone + 'static + Sized { } } - fn swap_modifier_mouse(&self, _msg: &mut hbb_common::protos::message::MouseEvent) {} + fn swap_modifier_mouse(&self, _msg: &mut base::protos::message::MouseEvent) {} fn update_direct(&self, direct: Option) { self.get_lch().write().unwrap().direct = direct; diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index bd23883e5..5c2372d52 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,4 +1,5 @@ -use hbb_common::{fs, log, message_proto::*}; +use hbb_common::log; +use base::{fs, message_proto::*}; use super::{Data, Interface}; diff --git a/src/client/helper.rs b/src/client/helper.rs index 98d1239e1..c4649a9ec 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -1,7 +1,5 @@ -use hbb_common::{ - get_time, - message_proto::{Message, VoiceCallRequest, VoiceCallResponse}, -}; +use base::message_proto::{Message, VoiceCallRequest, VoiceCallResponse}; +use hbb_common::get_time; use scrap::CodecFormat; use std::collections::HashMap; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 34c462f4d..a49a2d841 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -17,6 +17,14 @@ const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5); const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30); #[cfg(feature = "unix-file-copy-paste")] use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; +use base::{ + config::keys, + fs::{ + self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, + DigestCheckResult, RemoveJobMeta, + }, + message_proto::{permission_info::Permission, *}, +}; #[cfg(any( target_os = "windows", all(target_os = "macos", feature = "unix-file-copy-paste") @@ -28,12 +36,7 @@ use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ allow_err, config::{self, LocalConfig, PeerConfig, TransferSerde}, - fs::{ - self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, - DigestCheckResult, RemoveJobMeta, - }, get_time, log, - message_proto::{permission_info::Permission, *}, protobuf::Message as _, rendezvous_proto::ConnType, timeout, @@ -2036,9 +2039,8 @@ impl Remote { #[cfg(target_os = "windows")] Ok(file_transfer_send_request::FileType::Printer) => { #[cfg(feature = "flutter")] - let action = LocalConfig::get_option( - config::keys::OPTION_PRINTER_INCOMING_JOB_ACTION, - ); + let action = + LocalConfig::get_option(keys::OPTION_PRINTER_INCOMING_JOB_ACTION); #[cfg(not(feature = "flutter"))] let action = ""; if action == "dismiss" { @@ -2047,7 +2049,7 @@ impl Remote { let id = fs::get_next_job_id(); #[cfg(feature = "flutter")] let allow_auto_print = LocalConfig::get_bool_option( - config::keys::OPTION_PRINTER_ALLOW_AUTO_PRINT, + keys::OPTION_PRINTER_ALLOW_AUTO_PRINT, ); #[cfg(not(feature = "flutter"))] let allow_auto_print = false; @@ -2055,9 +2057,7 @@ impl Remote { let printer_name = if action == "" { "".to_string() } else { - LocalConfig::get_option( - config::keys::OPTION_PRINTER_SELECTED_NAME, - ) + LocalConfig::get_option(keys::OPTION_PRINTER_SELECTED_NAME) }; self.handler.printer_response(id, _s.path, printer_name); } else { @@ -2123,7 +2123,7 @@ impl Remote { .handle_screenshot_resp(response.sid, response.msg); } Some(message::Union::TerminalResponse(response)) => { - use hbb_common::message_proto::terminal_response::Union; + use base::message_proto::terminal_response::Union; if let Some(Union::Opened(opened)) = &response.union { if opened.success && !opened.service_id.is_empty() { let mut lc = self.handler.lc.write().unwrap(); @@ -2347,14 +2347,10 @@ impl Remote { } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] - async fn handle_cliprdr_msg( - &mut self, - clip: hbb_common::message_proto::Cliprdr, - _peer: &mut Stream, - ) { + async fn handle_cliprdr_msg(&mut self, clip: base::message_proto::Cliprdr, _peer: &mut Stream) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] - if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { + if let Some(base::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id != clipboard::get_client_conn_id(&crate::flutter::get_cur_peer_id()).unwrap_or(0) { @@ -2464,8 +2460,7 @@ impl Remote { ); self.video_threads.insert(display, video_thread); if self.video_threads.len() == 1 { - let auto_record = - LocalConfig::get_bool_option(config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING); + let auto_record = LocalConfig::get_bool_option(keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING); self.handler.lc.write().unwrap().record_state = auto_record; self.update_record_state(); } diff --git a/src/client/screenshot.rs b/src/client/screenshot.rs index 82a95bee9..e2e8835a1 100644 --- a/src/client/screenshot.rs +++ b/src/client/screenshot.rs @@ -1,6 +1,7 @@ #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; -use hbb_common::{message_proto::*, ResultType}; +use base::message_proto::*; +use hbb_common::ResultType; use std::sync::Mutex; lazy_static::lazy_static! { diff --git a/src/clipboard.rs b/src/clipboard.rs index 89b96f876..d6d4c4994 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -2,7 +2,8 @@ use arboard::{ClipboardData, ClipboardFormat}; #[cfg(target_os = "linux")] use arboard::{LinuxClipboardKind, SetExtLinux}; -use hbb_common::{bail, log, message_proto::*, ResultType}; +use hbb_common::{bail, log, ResultType}; +use base::message_proto::*; use std::{ sync::{Arc, Mutex}, time::Duration, @@ -515,10 +516,10 @@ impl ClipboardContext { // The host-side clear file clipboard `let _ = self.inner.clear();`, // does not work on KDE Plasma for the installed version. - // Don't use `hbb_common::platform::linux::is_kde()` here. + // Don't use `base::platform::linux::is_kde()` here. // It's not correct in the server process. #[cfg(target_os = "linux")] - let is_kde_x11 = hbb_common::platform::linux::is_kde_session() + let is_kde_x11 = base::platform::linux::is_kde_session() && crate::platform::linux::is_x11(); #[cfg(target_os = "macos")] let is_kde_x11 = false; @@ -581,7 +582,7 @@ pub fn get_current_clipboard_msg( multi_clipboards .clipboards .iter() - .find(|c| c.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text)) + .find(|c| c.format.enum_value() == Ok(base::message_proto::ClipboardFormat::Text)) .map(|c| { let mut msg = Message::new(); msg.set_clipboard(c.clone()); @@ -629,8 +630,8 @@ mod proto { use arboard::ClipboardData; use hbb_common::{ compress::{compress as compress_func, decompress}, - message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}, }; + use base::message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}; fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard { let compressed = compress_func(s.as_bytes()); diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index 4fa64e26f..f4458b098 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -1,5 +1,5 @@ use clipboard::ClipboardFile; -use hbb_common::message_proto::*; +use base::message_proto::*; pub fn clip_2_msg(clip: ClipboardFile) -> Message { match clip { diff --git a/src/common.rs b/src/common.rs index ef9288f5f..00a409a63 100644 --- a/src/common.rs +++ b/src/common.rs @@ -8,6 +8,7 @@ use std::{ use serde_json::{json, Map, Value}; +use base::{config::keys, message_proto::*}; #[cfg(not(target_os = "ios"))] use hbb_common::whoami; use hbb_common::{ @@ -16,13 +17,10 @@ use hbb_common::{ async_recursion::async_recursion, bail, base64, bytes::Bytes, - config::{ - self, keys, use_ws, Config, LocalConfig, CONNECT_TIMEOUT, READ_TIMEOUT, RENDEZVOUS_PORT, - }, + config::{self, use_ws, Config, LocalConfig, CONNECT_TIMEOUT, READ_TIMEOUT, RENDEZVOUS_PORT}, futures::future::join_all, futures_util::future::poll_fn, get_version_number, log, - message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, socket_client, diff --git a/src/core_main.rs b/src/core_main.rs index 9b3d76f0a..b5a451ed8 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -3,9 +3,10 @@ use crate::client::translate; #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::platform::breakdown_callback; +use base::config::keys; #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::platform::register_breakdown_handler; +use base::platform::register_breakdown_handler; use hbb_common::{config, log}; #[cfg(windows)] use tauri_winrt_notification::{Duration, Sound, Toast}; @@ -113,7 +114,7 @@ pub fn core_main() -> Option> { } #[cfg(windows)] if args.contains(&"--connect".to_string()) || args.contains(&"--view-camera".to_string()) { - hbb_common::platform::windows::start_cpu_performance_monitor(); + base::platform::windows::start_cpu_performance_monitor(); } #[cfg(feature = "flutter")] if _is_flutter_invoke_new_connection { @@ -889,7 +890,7 @@ fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool { #[inline] fn is_cli_setting_change_disabled() -> bool { - let option = config::keys::OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED; + let option = keys::OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED; let allow_command_line_settings = config::option2bool(option, &crate::get_builtin_option(option)); config::is_disable_settings() && !allow_command_line_settings diff --git a/src/flutter.rs b/src/flutter.rs index f4971f18c..a6d3496dd 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -10,9 +10,10 @@ use hbb_common::dlopen::{ Error as LibError, }; use hbb_common::{ - anyhow::anyhow, bail, config::LocalConfig, get_version_number, log, message_proto::*, + anyhow::anyhow, bail, config::LocalConfig, get_version_number, log, rendezvous_proto::ConnType, ResultType, }; +use base::message_proto::*; use serde::Serialize; use serde_json::json; #[cfg(target_os = "windows")] @@ -1102,7 +1103,7 @@ impl InvokeUiSession for FlutterHandler { } fn handle_terminal_response(&self, response: TerminalResponse) { - use hbb_common::message_proto::terminal_response::Union; + use base::message_proto::terminal_response::Union; match response.union { Some(Union::Opened(opened)) => { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1528376ab..2e869d141 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -14,10 +14,14 @@ use crate::{ use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, - fs, lazy_static, log, + lazy_static, log, rendezvous_proto::ConnType, ResultType, }; +use base::{ + config::keys, + fs, +}; use std::{ collections::HashMap, path::PathBuf, @@ -330,7 +334,7 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { } #[cfg(feature = "unix-file-copy-paste")] if sessions::get_session_by_session_id(&session_id).is_some() - && (value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE || value == "view-only") + && (value == keys::OPTION_ENABLE_FILE_COPY_PASTE || value == "view-only") { crate::flutter::update_file_clipboard_required(); } @@ -965,12 +969,12 @@ pub fn main_get_error() -> String { pub fn main_set_option(key: String, value: String) { #[cfg(target_os = "android")] { - let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) - || key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER) - || key.eq(config::keys::OPTION_ENABLE_AUDIO); + let is_permission_option = key.eq(keys::OPTION_ENABLE_CLIPBOARD) + || key.eq(keys::OPTION_ENABLE_FILE_TRANSFER) + || key.eq(keys::OPTION_ENABLE_AUDIO); let allow_perm_change_in_accept_window = config::option2bool( - config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), ); if is_permission_option && !allow_perm_change_in_accept_window @@ -985,14 +989,14 @@ pub fn main_set_option(key: String, value: String) { } } #[cfg(target_os = "android")] - if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { + if key.eq(keys::OPTION_ENABLE_KEYBOARD) { crate::ui_cm_interface::switch_permission_all( "keyboard".to_owned(), config::option2bool(&key, &value), ); } #[cfg(target_os = "android")] - if key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) { + if key.eq(keys::OPTION_ENABLE_CLIPBOARD) { crate::ui_cm_interface::switch_permission_all( "clipboard".to_owned(), config::option2bool(&key, &value), @@ -1002,11 +1006,11 @@ pub fn main_set_option(key: String, value: String) { // If `is_allow_tls_fallback` and https proxy is used, we need to restart rendezvous mediator. // No need to check if https proxy is used, because this option does not change frequently // and restarting mediator is safe even https proxy is not used. - let is_allow_tls_fallback = key.eq(config::keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK); + let is_allow_tls_fallback = key.eq(keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK); if is_allow_tls_fallback || key.eq("custom-rendezvous-server") - || key.eq(config::keys::OPTION_ALLOW_WEBSOCKET) - || key.eq(config::keys::OPTION_DISABLE_UDP) + || key.eq(keys::OPTION_ALLOW_WEBSOCKET) + || key.eq(keys::OPTION_DISABLE_UDP) || key.eq("api-server") { if is_allow_tls_fallback { @@ -1035,14 +1039,14 @@ pub fn main_set_options(json: String) { #[cfg(target_os = "android")] { let allow_perm_change_in_accept_window = config::option2bool( - config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), ); if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() { for key in [ - config::keys::OPTION_ENABLE_CLIPBOARD, - config::keys::OPTION_ENABLE_FILE_TRANSFER, - config::keys::OPTION_ENABLE_AUDIO, + keys::OPTION_ENABLE_CLIPBOARD, + keys::OPTION_ENABLE_FILE_TRANSFER, + keys::OPTION_ENABLE_AUDIO, ] { if let Some(value) = map.remove(key) { log::info!( @@ -1210,8 +1214,8 @@ pub fn main_set_env(key: String, value: Option) -> SyncReturn<()> { } pub fn main_set_local_option(key: String, value: String) { - let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); - let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); + let is_texture_render_key = key.eq(keys::OPTION_TEXTURE_RENDER); + let is_d3d_render_key = key.eq(keys::OPTION_ALLOW_D3D_RENDER); set_local_option(key, value.clone()); let is_render_target = |session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera(); @@ -2651,7 +2655,7 @@ pub fn main_get_common(key: String) -> String { #[cfg(not(target_os = "windows"))] return false.to_string(); } else if key == "transfer-job-id" { - return hbb_common::fs::get_next_job_id().to_string(); + return base::fs::get_next_job_id().to_string(); } else if key == "is-remote-modify-enabled-by-control-permissions" { return match is_remote_modify_enabled_by_control_permissions() { Some(true) => "true", diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index d78f90194..29e34b745 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -7,10 +7,11 @@ use std::{ #[cfg(not(any(target_os = "ios")))] use crate::{ui_interface::get_builtin_option, Connection}; use hbb_common::{ - config::{self, keys, Config, LocalConfig}, + config::{self, Config, LocalConfig}, log, tokio::{self, sync::broadcast, time::Instant}, }; +use base::config::keys; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; diff --git a/src/ipc.rs b/src/ipc.rs index 804b89db6..f4dc4f3f2 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -34,7 +34,7 @@ use hbb_common::anyhow; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, - config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, + config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, log, password_security as password, timeout, @@ -45,6 +45,7 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; +use base::config::keys::{self, OPTION_ALLOW_WEBSOCKET}; #[cfg(windows)] pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection; #[cfg(windows)] @@ -752,9 +753,9 @@ impl CheckIfRestart { audio_input: Config::get_option("audio-input"), voice_call_input: Config::get_option("voice-call-input"), ws: Config::get_option(OPTION_ALLOW_WEBSOCKET), - disable_udp: Config::get_option(config::keys::OPTION_DISABLE_UDP), + disable_udp: Config::get_option(keys::OPTION_DISABLE_UDP), allow_insecure_tls_fallback: Config::get_option( - config::keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK, + keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK, ), api_server: Config::get_option("api-server"), } @@ -766,12 +767,12 @@ impl Drop for CheckIfRestart { // No need to check if https proxy is used, because this option does not change frequently // and restarting mediator is safe even https proxy is not used. let allow_insecure_tls_fallback_changed = self.allow_insecure_tls_fallback - != Config::get_option(config::keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK); + != Config::get_option(keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK); if allow_insecure_tls_fallback_changed || self.stop_service != Config::get_option("stop-service") || self.rendezvous_servers != Config::get_rendezvous_servers() || self.ws != Config::get_option(OPTION_ALLOW_WEBSOCKET) - || self.disable_udp != Config::get_option(config::keys::OPTION_DISABLE_UDP) + || self.disable_udp != Config::get_option(keys::OPTION_DISABLE_UDP) || self.api_server != Config::get_option("api-server") { if allow_insecure_tls_fallback_changed { @@ -1035,7 +1036,7 @@ async fn handle(data: Data, stream: &mut Connection) { allow_err!( stream .send(&Data::SyncWinCpuUsage( - hbb_common::platform::windows::cpu_uage_one_minute() + base::platform::windows::cpu_uage_one_minute() )) .await ); @@ -1227,7 +1228,7 @@ async fn handle(data: Data, stream: &mut Connection) { let state = crate::server::get_control_permission_state(Permission::file, false); let enabled = state.unwrap_or_else(|| { crate::server::Connection::is_permission_enabled_locally( - config::keys::OPTION_ENABLE_FILE_TRANSFER, + keys::OPTION_ENABLE_FILE_TRANSFER, ) }); allow_err!( diff --git a/src/keyboard.rs b/src/keyboard.rs index 3b6e57bea..e7bdce9b5 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -9,7 +9,7 @@ use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::get_key_state, common::GrabState}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::log; -use hbb_common::message_proto::*; +use base::message_proto::*; #[cfg(any(target_os = "windows", target_os = "macos"))] use rdev::KeyCode; use rdev::{Event, EventType, Key}; diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs index 37b541cbe..ccd7d8ace 100644 --- a/src/platform/gtk_sudo.rs +++ b/src/platform/gtk_sudo.rs @@ -2,12 +2,11 @@ // Sometimes reboot is needed to refresh sudoers. use crate::lang::translate; +use base::platform::linux::CMD_SH; use gtk::{glib, prelude::*}; use hbb_common::{ anyhow::{bail, Error}, - log, - platform::linux::CMD_SH, - ResultType, + log, ResultType, }; use nix::{ libc::{fcntl, kill}, diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 099d00a2d..7b52a3571 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,6 +1,6 @@ use super::{gtk_sudo, CursorData, ResultType}; use desktop::Desktop; -pub use hbb_common::platform::linux::*; +pub use base::platform::linux::*; #[cfg(feature = "drm")] pub fn dispatch_wayland_display_probe() { @@ -17,10 +17,10 @@ use hbb_common::{ config::Config, libc::{c_char, c_int, c_long, c_uint, c_ulong, c_void}, log, - message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, users::{get_user_by_name, os::unix::UserExt}, }; +use base::message_proto::{DisplayInfo, Resolution}; use libxdo_sys::{self, xdo_t, Window}; use std::{ cell::RefCell, @@ -64,7 +64,7 @@ lazy_static::lazy_static! { /// serve but the DRM path can. Unmemoised lookup on purpose: this may run mid-boot, and /// a "no" cached that early would be wrong for the rest of the process. pub static ref IS_X11: bool = { - let x11 = hbb_common::platform::linux::is_x11_or_headless(); + let x11 = base::platform::linux::is_x11_or_headless(); #[cfg(feature = "drm")] { if x11 && !display_server_forced() && is_login_screen_wayland() { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 4f85a4b0f..82bcf9d6f 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -20,9 +20,9 @@ use core_graphics::{ use hbb_common::{ anyhow::anyhow, bail, log, - message_proto::{DisplayInfo, Resolution}, sysinfo::{Pid, Process, ProcessRefreshKind, System}, }; +use base::message_proto::{DisplayInfo, Resolution}; use include_dir::{include_dir, Dir}; use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index d55005b4d..7a7ddc035 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -23,13 +23,15 @@ pub mod linux; #[cfg(target_os = "linux")] pub mod gtk_sudo; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use base::message_proto::CursorData; #[cfg(all( not(all(target_os = "windows", not(target_pointer_width = "64"))), not(any(target_os = "android", target_os = "ios")) ))] use hbb_common::sysinfo::System; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::{message_proto::CursorData, sysinfo::Pid, ResultType}; +use hbb_common::{sysinfo::Pid, ResultType}; use std::sync::{Arc, Mutex}; #[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))] pub const SERVICE_INTERVAL: u64 = 300; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index d5e306404..a3c3d68f0 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -5,15 +5,14 @@ use crate::{ ipc, privacy_mode::win_topmost_window::{self, WIN_TOPMOST_INJECTED_PROCESS_EXE}, }; +use base::message_proto::{DisplayInfo, Resolution, WindowsSession}; use hbb_common::{ allow_err, anyhow::anyhow, bail, config::{self, Config}, libc::{c_int, wchar_t}, - log, - message_proto::{DisplayInfo, Resolution, WindowsSession}, - sleep, + log, sleep, sysinfo::{Pid, System}, timeout, tokio, }; @@ -2482,7 +2481,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst } pub fn is_elevated(process_id: Option) -> ResultType { - use hbb_common::platform::windows::RAIIHandle; + use base::platform::windows::RAIIHandle; unsafe { let handle: HANDLE = match process_id { Some(process_id) => OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id), @@ -4754,7 +4753,7 @@ mod tests { // Test-only reusable Win32 HANDLE RAII helper. // If a future non-test path needs the same pattern, move it out of this test module. // - // This struct is similar to `hbb_common::platform::windows::RAIIHandle`, + // This struct is similar to `base::platform::windows::RAIIHandle`, // but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate. struct HandleGuard(WinHANDLE); diff --git a/src/port_forward.rs b/src/port_forward.rs index fd3d1eecf..d1f4df434 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -7,7 +7,6 @@ use hbb_common::{ config::READ_TIMEOUT, futures::{SinkExt, StreamExt}, log, - message_proto::*, protobuf::Message as _, rendezvous_proto::ConnType, tcp, timeout, @@ -15,6 +14,7 @@ use hbb_common::{ tokio_util::codec::{BytesCodec, Framed}, ResultType, Stream, }; +use base::message_proto::*; fn run_rdp(port: u16, name: &str) { std::process::Command::new("cmdkey") @@ -551,7 +551,8 @@ fn take_socket(forward: Framed, mut prebuf: Vec) -> ( /// The controlling side's `enable-port-forward-mux`: on unless set to `N`. pub fn mux_enabled() -> bool { - use hbb_common::config::{keys, option2bool, LocalConfig}; + use hbb_common::config::{option2bool, LocalConfig}; + use base::config::keys; option2bool( keys::OPTION_ENABLE_PORT_FORWARD_MUX, &LocalConfig::get_option(keys::OPTION_ENABLE_PORT_FORWARD_MUX), @@ -816,7 +817,8 @@ mod tests { #[test] fn port_forward_mux_defaults_to_on() { - use hbb_common::config::{keys, option2bool}; + use hbb_common::config::option2bool; + use base::config::keys; // option2bool's fallback branch is also "on unless N", so the value // assertions below would pass for a prefixless key too. The `enable-` // prefix is what actually guarantees the default, and renaming the key diff --git a/src/port_forward_mux.rs b/src/port_forward_mux.rs index ec54f1129..4f0d31b80 100644 --- a/src/port_forward_mux.rs +++ b/src/port_forward_mux.rs @@ -1,7 +1,6 @@ use hbb_common::{ bytes::Bytes, log, - message_proto::*, tokio::{ self, io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, @@ -10,6 +9,7 @@ use hbb_common::{ }, ResultType, }; +use base::message_proto::*; use std::sync::{Arc, Mutex}; /// On the wire and fixed forever: what the controller may have in flight on a @@ -892,7 +892,7 @@ mod tests { #[test] fn frame_builders_set_the_expected_union_variant() { - use hbb_common::message_proto::{message, port_forward_channel}; + use base::message_proto::{message, port_forward_channel}; let m = data_msg(7, Bytes::from_static(b"abc")); match m.union { Some(message::Union::PortForwardChannel(ch)) => match ch.union { @@ -917,7 +917,7 @@ mod tests { } } - use hbb_common::message_proto::{message, port_forward_channel}; + use base::message_proto::{message, port_forward_channel}; use hbb_common::tokio::{self, io::AsyncReadExt, io::AsyncWriteExt, sync::mpsc}; use std::sync::{Arc, Mutex}; diff --git a/src/privacy_mode/win_exclude_from_capture.rs b/src/privacy_mode/win_exclude_from_capture.rs index 7d680011f..854efa7d4 100644 --- a/src/privacy_mode/win_exclude_from_capture.rs +++ b/src/privacy_mode/win_exclude_from_capture.rs @@ -1,4 +1,4 @@ -use hbb_common::platform::windows::is_windows_version_or_greater; +use base::platform::windows::is_windows_version_or_greater; pub use super::win_topmost_window::PrivacyModeImpl; diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 64eb72f37..0436f060a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -14,9 +14,7 @@ use uuid::Uuid; use hbb_common::{ allow_err, anyhow::{self, bail}, - config::{ - self, keys::*, option2bool, use_ws, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT, - }, + config::{self, option2bool, use_ws, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT}, futures::future::join_all, log, protobuf::Message as _, @@ -32,6 +30,7 @@ use hbb_common::{ webrtc::WebRTCStream, AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr, }; +use base::config::keys::*; use crate::{ check_port, diff --git a/src/server.rs b/src/server.rs index c7cac086a..f39620f51 100644 --- a/src/server.rs +++ b/src/server.rs @@ -17,13 +17,13 @@ use hbb_common::{ bail, config::{Config, CONNECT_TIMEOUT, RELAY_PORT}, log, - message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, socket_client, sodiumoxide::crypto::{box_, sign}, timeout, tokio, ResultType, Stream, }; +use base::message_proto::*; use scrap::camera; #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; @@ -597,7 +597,7 @@ pub async fn start_server(is_server: bool, no_server: bool) { log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); } #[cfg(windows)] - hbb_common::platform::windows::start_cpu_performance_monitor(); + base::platform::windows::start_cpu_performance_monitor(); }); if is_server { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 67086fbc3..8fd757c22 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -11,12 +11,14 @@ pub use crate::{ clipboard::{check_clipboard_files, FILE_CLIPBOARD_NAME as FILE_NAME}, clipboard_file::unix_file_clip, }; +#[cfg(target_os = "android")] +use base::config::keys; #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] use clipboard::platform::unix::fuse::{init_fuse_context, uninit_fuse_context}; #[cfg(not(target_os = "android"))] use clipboard_master::CallbackResult; #[cfg(target_os = "android")] -use hbb_common::config::{keys, option2bool}; +use hbb_common::config::option2bool; #[cfg(target_os = "android")] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ diff --git a/src/server/connection.rs b/src/server/connection.rs index 67799646d..fc61420dd 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -30,13 +30,11 @@ use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::{ self, decode_permanent_password_h1_from_storage, decode_preset_password_h1_from_storage, - keys, local_permanent_password_storage_is_usable_for_auth, + local_permanent_password_storage_is_usable_for_auth, preset_permanent_password_storage_is_usable_for_auth, Config, TrustedDevice, }, - fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, get_time, get_version_number, - message_proto::{option_message::BoolOption, permission_info::Permission}, password_security::{self as password, ApproveMode}, sha2::{Digest, Sha256}, sleep, timeout, @@ -47,6 +45,11 @@ use hbb_common::{ }, tokio_util::codec::{BytesCodec, Framed}, }; +use base::{ + config::keys, + fs::{self, can_enable_overwrite_detection, JobType}, + message_proto::{option_message::BoolOption, permission_info::Permission}, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; use scrap::camera; @@ -5980,7 +5983,7 @@ impl Connection { "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, file_transfer_enabled); if !stop { - use hbb_common::config::keys::OPTION_ONE_WAY_FILE_TRANSFER; + use base::config::keys::OPTION_ONE_WAY_FILE_TRANSFER; // Note: Code will not reach here if `crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"` is true. // Because `file-clipboard` service will not be subscribed. // But we still check it here to keep the same logic to windows version in `ui_cm_interface.rs`. diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 8d42214d2..16798b277 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -158,7 +158,7 @@ pub(super) fn set_wayland_layout_baseline(baseline: Vec Vec> { let mut taken = vec![false; wl.len()]; let mut matched: Vec> = vec![None; drm.len()]; @@ -1621,7 +1622,7 @@ fn identity_matches( fn assign_wayland_outputs( drm: &[DrmDisplayInfo], - wl: &[hbb_common::platform::linux::WaylandDisplayInfo], + wl: &[base::platform::linux::WaylandDisplayInfo], ) -> Vec> { let mut matched = identity_matches(drm, wl); let mut taken = vec![false; wl.len()]; @@ -2107,8 +2108,8 @@ mod drm_capturer_tests { y: i32, w: i32, h: i32, - ) -> hbb_common::platform::linux::WaylandDisplayInfo { - hbb_common::platform::linux::WaylandDisplayInfo { + ) -> base::platform::linux::WaylandDisplayInfo { + base::platform::linux::WaylandDisplayInfo { name: name.to_owned(), x, y, diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f8f943276..4eb6c7b76 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -4,14 +4,13 @@ use super::*; use crate::input::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::whiteboard; +use base::message_proto::{ + pointer_device_event::Union::TouchEvent, touch_event::Union::ScaleUpdate, +}; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::{ - get_time, - message_proto::{pointer_device_event::Union::TouchEvent, touch_event::Union::ScaleUpdate}, - protobuf::EnumOrUnknown, -}; +use hbb_common::{get_time, protobuf::EnumOrUnknown}; use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; #[cfg(target_os = "macos")] use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; diff --git a/src/server/port_forward_mux.rs b/src/server/port_forward_mux.rs index cbefa15fb..69bded25f 100644 --- a/src/server/port_forward_mux.rs +++ b/src/server/port_forward_mux.rs @@ -6,10 +6,10 @@ use crate::port_forward_mux::{ use hbb_common::{ bytes::Bytes, log, - message_proto::*, timeout, tokio::{self, net::TcpStream, sync::{mpsc, watch}}, }; +use base::message_proto::*; use std::{ collections::HashMap, sync::{Arc, Mutex}, @@ -262,7 +262,6 @@ mod tests { use super::*; use crate::port_forward_mux::{CHANNEL_WINDOW, INITIAL_WINDOW, MAX_CHANNELS, MIN_FRAME_CHARGE}; use hbb_common::{ - message_proto::{message, port_forward_channel}, tokio::{ self, io::{AsyncReadExt, AsyncWriteExt}, @@ -271,6 +270,7 @@ mod tests { time::Instant, }, }; + use base::message_proto::{message, port_forward_channel}; fn rt() -> tokio::runtime::Runtime { tokio::runtime::Builder::new_current_thread() diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 23b69a70c..0b2601e71 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -6,12 +6,12 @@ use crate::{ validate_path_for_portable_service_shmem_dir, }, }; +use base::message_proto::{KeyEvent, MouseEvent}; use core::slice; use hbb_common::{ allow_err, anyhow::anyhow, bail, libc, log, - message_proto::{KeyEvent, MouseEvent}, protobuf::Message, tokio::{self, sync::mpsc}, ResultType, @@ -435,7 +435,7 @@ mod utils { // functions called in separate SYSTEM user process. pub mod server { - use hbb_common::message_proto::PointerDeviceEvent; + use base::message_proto::PointerDeviceEvent; use crate::display_service; @@ -826,7 +826,8 @@ pub mod server { pub mod client { use super::*; use crate::display_service; - use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent}; + use base::message_proto::PointerDeviceEvent; + use hbb_common::anyhow::Context; use scrap::PixelBuffer; lazy_static::lazy_static! { diff --git a/src/server/rdp_input.rs b/src/server/rdp_input.rs index 546f946c0..2efeff530 100644 --- a/src/server/rdp_input.rs +++ b/src/server/rdp_input.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use std::sync::Arc; pub mod client { - use hbb_common::platform::linux::{DISPLAY_DESKTOP_KDE, XDG_CURRENT_DESKTOP}; + use base::platform::linux::{DISPLAY_DESKTOP_KDE, XDG_CURRENT_DESKTOP}; use super::*; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 9d97b1ce9..e91ecc7d4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -1076,7 +1076,7 @@ fn get_recorder( #[cfg(target_os = "android")] fn check_change_scale(hardware: bool) -> ResultType<()> { - use hbb_common::config::keys::OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE as SCALE_SOFT; + use base::config::keys::OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE as SCALE_SOFT; // isStart flag is set at the end of startCapture() in Android, wait it to be set. let n = 60; // 3s diff --git a/src/server/wayland.rs b/src/server/wayland.rs index ac803369f..d48a215a4 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,5 +1,6 @@ use super::*; -use hbb_common::{allow_err, anyhow, platform::linux::DISTRO}; +use hbb_common::{allow_err, anyhow}; +use base::platform::linux::DISTRO; use scrap::{ is_cursor_embedded, set_map_err, wayland::pipewire::{fill_displays, try_fix_logical_size}, diff --git a/src/tray.rs b/src/tray.rs index f585b3f42..50e2de05b 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -4,12 +4,13 @@ use crate::ipc::Data; #[cfg(windows)] use hbb_common::tokio; use hbb_common::{allow_err, log}; +use base::config::keys; use std::sync::{Arc, Mutex}; #[cfg(windows)] use std::time::Duration; pub fn start_tray() { - if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { + if crate::ui_interface::get_builtin_option(keys::OPTION_HIDE_TRAY) == "Y" { #[cfg(not(target_os = "macos"))] { return; @@ -64,7 +65,7 @@ fn make_tray() -> hbb_common::ResultType<()> { let tray_menu = Menu::new(); let hide_stop_service = crate::ui_interface::get_builtin_option( - hbb_common::config::keys::OPTION_HIDE_STOP_SERVICE, + keys::OPTION_HIDE_STOP_SERVICE, ) == "Y"; // The tray icon is only shown when the service is running, so we don't need to check // the `stop-service` option here. @@ -147,7 +148,7 @@ fn make_tray() -> hbb_common::ResultType<()> { if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event { // for fixing https://github.com/rustdesk/rustdesk/discussions/10210#discussioncomment-14600745 // so we start tray, but not to show it - if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { + if crate::ui_interface::get_builtin_option(keys::OPTION_HIDE_TRAY) == "Y" { return; } // We create the icon once the event loop is actually running diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3a2cca3e0..2f36f5d81 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -15,7 +15,10 @@ use sciter::{ }; use hbb_common::{ - allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, + allow_err, log, rendezvous_proto::ConnType, +}; +use base::{ + fs::TransferJobMeta, message_proto::*, }; use crate::{ diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c659170e3..ee64eaaf5 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -5,20 +5,24 @@ use crate::ipc::{self, Data}; #[cfg(target_os = "windows")] use crate::{clipboard::ClipboardSide, ipc::ClipboardNonFile}; #[cfg(target_os = "windows")] -use clipboard::ContextSend; +use base::config::keys::*; #[cfg(not(any(target_os = "ios")))] -use hbb_common::fs::serialize_transfer_job; +use base::fs::serialize_transfer_job; +use base::{ + config::keys::{OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, OPTION_FILE_TRANSFER_MAX_FILES}, + fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult}, + message_proto::*, +}; +#[cfg(target_os = "windows")] +use clipboard::ContextSend; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::unbounded_channel; +#[cfg(target_os = "windows")] +use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::{ allow_err, bail, - config::{ - keys::{OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, OPTION_FILE_TRANSFER_MAX_FILES}, - option2bool, Config, - }, - fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult}, + config::{option2bool, Config}, log, - message_proto::*, protobuf::Message as _, tokio::{ self, @@ -27,8 +31,6 @@ use hbb_common::{ }, ResultType, }; -#[cfg(target_os = "windows")] -use hbb_common::{config::keys::*, tokio::sync::Mutex as TokioMutex}; use serde_derive::Serialize; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use std::iter::FromIterator; @@ -1400,7 +1402,7 @@ async fn start_read_job( /// Process read jobs periodically, reading file blocks and sending them via IPC. /// /// NOTE: This is the CM-side equivalent of `handle_read_jobs()` in -/// `libs/hbb_common/src/fs.rs`. The logic mirrors that implementation +/// `libs/base/src/fs.rs`. The logic mirrors that implementation /// but communicates via IPC instead of direct network stream. /// When modifying job processing logic, ensure both implementations stay in sync. #[cfg(not(any(target_os = "ios")))] @@ -1499,7 +1501,7 @@ async fn handle_read_jobs_tick( /// Initialize a read job's data stream and handle digest sending for overwrite detection. /// /// NOTE: This is the CM-side equivalent of `TransferJob::init_data_stream()` in -/// `libs/hbb_common/src/fs.rs`. It calls `init_data_stream_for_cm()` and sends +/// `libs/base/src/fs.rs`. It calls `init_data_stream_for_cm()` and sends /// digest via IPC instead of direct network stream. /// When modifying initialization or digest logic, ensure both paths stay in sync. #[cfg(not(any(target_os = "ios")))] @@ -1777,10 +1779,8 @@ mod tests { use super::*; use crate::ipc::Data; - use hbb_common::{ - message_proto::{FileDirectory, Message}, - tokio::{runtime::Runtime, sync::mpsc::unbounded_channel}, - }; + use base::message_proto::{FileDirectory, Message}; + use hbb_common::tokio::{runtime::Runtime, sync::mpsc::unbounded_channel}; use std::fs; #[test] diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 94fde4392..b1eeefde2 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1,9 +1,10 @@ +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, keys::*, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT}, + config::{self, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT}, directories_next, futures::future::join_all, log, @@ -185,11 +186,11 @@ pub fn use_texture_render() -> bool { #[cfg(target_os = "macos")] return cfg!(feature = "flutter") - && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + && LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) == "Y"; #[cfg(target_os = "linux")] return cfg!(feature = "flutter") - && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"; + && LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) != "N"; #[cfg(target_os = "windows")] { @@ -202,9 +203,9 @@ pub fn use_texture_render() -> bool { #[cfg(not(debug_assertions))] let default_texture = crate::platform::is_win_10_or_greater(); if default_texture { - LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" + LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) != "N" } else { - return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + return LocalConfig::get_option(keys::OPTION_TEXTURE_RENDER) == "Y"; } } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index fc09ed5e0..37aa7a67e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -7,16 +7,16 @@ use crate::{ ui_interface::use_texture_render, }; use async_trait::async_trait; -use bytes::Bytes; #[cfg(all(target_os = "windows", not(feature = "flutter")))] -use hbb_common::config::keys; +use base::config::keys; #[cfg(not(feature = "flutter"))] -use hbb_common::fs; +use base::fs; +use base::message_proto::*; +use bytes::Bytes; use hbb_common::{ allow_err, config::{Config, LocalConfig, PeerConfig}, get_version_number, log, - message_proto::*, rendezvous_proto::ConnType, tokio::{ self, @@ -1666,7 +1666,7 @@ impl Session { let to = std::env::temp_dir().join(format!("rustdesk_printer_{id}")); self.send(Data::SendFiles(( id, - hbb_common::fs::JobType::Printer, + base::fs::JobType::Printer, path, to.to_string_lossy().to_string(), 0, @@ -1908,7 +1908,7 @@ impl Interface for Session { } } - fn swap_modifier_mouse(&self, msg: &mut hbb_common::protos::message::MouseEvent) { + fn swap_modifier_mouse(&self, msg: &mut base::protos::message::MouseEvent) { let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); if allow_swap_key { msg.modifiers = msg diff --git a/src/updater.rs b/src/updater.rs index beab97e53..0c1784000 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,5 +1,6 @@ use crate::{common::do_check_software_update, hbbs_http::create_http_client_with_url_strict}; use hbb_common::{bail, config, log, ResultType}; +use base::config::keys; use std::{ io::Write, path::{Component, Path, PathBuf}, @@ -181,7 +182,7 @@ fn check_update(manually: bool) -> ResultType<()> { } #[cfg(target_os = "windows")] let update_msi = crate::platform::is_msi_installed()? && !crate::is_custom_client(); - if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) { + if !(manually || config::Config::get_bool_option(keys::OPTION_ALLOW_AUTO_UPDATE)) { return Ok(()); } if do_check_software_update().is_err() { @@ -535,7 +536,7 @@ pub fn start_auto_update_macos() { pub fn check_update_as_root() -> ResultType { let _update_lock = acquire_mac_update_lock()?; // Allow-auto-update setting - if !config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE) { + if !config::Config::get_bool_option(keys::OPTION_ALLOW_AUTO_UPDATE) { log::info!("[root-update] Auto update is disabled, skipping."); return Ok(false); } diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index f0645e4bf..c7bbd518f 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -1,4 +1,5 @@ -use hbb_common::{bail, platform::windows::is_windows_version_or_greater, ResultType}; +use base::platform::windows::is_windows_version_or_greater; +use hbb_common::{bail, ResultType}; // This string is defined here. // https://github.com/rustdesk-org/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40