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